diff options
Diffstat (limited to 'autoload')
-rw-r--r-- | autoload/fsharp.vim | 517 | ||||
-rw-r--r-- | autoload/ledger.vim | 744 | ||||
-rw-r--r-- | autoload/nim.vim | 244 |
3 files changed, 1505 insertions, 0 deletions
diff --git a/autoload/fsharp.vim b/autoload/fsharp.vim new file mode 100644 index 00000000..6816d822 --- /dev/null +++ b/autoload/fsharp.vim @@ -0,0 +1,517 @@ +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://ci.appveyor.com/api/projects/fsautocomplete/fsautocomplete/artifacts/bin/pkgs/fsautocomplete.netcore.zip?branch=' . a:branch . '"' + \ ) + 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 diff --git a/autoload/ledger.vim b/autoload/ledger.vim new file mode 100644 index 00000000..225f96a3 --- /dev/null +++ b/autoload/ledger.vim @@ -0,0 +1,744 @@ +if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'ledger') == -1 + +scriptencoding utf-8 +" vim:ts=2:sw=2:sts=2:foldmethod=marker +function! ledger#transaction_state_toggle(lnum, ...) abort + if a:0 == 1 + let chars = a:1 + else + let chars = ' *' + endif + let trans = s:transaction.from_lnum(a:lnum) + if empty(trans) || has_key(trans, 'expr') + return + endif + + let old = has_key(trans, 'state') ? trans['state'] : ' ' + let i = stridx(chars, old) + 1 + let new = chars[i >= len(chars) ? 0 : i] + + call trans.set_state(new) + + call setline(trans['head'], trans.format_head()) +endf + +function! ledger#transaction_state_set(lnum, char) abort + " modifies or sets the state of the transaction at the cursor, + " removing the state altogether if a:char is empty + let trans = s:transaction.from_lnum(a:lnum) + if empty(trans) || has_key(trans, 'expr') + return + endif + + call trans.set_state(a:char) + + call setline(trans['head'], trans.format_head()) +endf + +function! ledger#transaction_date_set(lnum, type, ...) abort + let time = a:0 == 1 ? a:1 : localtime() + let trans = s:transaction.from_lnum(a:lnum) + if empty(trans) || has_key(trans, 'expr') + return + endif + + let formatted = strftime(g:ledger_date_format, time) + if has_key(trans, 'date') && ! empty(trans['date']) + let date = split(trans['date'], '=') + else + let date = [formatted] + endif + + if a:type =~? 'effective\|actual' + echoerr 'actual/effective arguments were replaced by primary/auxiliary' + return + endif + + if a:type ==? 'primary' + let date[0] = formatted + elseif a:type ==? 'auxiliary' + if time < 0 + " remove auxiliary date + let date = [date[0]] + else + " set auxiliary date + if len(date) >= 2 + let date[1] = formatted + else + call add(date, formatted) + endif + endif + elseif a:type ==? 'unshift' + let date = [formatted, date[0]] + endif + + let trans['date'] = join(date[0:1], '=') + + call setline(trans['head'], trans.format_head()) +endf + +" == get transactions == + +function! ledger#transaction_from_lnum(lnum) abort + return s:transaction.from_lnum(a:lnum) +endf + +function! ledger#transactions(...) abort + if a:0 == 2 + let lnum = a:1 + let end = a:2 + elseif a:0 == 0 + let lnum = 1 + let end = line('$') + else + throw 'wrong number of arguments for get_transactions()' + return [] + endif + + " safe view / position + let view = winsaveview() + let fe = &foldenable + set nofoldenable + + let transactions = [] + call cursor(lnum, 0) + while lnum && lnum < end + let trans = s:transaction.from_lnum(lnum) + if ! empty(trans) + call add(transactions, trans) + call cursor(trans['tail'], 0) + endif + let lnum = search('^[~=[:digit:]]', 'cW') + endw + + " restore view / position + let &foldenable = fe + call winrestview(view) + + return transactions +endf + +" == transaction object implementation == + +let s:transaction = {} "{{{1 +function! s:transaction.new() abort dict + return copy(s:transaction) +endf + +function! s:transaction.from_lnum(lnum) abort dict "{{{2 + let [head, tail] = s:get_transaction_extents(a:lnum) + if ! head + return {} + endif + + let trans = copy(s:transaction) + let trans['head'] = head + let trans['tail'] = tail + + " split off eventual comments at the end of line + let line = split(getline(head), '\ze\s*\%(\t\| \);', 1) + if len(line) > 1 + let trans['appendix'] = join(line[1:], '') + endif + + " parse rest of line + " FIXME (minor): will not preserve spacing (see 'join(parts)') + let parts = split(line[0], '\s\+') + if parts[0] ==# '~' + let trans['expr'] = join(parts[1:]) + return trans + elseif parts[0] ==# '=' + let trans['auto'] = join(parts[1:]) + return trans + elseif parts[0] !~# '^\d' + " this case is avoided in s:get_transaction_extents(), + " but we'll check anyway. + return {} + endif + + for part in parts + if ! has_key(trans, 'date') && part =~# '^\d' + let trans['date'] = part + elseif ! has_key(trans, 'code') && part =~# '^([^)]*)$' + let trans['code'] = part[1:-2] + elseif ! has_key(trans, 'state') && part =~# '^[[:punct:]]$' + " the first character by itself is assumed to be the state of the transaction. + let trans['state'] = part + else + " everything after date/code or state belongs to the description + break + endif + call remove(parts, 0) + endfor + + let trans['description'] = join(parts) + return trans +endf "}}} + +function! s:transaction.set_state(char) abort dict "{{{2 + if has_key(self, 'state') && a:char =~# '^\s*$' + call remove(self, 'state') + else + let self['state'] = a:char + endif +endf "}}} + +function! s:transaction.parse_body(...) abort dict "{{{2 + if a:0 == 2 + let head = a:1 + let tail = a:2 + elseif a:0 == 0 + let head = self['head'] + let tail = self['tail'] + else + throw 'wrong number of arguments for parse_body()' + return [] + endif + + if ! head || tail <= head + return [] + endif + + let lnum = head + let tags = {} + let postings = [] + while lnum <= tail + let line = split(getline(lnum), '\s*\%(\t\| \);', 1) + + if line[0] =~# '^\s\+[^[:blank:];]' + " posting + let [state, rest] = matchlist(line[0], '^\s\+\([*!]\?\)\s*\(.*\)$')[1:2] + if rest =~# '\t\| ' + let [account, amount] = matchlist(rest, '^\(.\{-}\)\%(\t\| \)\s*\(.\{-}\)\s*$')[1:2] + else + let amount = '' + let account = matchstr(rest, '^\s*\zs.\{-}\ze\s*$') + endif + call add(postings, {'account': account, 'amount': amount, 'state': state}) + end + + " where are tags to be stored? + if empty(postings) + " they belong to the transaction + let tag_container = tags + else + " they belong to last posting + if ! has_key(postings[-1], 'tags') + let postings[-1]['tags'] = {} + endif + let tag_container = postings[-1]['tags'] + endif + + let comment = join(line[1:], ' ;') + if comment =~# '^\s*:' + " tags without values + for t in s:findall(comment, ':\zs[^:[:blank:]]\([^:]*[^:[:blank:]]\)\?\ze:') + let tag_container[t] = '' + endfor + elseif comment =~# '^\s*[^:[:blank:]][^:]\+:' + " tag with value + let key = matchstr(comment, '^\s*\zs[^:]\+\ze:') + if ! empty(key) + let val = matchstr(comment, ':\s*\zs.*\ze\s*$') + let tag_container[key] = val + endif + endif + let lnum += 1 + endw + return [tags, postings] +endf "}}} + +function! s:transaction.format_head() abort dict "{{{2 + if has_key(self, 'expr') + return '~ '.self['expr'] + elseif has_key(self, 'auto') + return '= '.self['auto'] + endif + + let parts = [] + if has_key(self, 'date') | call add(parts, self['date']) | endif + if has_key(self, 'state') | call add(parts, self['state']) | endif + if has_key(self, 'code') | call add(parts, '('.self['code'].')') | endif + if has_key(self, 'description') | call add(parts, self['description']) | endif + + let line = join(parts) + if has_key(self, 'appendix') | let line .= self['appendix'] | endif + + return line +endf "}}} +"}}} + +" == helper functions == + +" get a list of declared accounts in the buffer +function! ledger#declared_accounts(...) abort + if a:0 == 2 + let lnum = a:1 + let lend = a:2 + elseif a:0 == 0 + let lnum = 1 + let lend = line('$') + else + throw 'wrong number of arguments for ledger#declared_accounts()' + return [] + endif + + " save view / position + let view = winsaveview() + let fe = &foldenable + set nofoldenable + + let accounts = [] + call cursor(lnum, 0) + while 1 + let lnum = search('^account\s', 'cW', lend) + if !lnum || lnum > lend + break + endif + + " remove comments at the end and "account" at the front + let line = split(getline(lnum), '\s\+;')[0] + let line = matchlist(line, 'account\s\+\(.\+\)')[1] + + if len(line) > 1 + call add(accounts, line) + endif + + call cursor(lnum+1,0) + endw + + " restore view / position + let &foldenable = fe + call winrestview(view) + + return accounts +endf + +function! s:get_transaction_extents(lnum) abort + if ! (indent(a:lnum) || getline(a:lnum) =~# '^[~=[:digit:]]') + " only do something if lnum is in a transaction + return [0, 0] + endif + + " safe view / position + let view = winsaveview() + let fe = &foldenable + set nofoldenable + + call cursor(a:lnum, 0) + let head = search('^[~=[:digit:]]', 'bcnW') + let tail = search('^[^;[:blank:]]\S\+', 'nW') + let tail = tail > head ? tail - 1 : line('$') + + " restore view / position + let &foldenable = fe + call winrestview(view) + + return head ? [head, tail] : [0, 0] +endf + +function! ledger#find_in_tree(tree, levels) abort + if empty(a:levels) + return [] + endif + let results = [] + let currentlvl = a:levels[0] + let nextlvls = a:levels[1:] + let branches = ledger#filter_items(keys(a:tree), currentlvl) + for branch in branches + let exact = empty(nextlvls) + call add(results, [branch, exact]) + if ! empty(nextlvls) + for [result, exact] in ledger#find_in_tree(a:tree[branch], nextlvls) + call add(results, [branch.':'.result, exact]) + endfor + endif + endfor + return results +endf + +function! ledger#filter_items(list, keyword) abort + " return only those items that start with a specified keyword + return filter(copy(a:list), 'v:val =~ ''^\V'.substitute(a:keyword, '\\', '\\\\', 'g').'''') +endf + +function! s:findall(text, rx) abort + " returns all the matches in a string, + " there will be overlapping matches according to :help match() + let matches = [] + + while 1 + let m = matchstr(a:text, a:rx, 0, len(matches)+1) + if empty(m) + break + endif + + call add(matches, m) + endw + + return matches +endf + +" Move the cursor to the specified column, filling the line with spaces if necessary. +" Ensure that at least min_spaces are added, and go to the end of the line if +" the line is already too long +function! s:goto_col(pos, min_spaces) abort + exec 'normal!' '$' + let diff = max([a:min_spaces, a:pos - virtcol('.')]) + if diff > 0 | exec 'normal!' diff . 'a ' | endif +endf + +" Return character position of decimal separator (multibyte safe) +function! s:decimalpos(expr) abort + let pos = match(a:expr, '\V' . g:ledger_decimal_sep) + if pos > 0 + let pos = strchars(a:expr[:pos]) - 1 + endif + return pos +endf + +" Align the amount expression after an account name at the decimal point. +" +" This function moves the amount expression of a posting so that the decimal +" separator is aligned at the column specified by g:ledger_align_at. +" +" For example, after selecting: +" +" 2015/05/09 Some Payee +" Expenses:Other $120,23 ; Tags here +" Expenses:Something $-4,99 +" Expenses:More ($12,34 + $16,32) +" +" :'<,'>call ledger#align_commodity() produces: +" +" 2015/05/09 Some Payee +" Expenses:Other $120,23 ; Tags here +" Expenses:Something $-4,99 +" Expenses:More ($12,34 + $16,32) +" +function! ledger#align_commodity() abort + " Extract the part of the line after the account name (excluding spaces): + let l:line = getline('.') + let rhs = matchstr(l:line, '\m^\s\+[^;[:space:]].\{-}\(\t\| \)\s*\zs.*$') + if rhs !=# '' + " Remove everything after the account name (including spaces): + call setline('.', substitute(l:line, '\m^\s\+[^[:space:]].\{-}\zs\(\t\| \).*$', '', '')) + let pos = -1 + if g:ledger_decimal_sep !=# '' + " Find the position of the first decimal separator: + let pos = s:decimalpos(rhs) + endif + if pos < 0 + " Find the position after the first digits + let pos = matchend(rhs, '\m\d[^[:space:]]*') + endif + " Go to the column that allows us to align the decimal separator at g:ledger_align_at: + if pos > 0 + call s:goto_col(g:ledger_align_at - pos - 1, 2) + else + call s:goto_col(g:ledger_align_at - strdisplaywidth(rhs) - 2, 2) + endif " Append the part of the line that was previously removed: + exe 'normal! a' . rhs + endif +endf + +" Align the amount under the cursor and append/prepend the default currency. +function! ledger#align_amount_at_cursor() abort + " Select and cut text: + normal! viWd + " Find the position of the decimal separator + let pos = s:decimalpos(@") " Returns zero when the separator is the empty string + if pos <= 0 + let pos = len(@") + endif + " Paste text at the correct column and append/prepend default commodity: + if g:ledger_commodity_before + call s:goto_col(g:ledger_align_at - pos - len(g:ledger_default_commodity) - len(g:ledger_commodity_sep) - 1, 2) + exe 'normal! a' . g:ledger_default_commodity . g:ledger_commodity_sep + normal! p + else + call s:goto_col(g:ledger_align_at - pos - 1, 2) + exe 'normal! pa' . g:ledger_commodity_sep . g:ledger_default_commodity + endif +endf + +" Report generation {{{1 + +" Helper functions and variables {{{2 +" Position of report windows +let s:winpos_map = { + \ 'T': 'to new', 't': 'abo new', 'B': 'bo new', 'b': 'bel new', + \ 'L': 'to vnew', 'l': 'abo vnew', 'R': 'bo vnew', 'r': 'bel vnew' + \ } + +function! s:error_message(msg) abort + redraw " See h:echo-redraw + echohl ErrorMsg + echo "\r" + echomsg a:msg + echohl NONE +endf + +function! s:warning_message(msg) abort + redraw " See h:echo-redraw + echohl WarningMsg + echo "\r" + echomsg a:msg + echohl NONE +endf + +" Open the quickfix/location window when it is not empty, +" closes it if it is empty. +" +" Optional parameters: +" a:1 Quickfix window title. +" a:2 Message to show when the window is empty. +" +" Returns 0 if the quickfix window is empty, 1 otherwise. +function! s:quickfix_toggle(...) abort + if g:ledger_use_location_list + let l:list = 'l' + let l:open = (len(getloclist(winnr())) > 0) + else + let l:list = 'c' + let l:open = (len(getqflist()) > 0) + endif + + if l:open + execute (g:ledger_qf_vertical ? 'vert' : 'botright') l:list.'open' g:ledger_qf_size + " Set local mappings to quit the quickfix window or lose focus. + nnoremap <silent> <buffer> <tab> <c-w><c-w> + execute 'nnoremap <silent> <buffer> q :' l:list.'close<CR>' + " Note that the following settings do not persist (e.g., when you close and re-open the quickfix window). + " See: https://superuser.com/questions/356912/how-do-i-change-the-quickix-title-status-bar-in-vim + if g:ledger_qf_hide_file + setl conceallevel=2 + setl concealcursor=nc + syntax match qfFile /^[^|]*/ transparent conceal + endif + if a:0 > 0 + let w:quickfix_title = a:1 + endif + return 1 + endif + + execute l:list.'close' + call s:warning_message((a:0 > 1) ? a:2 : 'No results') + return 0 +endf + +" Populate a quickfix/location window with data. The argument must be a String +" or a List. +function! s:quickfix_populate(data) abort + " Note that cexpr/lexpr always uses the global value of errorformat + let l:efm = &errorformat " Save global errorformat + set errorformat=%EWhile\ parsing\ file\ \"%f\"\\,\ line\ %l:,%ZError:\ %m,%-C%.%# + set errorformat+=%tarning:\ \"%f\"\\,\ line\ %l:\ %m + " Format to parse command-line errors: + set errorformat+=Error:\ %m + " Format to parse reports: + set errorformat+=%f:%l\ %m + set errorformat+=%-G%.%# + execute (g:ledger_use_location_list ? 'l' : 'c').'getexpr' 'a:data' + let &errorformat = l:efm " Restore global errorformat + return +endf + +" Build a ledger command to process the given file. +function! s:ledger_cmd(file, args) abort + let l:options = g:ledger_extra_options + if len(g:ledger_date_format) > 0 && !g:ledger_is_hledger + let l:options = join([l:options, '--date-format', g:ledger_date_format, + \ '--input-date-format', g:ledger_date_format]) + endif + return join([g:ledger_bin, l:options, '-f', shellescape(expand(a:file)), a:args]) +endf +" }}} + +function! ledger#autocomplete_and_align() abort + if pumvisible() + return "\<c-n>" + endif + " Align an amount only if there is a digit immediately before the cursor and + " such digit is preceded by at least one space (the latter condition is + " necessary to avoid situations where a date starting at the first column is + " confused with a commodity to be aligned). + if match(getline('.'), '\s.*\d\%'.col('.').'c') > -1 + normal! h + call ledger#align_amount_at_cursor() + return "\<c-o>A" + endif + return "\<c-x>\<c-o>" +endf + +" Use current line as input to ledger entry and replace with output. If there +" are errors, they are echoed instead. +function! ledger#entry() abort + let l:output = systemlist(s:ledger_cmd(g:ledger_main, join(['entry', getline('.')]))) + " Filter out warnings + let l:output = filter(l:output, "v:val !~? '^Warning: '") + " Errors may occur + if v:shell_error + echomsg join(l:output) + return + endif + " Append output so we insert instead of overwrite, then delete line + call append('.', l:output) + normal! "_dd +endfunc + +" Run an arbitrary ledger command and show the output in a new buffer. If +" there are errors, no new buffer is opened: the errors are displayed in a +" quickfix window instead. +" +" Parameters: +" file The file to be processed. +" args A string of Ledger command-line arguments. +" +" Returns: +" Ledger's output as a String. +function! ledger#report(file, args) abort + let l:output = systemlist(s:ledger_cmd(a:file, a:args)) + if v:shell_error " If there are errors, show them in a quickfix/location list. + call s:quickfix_populate(l:output) + call s:quickfix_toggle('Errors', 'Unable to parse errors') + endif + return l:output +endf + +" Open the output of a Ledger's command in a new buffer. +" +" Parameters: +" report A String containing the output of a Ledger's command. +" +" Returns: +" 1 if a new buffer is created; 0 otherwise. +function! ledger#output(report) abort + if empty(a:report) + call s:warning_message('No results') + return 0 + endif + " Open a new buffer to show Ledger's output. + execute get(s:winpos_map, g:ledger_winpos, 'bo new') + setlocal buftype=nofile bufhidden=wipe modifiable nobuflisted noswapfile nowrap + call append(0, a:report) + setlocal nomodifiable + " Set local mappings to quit window or lose focus. + nnoremap <silent> <buffer> <tab> <c-w><c-p> + nnoremap <silent> <buffer> q <c-w><c-p>@=winnr('#')<cr><c-w>c + " Add some coloring to the report + syntax match LedgerNumber /-\@1<!\d\+\([,.]\d\+\)*/ + syntax match LedgerNegativeNumber /-\d\+\([,.]\d\+\)*/ + syntax match LedgerImproperPerc /\d\d\d\+%/ + return 1 +endf + +" Show an arbitrary register report in a quickfix list. +" +" Parameters: +" file The file to be processed +" args A string of Ledger command-line arguments. +function! ledger#register(file, args) abort + let l:cmd = s:ledger_cmd(a:file, join([ + \ 'register', + \ "--format='" . g:ledger_qf_register_format . "'", + \ "--prepend-format='%(filename):%(beg_line) '", + \ a:args + \ ])) + call s:quickfix_populate(systemlist(l:cmd)) + call s:quickfix_toggle('Register report') +endf + +" Reconcile the given account. +" This function accepts a file path as a third optional argument. +" The default is to use the value of g:ledger_main. +" +" Parameters: +" file The file to be processed +" account An account name (String) +" target_amount The target amount (Float) +function! ledger#reconcile(file, account, target_amount) abort + let l:cmd = s:ledger_cmd(a:file, join([ + \ 'register', + \ '--uncleared', + \ "--format='" . g:ledger_qf_reconcile_format . "'", + \ "--prepend-format='%(filename):%(beg_line) %(pending ? \"P\" : \"U\") '", + \ shellescape(a:account) + \ ])) + let l:file = expand(a:file) " Needed for #show_balance() later + call s:quickfix_populate(systemlist(l:cmd)) + if s:quickfix_toggle('Reconcile ' . a:account, 'Nothing to reconcile') + let g:ledger_target_amount = a:target_amount + " Show updated account balance upon saving, as long as the quickfix window is open + augroup reconcile + autocmd! + execute "autocmd BufWritePost *.ldg,*.ledger call ledger#show_balance('" . l:file . "','" . a:account . "')" + autocmd BufWipeout <buffer> call <sid>finish_reconciling() + augroup END + " Add refresh shortcut + execute "nnoremap <silent> <buffer> <c-l> :<c-u>call ledger#reconcile('" + \ . l:file . "','" . a:account . "'," . string(a:target_amount) . ')<cr>' + call ledger#show_balance(l:file, a:account) + endif +endf + +function! s:finish_reconciling() abort + unlet g:ledger_target_amount + augroup reconcile + autocmd! + augroup END + augroup! reconcile +endf + +" Show the pending/cleared balance of an account. +" This function has an optional parameter: +" +" a:1 An account name +" +" If no account if given, the account in the current line is used. +function! ledger#show_balance(file, ...) abort + let l:account = a:0 > 0 && !empty(a:1) ? a:1 : matchstr(getline('.'), '\m\( \|\t\)\zs\S.\{-}\ze\( \|\t\|$\)') + if empty(l:account) + call s:error_message('No account found') + return + endif + let l:cmd = s:ledger_cmd(a:file, join([ + \ 'cleared', + \ shellescape(l:account), + \ '--empty', + \ '--collapse', + \ "--format='%(scrub(get_at(display_total, 0)))|%(scrub(get_at(display_total, 1)))|%(quantity(scrub(get_at(display_total, 1))))'", + \ (empty(g:ledger_default_commodity) ? '' : '-X ' . shellescape(g:ledger_default_commodity)) + \ ])) + let l:output = systemlist(l:cmd) + " Errors may occur, for example, when the account has multiple commodities + " and g:ledger_default_commodity is empty. + if v:shell_error + call s:quickfix_populate(l:output) + call s:quickfix_toggle('Errors', 'Unable to parse errors') + return + endif + let l:amounts = split(l:output[-1], '|') + redraw " Necessary in some cases to overwrite previous messages. See :h echo-redraw + if len(l:amounts) < 3 + call s:error_message('Could not determine balance. Did you use a valid account?') + return + endif + echo g:ledger_pending_string + echohl LedgerPending + echon l:amounts[0] + echohl NONE + echon ' ' g:ledger_cleared_string + echohl LedgerCleared + echon l:amounts[1] + echohl NONE + if exists('g:ledger_target_amount') + echon ' ' g:ledger_target_string + echohl LedgerTarget + echon printf('%.2f', (g:ledger_target_amount - str2float(l:amounts[2]))) + echohl NONE + endif +endf +" }}} + +endif diff --git a/autoload/nim.vim b/autoload/nim.vim new file mode 100644 index 00000000..59f9122d --- /dev/null +++ b/autoload/nim.vim @@ -0,0 +1,244 @@ +if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'nim') == -1 + +let g:nim_log = [] +let s:plugin_path = escape(expand('<sfile>:p:h'), '\') + +if !exists("g:nim_caas_enabled") + let g:nim_caas_enabled = 0 +endif + +if !executable('nim') + echoerr "the Nim compiler must be in your system's PATH" +endif + +if has("python3") + exe 'py3file ' . fnameescape(s:plugin_path) . '/nim_vim.py' +elseif has("python") + exe 'pyfile ' . fnameescape(s:plugin_path) . '/nim_vim.py' +endif + +fun! nim#init() + let cmd = printf("nim --dump.format:json --verbosity:0 dump %s", s:CurrentNimFile()) + let raw_dumpdata = system(cmd) + if !v:shell_error && expand("%:e") == "nim" + let false = 0 " Needed for eval of json + let true = 1 " Needed for eval of json + let dumpdata = eval(substitute(raw_dumpdata, "\n", "", "g")) + + let b:nim_project_root = dumpdata['project_path'] + let b:nim_defined_symbols = dumpdata['defined_symbols'] + let b:nim_caas_enabled = g:nim_caas_enabled || index(dumpdata['defined_symbols'], 'forcecaas') != -1 + + for path in dumpdata['lib_paths'] + if finddir(path) == path + let &l:path = path . "," . &l:path + endif + endfor + else + let b:nim_caas_enabled = 0 + endif +endf + +fun! s:UpdateNimLog() + setlocal buftype=nofile + setlocal bufhidden=hide + setlocal noswapfile + + for entry in g:nim_log + call append(line('$'), split(entry, "\n")) + endfor + + let g:nim_log = [] + + match Search /^nim\ .*/ +endf + +augroup NimVim + au! + au BufEnter log://nim call s:UpdateNimLog() + if has("python3") || has("python") + " au QuitPre * :py nimTerminateAll() + au VimLeavePre * :py nimTerminateAll() + endif +augroup END + +command! NimLog :e log://nim + +command! NimTerminateService + \ :exe printf("py nimTerminateService('%s')", b:nim_project_root) + +command! NimRestartService + \ :exe printf("py nimRestartService('%s')", b:nim_project_root) + +fun! s:CurrentNimFile() + let save_cur = getpos('.') + call cursor(0, 0, 0) + + let PATTERN = "\\v^\\#\\s*included from \\zs.*\\ze" + let l = search(PATTERN, "n") + + if l != 0 + let f = matchstr(getline(l), PATTERN) + let l:to_check = expand('%:h') . "/" . f + else + let l:to_check = expand("%") + endif + + call setpos('.', save_cur) + return l:to_check +endf + +let g:nim_symbol_types = { + \ 'skParam': 'v', + \ 'skVar': 'v', + \ 'skLet': 'v', + \ 'skTemp': 'v', + \ 'skForVar': 'v', + \ 'skConst': 'v', + \ 'skResult': 'v', + \ 'skGenericParam': 't', + \ 'skType': 't', + \ 'skField': 'm', + \ 'skProc': 'f', + \ 'skMethod': 'f', + \ 'skIterator': 'f', + \ 'skConverter': 'f', + \ 'skMacro': 'f', + \ 'skTemplate': 'f', + \ 'skEnumField': 'v', + \ } + +fun! NimExec(op) + let isDirty = getbufvar(bufnr('%'), "&modified") + if isDirty + let tmp = tempname() . bufname("%") . "_dirty.nim" + silent! exe ":w " . tmp + + let cmd = printf("idetools %s --trackDirty:\"%s,%s,%d,%d\" \"%s\"", + \ a:op, tmp, expand('%:p'), line('.'), col('.')-1, s:CurrentNimFile()) + else + let cmd = printf("idetools %s --track:\"%s,%d,%d\" \"%s\"", + \ a:op, expand('%:p'), line('.'), col('.')-1, s:CurrentNimFile()) + endif + + if b:nim_caas_enabled + exe printf("py nimExecCmd('%s', '%s', False)", b:nim_project_root, cmd) + let output = l:py_res + else + let output = system("nim " . cmd) + endif + + call add(g:nim_log, "nim " . cmd . "\n" . output) + return output +endf + +fun! NimExecAsync(op, Handler) + let result = NimExec(a:op) + call a:Handler(result) +endf + +fun! NimComplete(findstart, base) + if b:nim_caas_enabled == 0 + return -1 + endif + + if a:findstart + if synIDattr(synIDtrans(synID(line("."),col("."),1)), "name") == 'Comment' + return -1 + endif + let line = getline('.') + let start = col('.') - 1 + while start > 0 && line[start - 1] =~? '\w' + let start -= 1 + endwhile + return start + else + let result = [] + let sugOut = NimExec("--suggest") + for line in split(sugOut, '\n') + let lineData = split(line, '\t') + if len(lineData) > 0 && lineData[0] == "sug" + let word = split(lineData[2], '\.')[-1] + if a:base ==? '' || word =~# '^' . a:base + let kind = get(g:nim_symbol_types, lineData[1], '') + let c = { 'word': word, 'kind': kind, 'menu': lineData[3], 'dup': 1 } + call add(result, c) + endif + endif + endfor + return result + endif +endf + +if !exists("g:neocomplcache_omni_patterns") + let g:neocomplcache_omni_patterns = {} +endif +let g:neocomplcache_omni_patterns['nim'] = '[^. *\t]\.\w*' + +if !exists('g:neocomplete#sources#omni#input_patterns') + let g:neocomplete#sources#omni#input_patterns = {} +endif +let g:neocomplete#sources#omni#input_patterns['nim'] = '[^. *\t]\.\w*' + +let g:nim_completion_callbacks = {} + +fun! NimAsyncCmdComplete(cmd, output) + call add(g:nim_log, a:output) + echom g:nim_completion_callbacks + if has_key(g:nim_completion_callbacks, a:cmd) + let Callback = get(g:nim_completion_callbacks, a:cmd) + call Callback(a:output) + " remove(g:nim_completion_callbacks, a:cmd) + else + echom "ERROR, Unknown Command: " . a:cmd + endif + return 1 +endf + +fun! GotoDefinition_nim_ready(def_output) + if v:shell_error + echo "nim was unable to locate the definition. exit code: " . v:shell_error + " echoerr a:def_output + return 0 + endif + + let rawDef = matchstr(a:def_output, 'def\t\([^\n]*\)') + if rawDef == "" + echo "the current cursor position does not match any definitions" + return 0 + endif + + let defBits = split(rawDef, '\t') + let file = defBits[4] + let line = defBits[5] + exe printf("e +%d %s", line, file) + return 1 +endf + +fun! GotoDefinition_nim() + call NimExecAsync("--def", function("GotoDefinition_nim_ready")) +endf + +fun! FindReferences_nim() + setloclist() +endf + +" Syntastic syntax checking +fun! SyntaxCheckers_nim_nim_GetLocList() + let makeprg = 'nim check --hints:off --listfullpaths ' . s:CurrentNimFile() + let errorformat = &errorformat + + return SyntasticMake({ 'makeprg': makeprg, 'errorformat': errorformat }) +endf + +function! SyntaxCheckers_nim_nim_IsAvailable() + return executable("nim") +endfunction + +if exists("g:SyntasticRegistry") + call g:SyntasticRegistry.CreateAndRegisterChecker({ + \ 'filetype': 'nim', + \ 'name': 'nim'}) +endif + +endif |