diff options
114 files changed, 5437 insertions, 924 deletions
diff --git a/.asan-blacklist b/.asan-blacklist new file mode 100644 index 0000000000..bd977dfe17 --- /dev/null +++ b/.asan-blacklist @@ -0,0 +1,2 @@ +# libuv queue.h pointer arithmetic is not accepted by asan +fun:queue_node_data diff --git a/.ci/common/test.sh b/.ci/common/test.sh index 2eaaf0b2b1..c1bbd8dc9a 100644 --- a/.ci/common/test.sh +++ b/.ci/common/test.sh @@ -82,4 +82,23 @@ install_nvim() { echo "Maybe the helptags have not been generated properly." exit 1 } + + # Check that all runtime files were installed + for file in doc/tags syntax/vim/generated.vim $( + cd runtime ; git ls-files | grep -e '.vim$' -e '.ps$' -e '.dict$' -e '.py$' -e '.tutor$' + ) ; do + if ! test -e "${INSTALL_PREFIX}/share/nvim/runtime/$file" ; then + echo "It appears that $file is not installed." + exit 1 + fi + done + + for file in $( + cd runtime ; git ls-files | grep -e '.awk$' -e '.sh$' -e '.bat$' + ) ; do + if ! test -x "${INSTALL_PREFIX}/share/nvim/runtime/$file" ; then + echo "It appears that $file is not installed or is not executable." + exit 1 + fi + done } diff --git a/clint-ignored-files.txt b/clint-ignored-files.txt index 2988b15fd2..98a26b7fbc 100644 --- a/clint-ignored-files.txt +++ b/clint-ignored-files.txt @@ -57,6 +57,7 @@ src/nvim/keymap.h src/nvim/lib/khash.h src/nvim/lib/klist.h src/nvim/lib/kvec.h +src/nvim/lib/queue.h src/nvim/macros.h src/nvim/main.c src/nvim/main.h @@ -56,12 +56,15 @@ import sre_compile import string import sys import unicodedata +import json +import collections # for defaultdict _USAGE = """ Syntax: clint.py [--verbose=#] [--output=vs7] [--filter=-x,+y,...] [--counting=total|toplevel|detailed] [--root=subdir] - [--linelength=digits] + [--linelength=digits] [--record-errors=file] + [--suppress-errors=file] <file> [file] ... The style guidelines this tries to follow are those in @@ -156,6 +159,13 @@ Syntax: clint.py [--verbose=#] [--output=vs7] [--filter=-x,+y,...] Examples: --extensions=hpp,cpp + + record-errors=file + Record errors to the given location. This file may later be used for error + suppression using suppress-errors flag. + + suppress-errors=file + Errors listed in the given file will not be reported. """ # We categorize each error message we print. Here are the categories. @@ -270,6 +280,10 @@ _RE_SUPPRESSION = re.compile(r'\bNOLINT\b(\([^)]*\))?') # on which those errors are expected and should be suppressed. _error_suppressions = {} +# {(str, int)}: a set of error categories and line numbers which are expected to +# be suppressed +_error_suppressions_2 = set() + # The allowed line length of files. # This is set by --linelength flag. _line_length = 80 @@ -309,11 +323,30 @@ def ParseNolintSuppressions(filename, raw_line, linenum, error): 'Unknown NOLINT error category: %s' % category) +def ParseKnownErrorSuppressions(filename, raw_lines, linenum): + """Updates the global list of error-suppressions from suppress-file. + + Args: + filename: str, the name of the input file. + raw_lines: list, all file lines + linenum: int, the number of the current line. + """ + key = tuple(raw_lines[linenum - 1 if linenum else 0:linenum + 2]) + if key in _cpplint_state.suppressed_errors[filename]: + for category in _cpplint_state.suppressed_errors[filename][key]: + _error_suppressions_2.add((category, linenum)) + + def ResetNolintSuppressions(): "Resets the set of NOLINT suppressions to empty." _error_suppressions.clear() +def ResetKnownErrorSuppressions(): + "Resets the set of suppress-errors=file suppressions to empty." + _error_suppressions_2.clear() + + def IsErrorSuppressedByNolint(category, linenum): """Returns true if the specified error category is suppressed on this line. @@ -330,6 +363,19 @@ def IsErrorSuppressedByNolint(category, linenum): linenum in _error_suppressions.get(None, set())) +def IsErrorInSuppressedErrorsList(category, linenum): + """Returns true if the specified error is suppressed by suppress-errors=file + + Args: + category: str, the category of the error. + linenum: int, the current line number. + Returns: + bool, True iff the error should be suppressed due to presense in + suppressions file. + """ + return (category, linenum) in _error_suppressions_2 + + def Match(pattern, s): """Matches the string with the pattern, caching the compiled regexp.""" # The regexp compilation caching is inlined in both Match and Search for @@ -454,6 +500,10 @@ class _CppLintState(object): # "vs7" - format that Microsoft Visual Studio 7 can parse self.output_format = 'emacs' + self.record_errors_file = None + self.suppressed_errors = collections.defaultdict( + lambda: collections.defaultdict(set)) + def SetOutputFormat(self, output_format): """Sets the output format for errors.""" self.output_format = output_format @@ -517,6 +567,25 @@ class _CppLintState(object): (category, count)) sys.stderr.write('Total errors found: %d\n' % self.error_count) + def SuppressErrorsFrom(self, fname): + """Open file and read a list of suppressed errors from it""" + if fname is None: + return + try: + with open(fname) as fp: + for line in fp: + fname, lines, category = json.loads(line) + lines = tuple(lines) + self.suppressed_errors[fname][lines].add(category) + except IOError: + pass + + def RecordErrorsTo(self, fname): + """Open file with suppressed errors for writing""" + if fname is None: + return + self.record_errors_file = open(fname, 'w') + _cpplint_state = _CppLintState() @@ -545,6 +614,16 @@ def _SetCountingStyle(level): _cpplint_state.SetCountingStyle(level) +def _SuppressErrorsFrom(fname): + """Sets the file containing suppressed errors.""" + _cpplint_state.SuppressErrorsFrom(fname) + + +def _RecordErrorsTo(fname): + """Sets the file containing suppressed errors to write to.""" + _cpplint_state.RecordErrorsTo(fname) + + def _Filters(): """Returns the module's list of output filters, as a list.""" return _cpplint_state.filters @@ -687,6 +766,8 @@ def _ShouldPrintError(category, confidence, linenum): # the verbosity level isn't high enough, or the filters filter it out. if IsErrorSuppressedByNolint(category, linenum): return False + if IsErrorInSuppressedErrorsList(category, linenum): + return False if confidence < _cpplint_state.verbose_level: return False @@ -2986,6 +3067,23 @@ def ProcessFileData(filename, file_extension, lines, error, nesting_state = _NestingState() ResetNolintSuppressions() + ResetKnownErrorSuppressions() + + for line in range(1, len(lines)): + ParseKnownErrorSuppressions(filename, lines, line) + + if _cpplint_state.record_errors_file: + raw_lines = lines[:] + + def RecordedError(filename, linenum, category, confidence, message): + if not IsErrorSuppressedByNolint(category, linenum): + key = raw_lines[linenum - 1 if linenum else 0:linenum + 2] + err = [filename, key, category] + json.dump(err, _cpplint_state.record_errors_file) + _cpplint_state.record_errors_file.write('\n') + Error(filename, linenum, category, confidence, message) + + error = RecordedError if file_extension == 'h': CheckForHeaderGuard(filename, lines, error) @@ -3113,7 +3211,9 @@ def ParseArguments(args): 'filter=', 'root=', 'linelength=', - 'extensions=']) + 'extensions=', + 'record-errors=', + 'suppress-errors=']) except getopt.GetoptError: PrintUsage('Invalid arguments.') @@ -3121,6 +3221,8 @@ def ParseArguments(args): output_format = _OutputFormat() filters = '' counting_style = '' + record_errors_file = None + suppress_errors_file = None for (opt, val) in opts: if opt == '--help': @@ -3153,6 +3255,10 @@ def ParseArguments(args): _valid_extensions = set(val.split(',')) except ValueError: PrintUsage('Extensions must be comma separated list.') + elif opt == '--record-errors': + record_errors_file = val + elif opt == '--suppress-errors': + suppress_errors_file = val if not filenames: PrintUsage('No files were specified.') @@ -3161,6 +3267,8 @@ def ParseArguments(args): _SetVerboseLevel(verbosity) _SetFilters(filters) _SetCountingStyle(counting_style) + _SuppressErrorsFrom(suppress_errors_file) + _RecordErrorsTo(record_errors_file) return filenames diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 6efd876d4f..4e39fcfbe5 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -39,6 +39,7 @@ add_custom_command(OUTPUT ${GENERATED_HELP_TAGS} -u NONE -i NONE -esX + --headless -c "helptags ++t ." -c quit DEPENDS @@ -61,18 +62,30 @@ add_custom_target( # If it's preexisting, leave it alone. install_helper( - DIRECTORY ${PROJECT_SOURCE_DIR}/runtime - DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/nvim) + FILES ${GENERATED_HELP_TAGS} ${BUILDDOCFILES} + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/nvim/runtime/doc) + install_helper( - DIRECTORY ${GENERATED_RUNTIME_DIR} - DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/nvim) + FILES ${GENERATED_SYN_VIM} + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/nvim/runtime/syntax/vim) file(GLOB_RECURSE RUNTIME_PROGRAMS RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} - *.awk *.sh) + *.awk *.sh *.bat) foreach(PROG ${RUNTIME_PROGRAMS}) get_filename_component(BASEDIR ${PROG} PATH) install_helper(PROGRAMS ${PROG} - DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/nvim/${BASEDIR}) + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/nvim/runtime/${BASEDIR}) +endforeach() + +file(GLOB_RECURSE RUNTIME_FILES + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + *.vim *.dict *.py *.ps *.tutor) +list(APPEND RUNTIME_FILES macros/dvorak) + +foreach(F ${RUNTIME_FILES}) + get_filename_component(BASEDIR ${F} PATH) + install_helper(FILES ${F} + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/nvim/runtime/${BASEDIR}) endforeach() diff --git a/runtime/autoload/phpcomplete.vim b/runtime/autoload/phpcomplete.vim index b014b4cdcf..5ddad88873 100644 --- a/runtime/autoload/phpcomplete.vim +++ b/runtime/autoload/phpcomplete.vim @@ -3,7 +3,7 @@ " Maintainer: Dávid Szabó ( complex857 AT gmail DOT com ) " Previous Maintainer: Mikolaj Machowski ( mikmach AT wp DOT pl ) " URL: https://github.com/shawncplus/phpcomplete.vim -" Last Change: 2014 Oct 02 +" Last Change: 2014 Dec 01 " " OPTIONS: " @@ -1172,11 +1172,11 @@ function! phpcomplete#GetCurrentInstruction(line_number, col_number, phpbegin) " " break if we are on a "naked" stop_char (operators, colon, openparent...) if index(stop_chars, current_char) != -1 let do_break = 1 - " dont break does not look like a "->" + " dont break if it does look like a "->" if (prev_char == '-' && current_char == '>') || (current_char == '-' && next_char == '>') let do_break = 0 endif - " dont break if its looks like a "::" + " dont break if it does look like a "::" if (prev_char == ':' && current_char == ':') || (current_char == ':' && next_char == ':') let do_break = 0 endif @@ -1356,8 +1356,12 @@ function! phpcomplete#GetCallChainReturnType(classname_candidate, class_candidat endif " make @return self, static, $this the same way " (not exactly what php means by these) - if returnclass == 'self' || returnclass == 'static' || returnclass == '$this' - let classname_candidate = a:classname_candidate + if returnclass == 'self' || returnclass == 'static' || returnclass == '$this' || returnclass == 'self[]' || returnclass == 'static[]' || returnclass == '$this[]' + if returnclass =~ '\[\]$' + let classname_candidate = a:classname_candidate.'[]' + else + let classname_candidate = a:classname_candidate + endif let class_candidate_namespace = a:class_candidate_namespace else let [classname_candidate, class_candidate_namespace] = phpcomplete#ExpandClassName(returnclass, fullnamespace, a:imports) @@ -1527,7 +1531,7 @@ function! phpcomplete#GetClassName(start_line, context, current_namespace, impor let function_boundary = phpcomplete#GetCurrentFunctionBoundaries() let search_end_line = max([1, function_boundary[0][0]]) " -1 makes us ignore the current line (where the completion was invoked - let lines = reverse(getline(search_end_line, line('.') - 1)) + let lines = reverse(getline(search_end_line, a:start_line - 1)) " check Constant lookup let constant_object = matchstr(a:context, '\zs'.class_name_pattern.'\ze::') @@ -1638,9 +1642,32 @@ function! phpcomplete#GetClassName(start_line, context, current_namespace, impor " assignment for the variable in question with a variable on the right hand side if line =~# '^\s*'.object.'\s*=&\?\s*'.variable_name_pattern - let tailing_semicolon = match(line, ';\s*$') - let tailing_semicolon = tailing_semicolon != -1 ? tailing_semicolon : strlen(getline(a:start_line - i)) - let prev_context = phpcomplete#GetCurrentInstruction(a:start_line - i, tailing_semicolon - 1, b:phpbegin) + + " try to find the next non-comment or string ";" char + let start_col = match(line, '^\s*'.object.'\C\s*=\zs&\?\s*'.variable_name_pattern) + let filelines = reverse(lines) + let [pos, char] = s:getNextCharWithPos(filelines, [a:start_line - i - 1, start_col]) + let chars_read = 1 + " read while end of the file + while char != 'EOF' && chars_read < 1000 + let last_pos = pos + let [pos, char] = s:getNextCharWithPos(filelines, pos) + let chars_read += 1 + " we got a candidate + if char == ';' + let synIDName = synIDattr(synID(pos[0] + 1, pos[1] + 1, 0), 'name') + " it's not a comment or string, end search + if synIDName !~? 'comment\|string' + break + endif + endif + endwhile + + let prev_context = phpcomplete#GetCurrentInstruction(last_pos[0] + 1, last_pos[1], b:phpbegin) + if prev_context == '' + " cannot get previous context give up + return + endif let prev_class = phpcomplete#GetClassName(a:start_line - i, prev_context, a:current_namespace, a:imports) if stridx(prev_class, '\') != -1 @@ -1656,9 +1683,32 @@ function! phpcomplete#GetClassName(start_line, context, current_namespace, impor " assignment for the variable in question with a function on the right hand side if line =~# '^\s*'.object.'\s*=&\?\s*'.function_invocation_pattern - let tailing_semicolon = match(line, ';\s*$') - let tailing_semicolon = tailing_semicolon != -1 ? tailing_semicolon : strlen(getline(a:start_line - i)) - let prev_context = phpcomplete#GetCurrentInstruction(a:start_line - i, tailing_semicolon - 1, b:phpbegin) + + " try to find the next non-comment or string ";" char + let start_col = match(line, '\C^\s*'.object.'\s*=\zs&\?\s*'.function_invocation_pattern) + let filelines = reverse(lines) + let [pos, char] = s:getNextCharWithPos(filelines, [a:start_line - i - 1, start_col]) + let chars_read = 1 + " read while end of the file + while char != 'EOF' && chars_read < 1000 + let last_pos = pos + let [pos, char] = s:getNextCharWithPos(filelines, pos) + let chars_read += 1 + " we got a candidate + if char == ';' + let synIDName = synIDattr(synID(pos[0] + 1, pos[1] + 1, 0), 'name') + " it's not a comment or string, end search + if synIDName !~? 'comment\|string' + break + endif + endif + endwhile + + let prev_context = phpcomplete#GetCurrentInstruction(last_pos[0] + 1, last_pos[1], b:phpbegin) + if prev_context == '' + " cannot get previous context give up + return + endif let function_name = matchstr(prev_context, '^'.function_invocation_pattern.'\ze') let function_name = matchstr(function_name, '^\zs.\+\ze\s*($') " strip the trailing ( diff --git a/runtime/autoload/provider/clipboard.vim b/runtime/autoload/provider/clipboard.vim index d20b3a9bf1..5d1ce7896d 100644 --- a/runtime/autoload/provider/clipboard.vim +++ b/runtime/autoload/provider/clipboard.vim @@ -55,13 +55,21 @@ endif let s:clipboard = {} function! s:clipboard.get(reg) - if s:selections[a:reg].owner > 0 - return s:selections[a:reg].data + let reg = a:reg == '"' ? '+' : a:reg + if s:selections[reg].owner > 0 + return s:selections[reg].data end - return s:try_cmd(s:paste[a:reg]) + return s:try_cmd(s:paste[reg]) endfunction function! s:clipboard.set(lines, regtype, reg) + if a:reg == '"' + call s:clipboard.set(a:lines,a:regtype,'+') + if s:copy['*'] != s:copy['+'] + call s:clipboard.set(a:lines,a:regtype,'*') + end + return 0 + end if s:cache_enabled == 0 call s:try_cmd(s:copy[a:reg], a:lines) return 0 diff --git a/runtime/autoload/provider/script_host.py b/runtime/autoload/provider/script_host.py index 0a7eb53a0e..96d70e0330 100644 --- a/runtime/autoload/provider/script_host.py +++ b/runtime/autoload/provider/script_host.py @@ -17,6 +17,9 @@ IS_PYTHON3 = sys.version_info >= (3, 0) if IS_PYTHON3: basestring = str + if sys.version_info >= (3, 4): + from importlib.machinery import PathFinder + @neovim.plugin class ScriptHost(object): @@ -190,31 +193,35 @@ def path_hook(nvim): name = oldtail[:idx] tail = oldtail[idx+1:] fmr = imp.find_module(name, path) - module = imp.load_module(fullname[:-len(oldtail)] + name, *fmr) + module = imp.find_module(fullname[:-len(oldtail)] + name, *fmr) return _find_module(fullname, tail, module.__path__) else: - fmr = imp.find_module(fullname, path) - return imp.load_module(fullname, *fmr) + return imp.find_module(fullname, path) class VimModuleLoader(object): def __init__(self, module): self.module = module def load_module(self, fullname, path=None): - return self.module + # Check sys.modules, required for reload (see PEP302). + if fullname in sys.modules: + return sys.modules[fullname] + return imp.load_module(fullname, *self.module) class VimPathFinder(object): - @classmethod - def find_module(cls, fullname, path=None): + @staticmethod + def find_module(fullname, path=None): + "Method for Python 2.7 and 3.3." try: return VimModuleLoader( _find_module(fullname, fullname, path or _get_paths())) except ImportError: return None - @classmethod - def load_module(cls, fullname, path=None): - return _find_module(fullname, fullname, path or _get_paths()) + @staticmethod + def find_spec(fullname, path=None, target=None): + "Method for Python 3.4+." + return PathFinder.find_spec(fullname, path or _get_paths(), target) def hook(path): if path == nvim.VIM_SPECIAL_PATH: diff --git a/runtime/autoload/tutor.vim b/runtime/autoload/tutor.vim new file mode 100644 index 0000000000..d2881f7f34 --- /dev/null +++ b/runtime/autoload/tutor.vim @@ -0,0 +1,346 @@ +" vim: fdm=marker et ts=4 sw=4 + +" Setup: {{{1 +function! tutor#SetupVim() + if has('syntax') + if !exists('g:syntax_on') || g:syntax_on == 0 + syntax on + endif + endif +endfunction + +" Mappings: {{{1 + +function! s:CheckMaps() + nmap +endfunction + +function! s:MapKeyWithRedirect(key, cmd) + if maparg(a:key) !=# '' + redir => l:keys + silent call s:CheckMaps() + redir END + let l:key_list = split(l:keys, '\n') + + let l:raw_map = filter(copy(l:key_list), "v:val =~# '\\* ".a:key."'") + if len(l:raw_map) == 0 + exe "nnoremap <buffer> <expr> ".a:key." ".a:cmd + return + endif + let l:map_data = split(l:raw_map[0], '\s*') + + exe "nnoremap <buffer> <expr> ".l:map_data[0]." ".a:cmd + else + exe "nnoremap <buffer> <expr> ".a:key." ".a:cmd + endif +endfunction + +function! tutor#MouseDoubleClick() + if foldclosed(line('.')) > -1 + normal! zo + else + if match(getline('.'), '^#\{1,} ') > -1 + normal! zc + else + call tutor#FollowLink(0) + endif + endif +endfunction + +function! tutor#InjectCommand() + let l:cmd = substitute(getline('.'), '^\s*', '', '') + exe l:cmd + redraw | echohl WarningMsg | echon "tutor: ran" | echohl None | echon " " | echohl Statement | echon l:cmd +endfunction + +function! tutor#SetNormalMappings() + call s:MapKeyWithRedirect('l', 'tutor#ForwardSkipConceal(v:count1)') + call s:MapKeyWithRedirect('h', 'tutor#BackwardSkipConceal(v:count1)') + call s:MapKeyWithRedirect('<right>', 'tutor#ForwardSkipConceal(v:count1)') + call s:MapKeyWithRedirect('<left>', 'tutor#BackwardSkipConceal(v:count1)') + + nnoremap <silent> <buffer> <CR> :call tutor#FollowLink(0)<cr> + nnoremap <silent> <buffer> <2-LeftMouse> :call tutor#MouseDoubleClick()<cr> + nnoremap <buffer> >> :call tutor#InjectCommand()<cr> +endfunction + +function! tutor#SetSampleTextMappings() + noremap <silent> <buffer> A :if match(getline('.'), '^--->') > -1 \| call search('\s{\@=', 'Wc') \| startinsert \| else \| startinsert! \| endif<cr> + noremap <silent> <buffer> $ :if match(getline('.'), '^--->') > -1 \| call search('.\s{\@=', 'Wc') \| else \| call search('$', 'Wc') \| endif<cr> + onoremap <silent> <buffer> $ :if match(getline('.'), '^--->') > -1 \| call search('.\s{\@=', 'Wc') \| else \| call search('$', 'Wc') \| endif<cr> + noremap <silent> <buffer> ^ :if match(getline('.'), '^--->') > -1 \| call search('\(--->\s\)\@<=.', 'bcW') \| else \| call search('^', 'bcW') \|endif<cr> + onoremap <silent> <buffer> ^ :if match(getline('.'), '^--->') > -1 \| call search('\(--->\s\)\@<=.', 'bcW') \| else \| call search('^', 'bcW') \|endif<cr> + nmap <silent> <buffer> 0 ^<esc> + nmap <silent> <buffer> <Home> ^<esc> + nmap <silent> <buffer> <End> $ + imap <silent> <buffer> <Home> <esc>^<esc>:startinsert<cr> + imap <silent> <buffer> <End> <esc>$:startinsert<cr> + noremap <silent> <buffer> I :exe "normal! 0" \| startinsert<cr> +endfunction + +" Navigation: {{{1 + +" taken from http://stackoverflow.com/a/24224578 + +function! tutor#ForwardSkipConceal(count) + let cnt=a:count + let mvcnt=0 + let c=col('.') + let l=line('.') + let lc=col('$') + let line=getline('.') + while cnt + if c>=lc + let mvcnt+=cnt + break + endif + if stridx(&concealcursor, 'n')==-1 + let isconcealed=0 + else + let [isconcealed, cchar, group] = synconcealed(l, c) + endif + if isconcealed + let cnt-=strchars(cchar) + let oldc=c + let c+=1 + while c < lc + let [isconcealed2, cchar2, group2] = synconcealed(l, c) + if !isconcealed2 || cchar2 != cchar + break + endif + let c+= 1 + endwhile + let mvcnt+=strchars(line[oldc-1:c-2]) + else + let cnt-=1 + let mvcnt+=1 + let c+=len(matchstr(line[c-1:], '.')) + endif + endwhile + return mvcnt.'l' +endfunction + +function! tutor#BackwardSkipConceal(count) + let cnt=a:count + let mvcnt=0 + let c=col('.') + let l=line('.') + let lc=0 + let line=getline('.') + while cnt + if c<=1 + let mvcnt+=cnt + break + endif + if stridx(&concealcursor, 'n')==-1 || c == 0 + let isconcealed=0 + else + let [isconcealed, cchar, group]=synconcealed(l, c-1) + endif + if isconcealed + let cnt-=strchars(cchar) + let oldc=c + let c-=1 + while c>1 + let [isconcealed2, cchar2, group2] = synconcealed(l, c-1) + if !isconcealed2 || cchar2 != cchar + break + endif + let c-=1 + endwhile + let c = max([c, 1]) + let mvcnt+=strchars(line[c-1:oldc-2]) + else + let cnt-=1 + let mvcnt+=1 + let c-=len(matchstr(line[:c-2], '.$')) + endif + endwhile + return mvcnt.'h' +endfunction + +" Hypertext: {{{1 + +function! tutor#FollowLink(force) + let l:stack_s = join(map(synstack(line('.'), col('.')), 'synIDattr(v:val, "name")'), '') + if l:stack_s =~# 'tutorLink' + let l:link_start = searchpairpos('\[', '', ')', 'nbcW') + let l:link_end = searchpairpos('\[', '', ')', 'ncW') + if l:link_start[0] == l:link_end[0] + let l:linkData = getline(l:link_start[0])[l:link_start[1]-1:l:link_end[1]-1] + else + return + endif + let l:target = matchstr(l:linkData, '(\@<=.*)\@=') + if a:force != 1 && match(l:target, '\*.\+\*') > -1 + call cursor(l:link_start[0], l:link_end[1]) + call search(l:target, '') + normal! ^ + elseif a:force != 1 && match(l:target, '^@tutor:') > -1 + let l:tutor = matchstr(l:target, '@tutor:\zs.*') + exe "Tutor ".l:tutor + else + exe "help ".l:target + endif + endif +endfunction + +" Folding And Info: {{{1 + +function! tutor#TutorFolds() + if getline(v:lnum) =~# '^#\{1,6}' + return ">". len(matchstr(getline(v:lnum), '^#\{1,6}')) + else + return "=" + endif +endfunction + +function! tutor#InfoText() + let l:info_parts = [] + if exists('b:tutor_infofunc') + call add(l:info_parts, eval(b:tutor_infofunc.'()')) + endif + return join(l:info_parts, " ") +endfunction + +" Marks {{{1 +function! tutor#PlaceXMarks() + call cursor(1, 1) + let b:tutor_sign_id = 1 + while search('^--->', 'W') > 0 + call tutor#CheckText(getline('.')) + let b:tutor_sign_id+=1 + endwhile + call cursor(1, 1) +endfunction + +function! tutor#CheckText(text) + if match(a:text, '{expect:ANYTHING}\s*$') == -1 + if match(getline('.'), '^--->\s*$') > -1 + exe "sign place ".b:tutor_sign_id." line=".line('.')." name=tutorbad buffer=".bufnr('%') + else + if match(getline('.'), '|expect:.\+|') == -1 + let l:cur_text = matchstr(a:text, '---> \zs.\{-}\ze {expect:') + let l:expected_text = matchstr(a:text, '{expect:\zs.*\ze}\s*$') + else + let l:cur_text = matchstr(a:text, '---> \zs.\{-}\ze |expect:') + let l:expected_text = matchstr(a:text, '|expect:\zs.*\ze|\s*$') + endif + if l:cur_text ==# l:expected_text + exe "sign place ".b:tutor_sign_id." line=".line('.')." name=tutorok buffer=".bufnr('%') + else + exe "sign place ".b:tutor_sign_id." line=".line('.')." name=tutorbad buffer=".bufnr('%') + endif + endif + endif +endfunction + +function! tutor#OnTextChanged() + let l:text = getline('.') + if match(l:text, '^--->') > -1 + call tutor#CheckText(l:text) + endif +endfunction + +" Tutor Cmd: {{{1 + +function! s:Locale() + let l:lang = "" + if exists('v:lang') && v:lang =~ '\a\a' + let l:lang = v:lang + elseif $LC_ALL =~ '\a\a' + let l:lang = $LC_ALL + elseif $LANG =~ '\a\a' + let l:lang = $LANG + endif + return split(l:lang, '_') +endfunction + +function! s:GlobPath(lp, pat) + if version >= 704 && has('patch279') + return globpath(a:lp, a:pat, 1, 1) + else + return split(globpath(a:lp, a:pat, 1), '\n') + endif +endfunction + +function! s:Sort(a, b) + let mod_a = fnamemodify(a:a, ':t') + let mod_b = fnamemodify(a:b, ':t') + if mod_a == mod_b + let retval = 0 + elseif mod_a > mod_b + if match(mod_a, '^vim-') > -1 && match(mod_b, '^vim-') == -1 + let retval = -1 + else + let retval = 1 + endif + else + if match(mod_b, '^vim-') > -1 && match(mod_a, '^vim-') == -1 + let retval = 1 + else + let retval = -1 + endif + endif + return retval +endfunction + +function! s:GlobTutorials(name) + " search for tutorials: + " 1. non-localized + let l:tutors = s:GlobPath(&rtp, 'tutor/'.a:name.'.tutor') + " 2. localized for current locale + let l:locale_tutors = s:GlobPath(&rtp, 'tutor/'.s:Locale()[0].'/'.a:name.'.tutor') + " 3. fallback to 'en' + if len(l:locale_tutors) == 0 + let l:locale_tutors = s:GlobPath(&rtp, 'tutor/en/'.a:name.'.tutor') + endif + call extend(l:tutors, l:locale_tutors) + return uniq(sort(l:tutors, 's:Sort'), 's:Sort') +endfunction + +function! tutor#TutorCmd(tutor_name) + if match(a:tutor_name, '[[:space:]]') > 0 + echom "Only one argument accepted (check spaces)" + return + endif + + if a:tutor_name == '' + let l:tutor_name = 'vim-01-beginner.tutor' + else + let l:tutor_name = a:tutor_name + endif + + if match(l:tutor_name, '\.tutor$') > 0 + let l:tutor_name = fnamemodify(l:tutor_name, ':r') + endif + + let l:tutors = s:GlobTutorials(l:tutor_name) + + if len(l:tutors) == 0 + echom "No tutorial with that name found" + return + endif + + if len(l:tutors) == 1 + let l:to_open = l:tutors[0] + else + let l:idx = 0 + let l:candidates = ['Several tutorials with that name found. Select one:'] + for candidate in map(copy(l:tutors), + \'fnamemodify(v:val, ":h:h:t")."/".s:Locale()[0]."/".fnamemodify(v:val, ":t")') + let l:idx += 1 + call add(l:candidates, l:idx.'. '.candidate) + endfor + let l:tutor_to_open = inputlist(l:candidates) + let l:to_open = l:tutors[l:tutor_to_open-1] + endif + + exe "edit ".l:to_open +endfunction + +function! tutor#TutorCmdComplete(lead,line,pos) + let l:tutors = s:GlobTutorials('*') + let l:names = uniq(sort(map(l:tutors, 'fnamemodify(v:val, ":t:r")'), 's:Sort')) + return join(l:names, "\n") +endfunction diff --git a/runtime/doc/editing.txt b/runtime/doc/editing.txt index 63c27b2ff2..bb3d89e4ac 100644 --- a/runtime/doc/editing.txt +++ b/runtime/doc/editing.txt @@ -569,7 +569,10 @@ list of the current window. Also see |++opt| and |+cmd|. :[count]arga[dd] {name} .. *:arga* *:argadd* *E479* - Add the {name}s to the argument list. +:[count]arga[dd] + Add the {name}s to the argument list. When {name} is + omitted add the current buffer name to the argument + list. If [count] is omitted, the {name}s are added just after the current entry in the argument list. Otherwise they are added after the [count]'th file. @@ -579,7 +582,8 @@ list of the current window. :argadd x a b x c :0argadd x x a b c :1argadd x a x b c - :99argadd x a b c x + :$argadd x a b c x + :+2argadd y a b c x y There is no check for duplicates, it is possible to add a file to the argument list twice. The currently edited file is not changed. @@ -597,11 +601,19 @@ list of the current window. Example: > :argdel *.obj -:{range}argd[elete] Delete the {range} files from the argument list. +:[range]argd[elete] Delete the {range} files from the argument list. + Example: > + :10,$argdel +< Deletes arguments 10 and further, keeping 1-9. > + :$argd +< Deletes just the last one. > + :argd + :.argd +< Deletes the current argument. > + :%argd +< Removes all the files from the arglist. When the last number in the range is too high, up to - the last argument is deleted. Example: > - :10,1000argdel -< Deletes arguments 10 and further, keeping 1-9. + the last argument is deleted. *:argu* *:argument* :[count]argu[ment] [count] [++opt] [+cmd] @@ -1018,7 +1030,7 @@ The names can be in upper- or lowercase. :q[uit]! Quit without writing, also when currently visible buffers have changes. Does not exit when this is the - last window and there are is a changed hidden buffer. + last window and there is a changed hidden buffer. In this case, the first changed hidden buffer becomes the current buffer. Use ":qall!" to exit always. diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index 61b23b7e03..5c2b570695 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -1,4 +1,4 @@ -*eval.txt* For Vim version 7.4. Last change: 2014 Nov 15 +*eval.txt* For Vim version 7.4. Last change: 2014 Nov 27 VIM REFERENCE MANUAL by Bram Moolenaar @@ -1517,6 +1517,12 @@ v:mouse_col Column number for a mouse click obtained with |getchar()|. This is the screen column number, like with |virtcol()|. The value is zero when there was no mouse button click. + *v:msgpack_types* *msgpack_types-variable* +v:msgpack_types Dictionary containing msgpack types used by |msgpackparse()| + and |msgpackdump()|. All types inside dictionary are fixed + (not editable) empty lists. To check whether some list is one + of msgpack types, use |is| operator. + *v:oldfiles* *oldfiles-variable* v:oldfiles List of file names that is loaded from the |viminfo| file on startup. These are the files that Vim remembers marks for. @@ -1904,6 +1910,8 @@ min( {list}) Number minimum value of items in {list} mkdir( {name} [, {path} [, {prot}]]) Number create directory {name} mode( [expr]) String current editing mode +msgpackdump( {list}) List dump a list of objects to msgpack +msgpackparse( {list}) List parse msgpack to a list of objects nextnonblank( {lnum}) Number line nr of non-blank line >= {lnum} nr2char( {expr}[, {utf8}]) String single char with ASCII/UTF8 value {expr} or( {expr}, {expr}) Number bitwise OR @@ -3303,6 +3311,17 @@ getchar([expr]) *getchar()* : endif : endwhile :endfunction +< + You may also receive syntetic characters, such as + |<CursorHold>|. Often you will want to ignore this and get + another character: > + :function GetKey() + : let c = getchar() + : while c == "\<CursorHold>" + : let c = getchar() + : endwhile + : return c + :endfunction getcharmod() *getcharmod()* The result is a Number which is the state of the modifiers for @@ -3513,7 +3532,7 @@ getpos({expr}) Get the position for {expr}. For possible values of {expr} This can be used to save and restore the position of a mark: > let save_a_mark = getpos("'a") ... - call setpos(''a', save_a_mark + call setpos("'a", save_a_mark) < Also see |getcurpos()| and |setpos()|. @@ -4614,6 +4633,92 @@ mode([expr]) Return a string that indicates the current mode. "c" or "n". Also see |visualmode()|. +msgpackdump({list}) {Nvim} *msgpackdump()* + Convert a list of VimL objects to msgpack. Returned value is + |readfile()|-style list. Example: > + call writefile(msgpackdump([{}]), 'fname.mpack', 'b') +< This will write the single 0x80 byte to `fname.mpack` file + (dictionary with zero items is represented by 0x80 byte in + messagepack). + + Limitations: + 1. |Funcref|s cannot be dumped. + 2. Containers that reference themselves cannot be dumped. + 3. Dictionary keys are always dumped as STR strings. + 4. Other strings are always dumped as BIN strings. + 5. Points 3. and 4. do not apply to |msgpack-special-dict|s. + +msgpackparse({list}) {Nvim} *msgpackparse()* + Convert a |readfile()|-style list to a list of VimL objects. + Example: > + let fname = expand('~/.nvim/shada/main.shada') + let mpack = readfile(fname, 'b') + let shada_objects = msgpackparse(mpack) +< This will read ~/.nvim/shada/main.shada file to + `shada_objects` list. + + Limitations: + 1. Mapping ordering is not preserved unless messagepack + mapping is dumped using generic mapping + (|msgpack-special-map|). + 2. Since the parser aims to preserve all data untouched + (except for 1.) some strings are parsed to + |msgpack-special-dict| format which is not convenient to + use. + *msgpack-special-dict* + Some messagepack strings may be parsed to special + dictionaries. Special dictionaries are dictionaries which + + 1. Contain exactly two keys: `_TYPE` and `_VAL`. + 2. `_TYPE` key is one of the types found in |v:msgpack_types| + variable. + 3. Value for `_VAL` has the following format (Key column + contains name of the key from |v:msgpack_types|): + + Key Value ~ + nil Zero, ignored when dumping. + boolean One or zero. When dumping it is only checked that + value is a |Number|. + integer |List| with four numbers: sign (-1 or 1), highest two + bits, number with bits from 62nd to 31st, lowest 31 + bits. I.e. to get actual number one will need to use + code like > + _VAL[0] * ((_VAL[1] << 62) + & (_VAL[2] << 31) + & _VAL[3]) +< Special dictionary with this type will appear in + |msgpackparse()| output under one of the following + circumstances: + 1. |Number| is 32-bit and value is either above + INT32_MAX or below INT32_MIN. + 2. |Number| is 64-bit and value is above INT64_MAX. It + cannot possibly be below INT64_MIN because msgpack + C parser does not support such values. + float |Float|. This value cannot possibly appear in + |msgpackparse()| output. + string |readfile()|-style list of strings. This value will + appear in |msgpackparse()| output if string contains + zero byte or if string is a mapping key and mapping is + being represented as special dictionary for other + reasons. + binary |readfile()|-style list of strings. This value will + appear in |msgpackparse()| output if binary string + contains zero byte. + array |List|. This value cannot appear in |msgpackparse()| + output. + *msgpack-special-map* + map |List| of |List|s with two items (key and value) each. + This value will appear in |msgpackparse()| output if + parsed mapping contains one of the following keys: + 1. Any key that is not a string (including keys which + are binary strings). + 2. String with NUL byte inside. + 3. Duplicate key. + 4. Empty key. + ext |List| with two values: first is a signed integer + representing extension type. Second is + |readfile()|-style list of strings. + nextnonblank({lnum}) *nextnonblank()* Return the line number of the first line at or below {lnum} that is not blank. Example: > diff --git a/runtime/doc/indent.txt b/runtime/doc/indent.txt index fc16b1a7e7..3f61e28651 100644 --- a/runtime/doc/indent.txt +++ b/runtime/doc/indent.txt @@ -1,4 +1,4 @@ -*indent.txt* For Vim version 7.4. Last change: 2014 Apr 23 +*indent.txt* For Vim version 7.4. Last change: 2014 Dec 06 VIM REFERENCE MANUAL by Bram Moolenaar diff --git a/runtime/doc/nvim.txt b/runtime/doc/nvim.txt new file mode 100644 index 0000000000..a7c512d1dc --- /dev/null +++ b/runtime/doc/nvim.txt @@ -0,0 +1,24 @@ +*nvim.txt* For Nvim. {Nvim} + + + NVIM REFERENCE MANUAL *nvim* + + +Introduction to Nvim *nvim-intro* + +This is an introduction for Vim users who are just getting started with Nvim. +It is not meant for Vim beginners. For a basic introduction to Vim, +see |help.txt|. + +1. Transitioning from Vim |nvim-from-vim| +2. Differences from Vim |vim-differences| +3. Msgpack-RPC |msgpack-rpc| +4. Job control |job-control| +5. Python plugins |nvim-python| +6. Clipboard integration |nvim-clipboard| +7. Remote plugins |remote-plugin| +8. Provider infrastructure |nvim-provider| +9. Integrated terminal emulator |nvim-terminal-emulator| + +============================================================================== + vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/nvim_from_vim.txt b/runtime/doc/nvim_from_vim.txt new file mode 100644 index 0000000000..220e67d0e7 --- /dev/null +++ b/runtime/doc/nvim_from_vim.txt @@ -0,0 +1,49 @@ +*nvim_from_vim.txt* For Nvim. {Nvim} + + + NVIM REFERENCE MANUAL + + +Transitioning from Vim *nvim-from-vim* + +Nvim is emphatically a fork of Vim, so compatibility to Vim should be pretty +good. + +To start the transition, link your previous configuration so Nvim can use +it: +> + ln -s ~/.vimrc ~/.nvimrc + ln -s ~/.vim ~/.nvim +< +See |nvim-intro|, especially |nvim-python| and |nvim-clipboard|, for +additional software you might need to install to use all of Nvim's features. + +Your Vim configuration might not be entirely compatible with Nvim. For a +full list of differences between Vim and Nvim, see |vim-differences|. + +The |'ttymouse'| option, for example, was removed from Nvim (mouse support +should work without it). If you use the same |vimrc| for Vim and Nvim, +consider guarding |'ttymouse'| in your configuration like so: +> + if !has('nvim') + set ttymouse=xterm2 + endif +< +Conversely, if you have Nvim specific configuration items, you could do +this: +> + if has('nvim') + tnoremap <Esc> <C-\><C-n> + endif +< +For a more granular approach, use |exists()|: +> + if exists(':tnoremap') + tnoremap <Esc> <C-\><C-n> + endif +< +Now you should be able to explore Nvim more comfortably. Check |nvim| for more +information. + +============================================================================== + vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/nvim_intro.txt b/runtime/doc/nvim_intro.txt deleted file mode 100644 index 40f65620af..0000000000 --- a/runtime/doc/nvim_intro.txt +++ /dev/null @@ -1,26 +0,0 @@ -*nvim_intro.txt* For Nvim. {Nvim} - - - NVIM REFERENCE MANUAL by Thiago de Arruda - - -Introduction to Nvim *nvim* *nvim-intro* - -This is an introduction for Vim users who are just getting started with Nvim. -It is not meant for Vim beginners. For a basic introduction to Vim, -see |help.txt|. - -For now, it is just an index with the most relevant topics/features that -differentiate Nvim from Vim: - -1. Differences from Vim |vim-differences| -2. Msgpack-RPC |msgpack-rpc| -3. Job control |job-control| -4. Python plugins |nvim-python| -5. Clipboard integration |nvim-clipboard| -6. Remote plugins |remote-plugin| -7. Provider infrastructure |nvim-provider| -8. Integrated terminal emulator |nvim-terminal-emulator| - -============================================================================== - vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/nvim_python.txt b/runtime/doc/nvim_python.txt index a93e89303c..1c345b4532 100644 --- a/runtime/doc/nvim_python.txt +++ b/runtime/doc/nvim_python.txt @@ -70,7 +70,7 @@ To disable Python 2 interface, set `g:loaded_python_provider` to 1: < *g:loaded_python3_provider* -To disable Python 3 interface, set `g:loaded_python3_provider` to 0: +To disable Python 3 interface, set `g:loaded_python3_provider` to 1: > let g:loaded_python3_provider = 1 < diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index fea8a59a74..e8c844f7d2 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -1402,8 +1402,8 @@ A jump table for the options with a short description can be found at |Q_op|. register '*' for all yank, delete, change and put operations which would normally go to the unnamed register. When "unnamed" is also included to the - option, yank operations (but not delete, change or - put) will additionally copy the text into register + option, yank and delete operations (but not put) + will additionally copy the text into register '*'. See |nvim-clipboard|. < *clipboard-autoselect* @@ -1427,35 +1427,6 @@ A jump table for the options with a short description can be found at |Q_op|. autoselectml Like "autoselect", but for the modeless selection only. Compare to the 'A' flag in 'guioptions'. - *clipboard-html* - html When the clipboard contains HTML, use this when - pasting. When putting text on the clipboard, mark it - as HTML. This works to copy rendered HTML from - Firefox, paste it as raw HTML in Vim, select the HTML - in Vim and paste it in a rich edit box in Firefox. - You probably want to add this only temporarily, - possibly use BufEnter autocommands. - Only supported for GTK version 2 and later. - Only available with the |+multi_byte| feature. - - *clipboard-exclude* - exclude:{pattern} - Defines a pattern that is matched against the name of - the terminal 'term'. If there is a match, no - connection will be made to the X server. This is - useful in this situation: - - Running Vim in a console. - - $DISPLAY is set to start applications on another - display. - - You do not want to connect to the X server in the - console, but do want this in a terminal emulator. - To never connect to the X server use: > - exclude:.* - The value of 'magic' is ignored, {pattern} is - interpreted as if 'magic' was on. - The rest of the option value will be used for - {pattern}, this must be the last entry. - *'cmdheight'* *'ch'* 'cmdheight' 'ch' number (default 1) global @@ -5192,7 +5163,7 @@ A jump table for the options with a short description can be found at |Q_op|. print/ files for printing |postscript-print-encoding| spell/ spell checking files |spell| syntax/ syntax files |mysyntaxfile| - tutor/ files for vimtutor |tutor| + tutor/ tutorial files |:Tutor| And any other file searched for with the |:runtime| command. diff --git a/runtime/doc/pi_tutor.txt b/runtime/doc/pi_tutor.txt new file mode 100644 index 0000000000..7ed47b24dd --- /dev/null +++ b/runtime/doc/pi_tutor.txt @@ -0,0 +1,50 @@ +*pi_tutor.txt* Interactive tutorials. +*vim-tutor-mode* + +vim-tutor-mode provides a system to follow and create interactive tutorials +for vim and third party plugins. It replaces the venerable `vimtutor` system. + +============================================================================== +1. Usage *vim-tutor-usage* + +vim-tutor-mode tutorials are hypertext documents, they have rich text and +contain links. To stand out from the rest of the text, links are underlined. +You can follow them by placing the cursor over them and pressing <Enter>, or +by double-clicking them. + +1.1 Commands +------------ + *:Tutor* +:Tutor {tutorial} Opens a tutorial. Command-line completion for + {tutorial} is provided, the candidates are a list of + '.tutor' files found in the 'tutor/' folder in + the 'runtimepath'. Tutorials prefixed with 'vim-' will + always be shown first. + + If no {tutorial} is provided, the command starts the + 'vim-01-beginner' tutorial, which is equivalent to + Vim's `vimtutor`. + +============================================================================= +2. Creating tutorials *vim-tutor-create* + +Writing vim-tutor-mode tutorials is easy. For an overview of the format used, +please consult the 'tutor.tutor' file: > + + :Tutor tutor +< +New tutorials must be placed in the 'tutor/' folder in the 'runtimepath' +to be detected by the :Tutor command. + +It is recommended to use a less formal style when writing tutorials than in +regular documentation (unless the content requires it). + +============================================================================ +3. Contributing + +Development of the plugin is done over at github [1]. Feel free to report +issues and make suggestions. + +[1]: https://github.com/fmoralesc/vim-tutor-mode + +" vim: set ft=help : diff --git a/runtime/doc/syntax.txt b/runtime/doc/syntax.txt index 6bc1b2873c..88f23e6c76 100644 --- a/runtime/doc/syntax.txt +++ b/runtime/doc/syntax.txt @@ -1,4 +1,4 @@ -*syntax.txt* For Vim version 7.4. Last change: 2014 Sep 27 +*syntax.txt* For Vim version 7.4. Last change: 2014 Nov 19 VIM REFERENCE MANUAL by Bram Moolenaar @@ -5055,6 +5055,7 @@ This will set the "w:current_syntax" variable to "foo". The value of restoring "b:current_syntax", since the syntax files do set "b:current_syntax". The value set by the syntax file is assigned to "w:current_syntax". +Note: This resets the 'spell', 'spellcapcheck' and 'spellfile' options. Once a window has its own syntax, syntax commands executed from other windows on the same buffer (including :syntax clear) have no effect. Conversely, diff --git a/runtime/doc/todo.txt b/runtime/doc/todo.txt index 88c277ef43..78ae536c5a 100644 --- a/runtime/doc/todo.txt +++ b/runtime/doc/todo.txt @@ -1,4 +1,4 @@ -*todo.txt* For Vim version 7.4. Last change: 2014 Nov 19 +*todo.txt* For Vim version 7.4. Last change: 2014 Dec 06 VIM REFERENCE MANUAL by Bram Moolenaar @@ -34,9 +34,23 @@ not be repeated below, unless there is extra information. *known-bugs* -------------------- Known bugs and current work ----------------------- +Patch to fix list range assign crash. (Yukihiro Nakadaira, 2014 Dec 1) + +Patch to fix range with user command. (Marcin Szamotulski, 2014 Dec 2) +Update Dec 6, with support for user commands. + +When window number in Ex range is too high, give an error? +Only when backwards compatible. + +:s/\n// doesn't change anything. Since 7.4.232? (Eliseo Martínez, 2014 Nov +28) Patch on Issue 287 + +Using vim_snprintf() in window.c can be in a function. + Regexp problems: - The NFA engine does not implement the time limit passed to nfa_regexec_multi() +- Very slow with a long line and Ruby highlighting. (John Whitley, 2014 Dec 4) - Bug with pattern: '\vblock (\d+)\.\n.*\d+%(\1)@<!\.$' (Lech Lorens, 2014 Feb 3) - Issue 164: freeze on regexp search. @@ -64,17 +78,11 @@ Breaks test_eval. Inefficient, can we only compute y_width when needed? Problem that a previous silent ":throw" causes a following try/catch not to work. (ZyX, 2013 Sep 28) +Patch to fix recognizing function name. (Ozaki Kiichi, 2014 Nov 28) + ":cd C:\Windows\System32\drivers\etc*" does not work, even though the directory exists. (Sergio Gallelli, 2013 Dec 29) -Patch by Marcin Szamotulski to add count to :close (2014 Aug 10, update Aug -14, Aug 30) - Make ":1close" close the first window. - Make ":+1close" close the next window. - Make ":-1close" close the previous window. -Doesn't look right, asked for updates. -Update 2014 Nov 8. Replied with suggestions. - The entries added by matchaddpos() are returned by getmatches() but can't be set with setmatches(). (lcd47, 2014 Jun 29) @@ -84,34 +92,34 @@ Problem using ":try" inside ":execute". (ZyX, 2013 Sep 15) Python: ":py raw_input('prompt')" doesn't work. (Manu Hack) -Patch to fix issue 78. (Christian Brabandt, 2014 Oct 8) - -Patch to fix leak in map() with error. (Christian Brabandt, 2014 Oct 11) - -Patch to fix incsearch for "2/pattern/e". - -Patch to fix memory leak in :hardcopy. (Christian Brabandt, 2014 Nov 16) - -Patch to fix warnings in if_ruby.c. (Ken Takata, 2014 Nov 17) - -Patch to make test 63 pass when in a B&W terminal. (Christian Brabandt, 2014 -Nov 15) Other patch (better) on Nov 17. - Change behavior of v:hlsearch? Patch from Christian, 2014 Oct 22. +Patch to recover from X server restart: hint on Issue 203 (2014 Nov 21 18:44) + MS-Windows: When editing a file with a leading space, writing it uses the wrong name. (Aram, 2014 Nov 7) Vim 7.4. +Add LessCss support. (Jenoma / Alessandro Vioni, 2014 Nov 24) +Now with updated license, Nov 24. + patch to remove FEAT_OSFILETYPE from fileio.c. (Christian, 2014 Nov 12) Value returned by virtcol() changes depending on how lines wrap. This is inconsistent with the documentation. -Patch to fix relatie numbers. (Christian Brabandt, 2014 Nov 17) +Ukrainian vimtutor. (Issue 288) + +Regenerate the Unicode tables in mbyte.c. +Diff from ZyX, 2014 Dec 6. + +Patch to fix relative numbers. (Christian Brabandt, 2014 Nov 17) +Update Nov 26. Patch to fix wrong formatting if 'linebreak' is set. (Christian Brabandt, 2014 Nov 12) +Patch to avoid recognizing polkit as hog files. (Issue 292) + Patch to support hex values for setting option value. (Zyx, 2015 Nov 6) @@ -122,17 +130,29 @@ Update Nov 5. MS-Windows: Crash opening very long file name starting with "\\". (Christian Brock, 2012 Jun 29) +Cursorline background color not mixed with character highlight. +Patch by Yasuhiro Matsumoto, 2014 Dec 3. + Problem using diff syntax with cp932 encoding. Idea from Yasuhiro Matsumoto, patch from Ken Takata (2014 Nov 6) ml_updatechunk() is slow when retrying for another encoding. (John Little, 2014 Sep 11) +Patch to add a different escape sequence for replace mode. +(Omar Sandoval, 2014 Nov 30) + +Patch to allow values greater than 255 for ctermfg/ctermbg on Windows. +(Yasuhiro Matsumoto, 2014 Dec 5) + When 'balloonexpr' returns a list the result has a trailing newline. Just remove one trailing newline. (lcd, 2014 Oct 17) Make comments in the test Makefile silent. (Kartik Agaram, 2014 Sep 24) +Result of systemlist() does not show whether text ended in line break. +(Bjorn Linse, 2014 Nov 27) + When in 'comments' "n:x" follows after three-part comment directly it repeats any one-character from the previous line. (Kartik Agaram, 2014 Sep 19) @@ -147,6 +167,15 @@ Plugins need to make a lot of effort, lots of mappings, to know what happened before pressing the key that triggers a plugin action. How about keeping the last N pressed keys, so that they do not need to be mapped? +":q!" should reset modified flag for current buffer, if another buffer is +modified no need to abandon it again. +Patch from Yasuhiro Matsumoto, 2014 Nov 21. +Update from Hirohito Higashi, 2014 Nov 21. +With test, Nov 23. + +Wrong scrolling when using incsearch. Patch by Christian Brabandt, 2014 Dec 4. +Is this a good solution? + Can assign to s:type when a function s:type has been defined. Also the other way around: define a function while a variable with that name was already defined. @@ -175,6 +204,7 @@ Bug: Autocompleting ":tag/pat" replaces "/pat" with a match but does not insert a space. (Micha Mos, 2014 Nov 7) Patch to add the :bvimgrep command. (Christian Brabandt, 2014 Nov 12) +Update Dec 6. Patch to add argument to :cquit. (Thinca, 2014 Oct 12) @@ -287,6 +317,10 @@ Yasuhiro Matsumoto, 2013 May 31. Or should we add a more general mechanism, like a lambda() function? Patch by Yasuhiro Matsumoto, 2014 Sep 16. +Patch to fix display of listchars on the cursorline. (Nayuri Aohime, 2013) +Update suggested by Yasuhiro Matsumoto, 2014 Nov 25: +https://gist.github.com/presuku/d3d6b230b9b6dcfc0477 + Patch for XDG base directory support. (Jean François Bignolles, 2014 Mar 4) Remark on the docs. Should not be a compile time feature. But then what? @@ -537,6 +571,14 @@ MS-Windows resizing problems: causes the window to move unnecessarily. (William E. Skeith III, 2012 Jan 12) Patch: 2012 Jan 13 Needs more work (2012 Feb 2) +Patch to use Modern UI 2.0 for the Nsis installer. (Guopeng Wen, 2010 Jul 30) +Latest version: 2011 May 18 +8 Windows install with NSIS: make it possible to do a silent install, see + http://nsis.sourceforge.net/Docs/Chapter4.html#4.12 + Version from Guopeng Wen that does this (2010 Dec 27) +Alternative: MSI installer: https://github.com/petrkle/vim-msi/ +Or the one on Issue 279 + 'iminsert' global value set when using ":setlocal iminsert"? (Wu, 2012 Jun 23) Patch to append regexp to tag commands to make it possible to select one out @@ -847,7 +889,7 @@ Assume the system converts between the actual encoding of the filesystem to the system encoding (usually utf-8). Patch to add GUI colors to the terminal, when it supports it. (ZyX, 2013 Jan -26, update 2013 Dec 14) +26, update 2013 Dec 14, another 2014 Nov 22) Problem producing tags file when hebrew.frx is present. It has a BOM. Results in E670. (Tony Mechelynck, 2010 May 2) @@ -1479,13 +1521,6 @@ with "gvim -nb:localhost:55555:foo". From nc do: '1:editFile!0 "foo"'. Then go to Insert mode and add a few lines. Then backspacing every other time moves the cursor instead of deleting. (Chris Kaiser, 2007 Sep 25) -Patch to use Modern UI 2.0 for the Nsis installer. (Guopeng Wen, 2010 Jul 30) -Latest version: 2011 May 18 -8 Windows install with NSIS: make it possible to do a silent install, see - http://nsis.sourceforge.net/Docs/Chapter4.html#4.12 - Version from Guopeng Wen that does this (2010 Dec 27) -Alternative: MSI installer: https://github.com/petrkle/vim-msi/ - Windows installer should install 32-bit version of right-click handler also on 64-bit systems. (Brian Cunningham, 2011 Dec 28) diff --git a/runtime/doc/usr_01.txt b/runtime/doc/usr_01.txt index cdb3035a40..c6d363ad5f 100644 --- a/runtime/doc/usr_01.txt +++ b/runtime/doc/usr_01.txt @@ -70,52 +70,18 @@ If the file already exists you probably want to keep it. For more info see |vimrc|. ============================================================================== -*01.3* Using the Vim tutor *tutor* *vimtutor* +*01.3* Using the Vim tutor *tutor* *vimtutor* Instead of reading the text (boring!) you can use the vimtutor to learn your first Vim commands. This is a 30 minute tutorial that teaches the most basic Vim functionality hands-on. -On Unix, if Vim has been properly installed, you can start it from the shell: -> - vimtutor +To start the tutorial, execute > -On MS-Windows you can find it in the Program/Vim menu. Or execute -vimtutor.bat in the $VIMRUNTIME directory. - -This will make a copy of the tutor file, so that you can edit it without -the risk of damaging the original. - There are a few translated versions of the tutor. To find out if yours is -available, use the two-letter language code. For French: > - - vimtutor fr - -On Unix, if you prefer using the GUI version of Vim, use "gvimtutor" or -"vimtutor -g", optionally with a two-letter language code. - - -On other systems, you have to do a little work: - -1. Copy the tutor file. You can do this with Vim (it knows where to find it): -> - vim -u NONE -c 'e $VIMRUNTIME/tutor/tutor' -c 'w! TUTORCOPY' -c 'q' -< - This will write the file "TUTORCOPY" in the current directory. To use a -translated version of the tutor, append the two-letter language code to the -filename. For French: -> - vim -u NONE -c 'e $VIMRUNTIME/tutor/tutor.fr' -c 'w! TUTORCOPY' -c 'q' + :Tutor < -2. Edit the copied file with Vim: -> - vim -u NONE TUTORCOPY -< - The extra arguments make sure Vim is started in a good mood. +from within nvim. The tutorial will lead you from that point. Have fun! -3. Delete the copied file when you are finished with it: -> - del TUTORCOPY -< ============================================================================== *01.4* Copyright *manual-copyright* diff --git a/runtime/doc/usr_02.txt b/runtime/doc/usr_02.txt index ebb918d7ec..cd25b14e32 100644 --- a/runtime/doc/usr_02.txt +++ b/runtime/doc/usr_02.txt @@ -164,7 +164,7 @@ right and j points down. In a picture: > The best way to learn these commands is by using them. Use the "i" command to insert some more lines of text. Then use the hjkl keys to move around and insert a word somewhere. Don't forget to press <Esc> to go back to Normal -mode. The |vimtutor| is also a nice way to learn by doing. +mode. |:Tutor| is also a nice way to learn by doing. For Japanese users, Hiroshi Iwatani suggested using this: diff --git a/runtime/doc/windows.txt b/runtime/doc/windows.txt index ba69b7cf2f..39d3c43d97 100644 --- a/runtime/doc/windows.txt +++ b/runtime/doc/windows.txt @@ -1,4 +1,4 @@ -*windows.txt* For Vim version 7.4. Last change: 2014 Sep 23 +*windows.txt* For Vim version 7.4. Last change: 2014 Dec 05 VIM REFERENCE MANUAL by Bram Moolenaar @@ -280,7 +280,7 @@ CTRL-W CTRL-Q *CTRL-W_CTRL-Q* " if there are less than 9 windows opened :-quit " quit the previous window :+quit " quit the next window - :+2quit " will also work as expected + :+2quit " quit the second next window < :q[uit]! :{count}q[uit]! @@ -324,9 +324,9 @@ CTRL-W CTRL-C *CTRL-W_CTRL-C* screen. For {count} see |:quit|. The buffer becomes hidden (unless there is another window - editing it or 'bufhidden' is `unload` or `delete`). If the - window is the last one in the current tab page the tab page is - closed. |tab-page| + editing it or 'bufhidden' is `unload`, `delete` or `wipe`). + If the window is the last one in the current tab page the tab + page is closed. |tab-page| The value of 'hidden' is irrelevant for this command. Changes to the buffer are not written and won't get lost, so diff --git a/runtime/filetype.vim b/runtime/filetype.vim index d7bac1bdcf..66e747a06f 100644 --- a/runtime/filetype.vim +++ b/runtime/filetype.vim @@ -1,7 +1,7 @@ " Vim support file to detect file types " " Maintainer: Bram Moolenaar <Bram@vim.org> -" Last Change: 2014 Nov 05 +" Last Change: 2014 Dec 06 " Listen very carefully, I will say this only once if exists("did_load_filetypes") @@ -1852,7 +1852,7 @@ au BufNewFile,BufRead sgml.catalog* call s:StarSetf('catalog') " Shell scripts (sh, ksh, bash, bash2, csh); Allow .profile_foo etc. " Gentoo ebuilds and Arch Linux PKGBUILDs are actually bash scripts -au BufNewFile,BufRead .bashrc*,bashrc,bash.bashrc,.bash_profile*,.bash_logout*,*.bash,*.ebuild,PKGBUILD* call SetFileTypeSH("bash") +au BufNewFile,BufRead .bashrc*,bashrc,bash.bashrc,.bash_profile*,.bash_logout*,.bash_aliases*,*.bash,*.ebuild,PKGBUILD* call SetFileTypeSH("bash") au BufNewFile,BufRead .kshrc*,*.ksh call SetFileTypeSH("ksh") au BufNewFile,BufRead */etc/profile,.profile*,*.sh,*.env call SetFileTypeSH(getline(1)) @@ -2259,6 +2259,9 @@ au BufNewFile,BufReadPost *.tssop setf tssop " TSS - Command Line (temporary) au BufNewFile,BufReadPost *.tsscl setf tsscl +" Tutor mode +au BufNewFile,BufReadPost *.tutor setf tutor + " TWIG files au BufNewFile,BufReadPost *.twig setf twig @@ -2506,6 +2509,8 @@ au BufNewFile,BufRead */etc/yum.conf setf dosini " Zimbu au BufNewFile,BufRead *.zu setf zimbu +" Zimbu Templates +au BufNewFile,BufRead *.zut setf zimbutempl " Zope " dtml (zope dynamic template markup language), pt (zope page template), diff --git a/runtime/ftplugin/tutor.vim b/runtime/ftplugin/tutor.vim new file mode 100644 index 0000000000..237176f5eb --- /dev/null +++ b/runtime/ftplugin/tutor.vim @@ -0,0 +1,43 @@ +" vim: fdm=marker + +" Base: {{{1 +call tutor#SetupVim() + +" Buffer Settings: {{{1 +if !exists('g:tutor_debug') || g:tutor_debug == 0 + setlocal buftype=nofile + setlocal concealcursor+=inv + setlocal conceallevel=2 +else + setlocal buftype= + setlocal concealcursor& + setlocal conceallevel=0 +endif +setlocal noundofile + +setlocal keywordprg=:help +setlocal iskeyword=@,-,_ + +setlocal foldmethod=expr +setlocal foldexpr=tutor#TutorFolds() +setlocal foldcolumn=3 +setlocal foldlevel=4 + +setlocal statusline=%{toupper(expand('%:t:r'))}\ tutorial%= +setlocal statusline+=%{tutor#InfoText()} + +" Mappings: {{{1 + +call tutor#SetNormalMappings() +call tutor#SetSampleTextMappings() + +" Checks: {{{1 + +sign define tutorok text=✓ texthl=tutorOK +sign define tutorbad text=✗ texthl=tutorX + +if !exists('g:tutor_debug') || g:tutor_debug == 0 + call tutor#PlaceXMarks() + autocmd! TextChanged <buffer> call tutor#OnTextChanged() + autocmd! TextChangedI <buffer> call tutor#OnTextChanged() +endif diff --git a/runtime/indent/php.vim b/runtime/indent/php.vim index b83a1923ed..1bffa7f195 100644 --- a/runtime/indent/php.vim +++ b/runtime/indent/php.vim @@ -3,8 +3,8 @@ " Author: John Wellesz <John.wellesz (AT) teaser (DOT) fr> " URL: http://www.2072productions.com/vim/indent/php.vim " Home: https://github.com/2072/PHP-Indenting-for-VIm -" Last Change: 2014 April 3rd -" Version: 1.49 +" Last Change: 2014 November 26th +" Version: 1.57 " " " Type :help php-indent for available options @@ -48,7 +48,7 @@ endif let b:did_indent = 1 -let php_sync_method = 0 +let g:php_sync_method = 0 @@ -112,7 +112,7 @@ setlocal nocindent setlocal nolisp setlocal indentexpr=GetPhpIndent() -setlocal indentkeys=0{,0},0),:,!^F,o,O,e,*<Return>,=?>,=<?,=*/ +setlocal indentkeys=0{,0},0),0],:,!^F,o,O,e,*<Return>,=?>,=<?,=*/ @@ -128,11 +128,14 @@ if exists("*GetPhpIndent") endif +let s:PHP_validVariable = '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*' let s:notPhpHereDoc = '\%(break\|return\|continue\|exit\|die\|else\)' let s:blockstart = '\%(\%(\%(}\s*\)\=else\%(\s\+\)\=\)\=if\>\|\%(}\s*\)\?else\>\|do\>\|while\>\|switch\>\|case\>\|default\>\|for\%(each\)\=\>\|declare\>\|class\>\|trait\>\|use\>\|interface\>\|abstract\>\|final\>\|try\>\|\%(}\s*\)\=catch\>\|\%(}\s*\)\=finally\>\)' -let s:functionDecl = '\<function\>\%(\s\+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*\)\=\s*(.*' +let s:functionDecl = '\<function\>\%(\s\+'.s:PHP_validVariable.'\)\=\s*(.*' let s:endline= '\s*\%(//.*\|#.*\|/\*.*\*/\s*\)\=$' -let s:terminated = '\%(\%(;\%(\s*\%(?>\|}\)\)\=\|<<<''\=\a\w*''\=$\|^\s*}\)'.s:endline.'\)\|^[^''"`]*[''"`]$' + + +let s:terminated = '\%(\%(;\%(\s*\%(?>\|}\)\)\=\|<<<''\=\a\w*''\=$\|^\s*}\|^\s*'.s:PHP_validVariable.':\)'.s:endline.'\)\|^[^''"`]*[''"`]$' let s:PHP_startindenttag = '<?\%(.*?>\)\@!\|<script[^>]*>\%(.*<\/script>\)\@!' @@ -140,7 +143,7 @@ let s:PHP_startindenttag = '<?\%(.*?>\)\@!\|<script[^>]*>\%(.*<\/script>\)\@!' let s:escapeDebugStops = 0 function! DebugPrintReturn(scriptLine) - if ! s:escapeDebugStops + if ! s:escapeDebugStops echo "debug:" . a:scriptLine let c = getchar() if c == "\<Del>" @@ -158,8 +161,6 @@ function! GetLastRealCodeLNum(startline) " {{{ let lnum = b:GetLastRealCodeLNum_ADD endif - let old_lnum = lnum - while lnum > 1 let lnum = prevnonblank(lnum) let lastline = getline(lnum) @@ -217,7 +218,7 @@ function! GetLastRealCodeLNum(startline) " {{{ let lnum=0 endif - if b:InPHPcode_and_script && !b:InPHPcode + if b:InPHPcode_and_script && 1 > b:InPHPcode let b:InPHPcode_and_script = 0 endif @@ -237,7 +238,7 @@ endfun function! Skippmatch() " {{{ let synname = synIDattr(synID(line("."), col("."), 0), "name") - if synname == "Delimiter" || synname == "phpRegionDelimiter" || synname =~# "^phpParent" || synname == "phpArrayParens" || synname =~# '^php\%(Block\|Brace\)' || synname == "javaScriptBraces" || synname =~# "^phpComment" && b:UserIsTypingComment + if synname == "Delimiter" || synname == "phpRegionDelimiter" || synname =~# "^phpParent" || synname == "phpArrayParens" || synname =~# '^php\%(Block\|Brace\)' || synname == "javaScriptBraces" || synname =~# '^php\%(Doc\)\?Comment' && b:UserIsTypingComment return 0 else return 1 @@ -249,7 +250,7 @@ function! FindOpenBracket(lnum, blockStarter) " {{{ let line = searchpair('{', '', '}', 'bW', 'Skippmatch()') if a:blockStarter == 1 - while line > 1 + while line > 1 let linec = getline(line) if linec =~ s:terminated || linec =~ '^\s*\%(' . s:blockstart . '\)\|'. s:functionDecl . s:endline @@ -310,7 +311,6 @@ let s:defaultORcase = '^\s*\%(default\|case\).*:' function! FindTheSwitchIndent (lnum) " {{{ - let test = GetLastRealCodeLNum(a:lnum - 1) if test <= 1 @@ -353,7 +353,7 @@ function! IslinePHP (lnum, tofind) " {{{ if synname == 'phpStringSingle' || synname == 'phpStringDouble' || synname == 'phpBacktick' if cline !~ '^\s*[''"`]' - return "" + return "SpecStringEntrails" else return synname end @@ -372,7 +372,7 @@ if ! s:autoresetoptions endif function! ResetPhpOptions() - if ! b:optionsset && &filetype == "php" + if ! b:optionsset && &filetype =~ "php" if b:PHP_autoformatcomment setlocal comments=s1:/*,mb:*,ex:*/,://,:# @@ -418,7 +418,7 @@ function! GetPhpIndent() let b:PHP_indentinghuge = 0 let b:PHP_CurrentIndentLevel = b:PHP_default_indenting endif - let b:PHP_lastindented = v:lnum + let real_PHP_lastindented = v:lnum let b:PHP_LastIndentedWasComment=0 let b:PHP_InsideMultilineComment=0 let b:PHP_indentbeforelast = 0 @@ -430,9 +430,12 @@ function! GetPhpIndent() elseif v:lnum > b:PHP_lastindented let real_PHP_lastindented = b:PHP_lastindented - let b:PHP_lastindented = v:lnum + else + let real_PHP_lastindented = v:lnum endif + let b:PHP_lastindented = v:lnum + if !b:InPHPcode_checked " {{{ One time check let b:InPHPcode_checked = 1 @@ -443,11 +446,15 @@ function! GetPhpIndent() endif if synname!="" - if synname != "phpHereDoc" && synname != "phpHereDocDelimiter" + if synname == "SpecStringEntrails" + let b:InPHPcode = -1 " thumb down + let b:UserIsTypingComment = 0 + let b:InPHPcode_tofind = "" + elseif synname != "phpHereDoc" && synname != "phpHereDocDelimiter" let b:InPHPcode = 1 let b:InPHPcode_tofind = "" - if synname =~# "^phpComment" + if synname =~# '^php\%(Doc\)\?Comment' let b:UserIsTypingComment = 1 else let b:UserIsTypingComment = 0 @@ -483,9 +490,16 @@ function! GetPhpIndent() if b:InPHPcode_tofind!="" if cline =~? b:InPHPcode_tofind - let b:InPHPcode = 1 let b:InPHPcode_tofind = "" let b:UserIsTypingComment = 0 + + if b:InPHPcode == -1 + let b:InPHPcode = 1 + return -1 + end + + let b:InPHPcode = 1 + if cline =~ '\*/' call cursor(v:lnum, 1) if cline !~ '^\*/' @@ -510,7 +524,7 @@ function! GetPhpIndent() endif endif - if b:InPHPcode + if 1 == b:InPHPcode if !b:InPHPcode_and_script && last_line =~ '\%(<?.*\)\@<!?>\%(.*<?\)\@!' && IslinePHP(lnum, '?>')=~"Delimiter" if cline !~? s:PHP_startindenttag @@ -520,8 +534,8 @@ function! GetPhpIndent() let b:InPHPcode_and_script = 1 endif - elseif last_line =~ '^[^''"`]\+[''"`]$' " a string identifier with nothing after it and no other string identifier before - let b:InPHPcode = 0 + elseif last_line =~ '^[^''"`]\+[''"`]$' + let b:InPHPcode = -1 let b:InPHPcode_tofind = substitute( last_line, '^.*\([''"`]\).*$', '^[^\1]*\1[;,]$', '') elseif last_line =~? '<<<''\=\a\w*''\=$' let b:InPHPcode = 0 @@ -538,7 +552,7 @@ function! GetPhpIndent() endif " }}} - if !b:InPHPcode && !b:InPHPcode_and_script + if 1 > b:InPHPcode && !b:InPHPcode_and_script return -1 endif @@ -568,7 +582,7 @@ function! GetPhpIndent() endif endif - if !b:PHP_InsideMultilineComment && cline =~ '^\s*/\*' && cline !~ '\*/\s*$' + if !b:PHP_InsideMultilineComment && cline =~ '^\s*/\*\%(.*\*/\)\@!' if getline(v:lnum + 1) !~ '^\s*\*' return -1 endif @@ -669,17 +683,17 @@ function! GetPhpIndent() endwhile elseif last_line =~# unstated && cline !~ '^\s*);\='.endline - let ind = ind + &sw " we indent one level further when the preceding line is not stated + let ind = ind + &sw return ind + addSpecial - elseif (ind != b:PHP_default_indenting || last_line =~ '^[)\]]' ) && last_line =~ terminated " Added || last_line =~ '^)' on 2007-12-30 (array indenting problem broke other things) + elseif (ind != b:PHP_default_indenting || last_line =~ '^[)\]]' ) && last_line =~ terminated let previous_line = last_line let last_line_num = lnum let LastLineClosed = 1 let isSingleLineBlock = 0 while 1 - if ! isSingleLineBlock && previous_line =~ '^\s*}\|;\s*}'.endline " XXX + if ! isSingleLineBlock && previous_line =~ '^\s*}\|;\s*}'.endline call cursor(last_line_num, 1) if previous_line !~ '^}' @@ -740,14 +754,19 @@ function! GetPhpIndent() endif endif - let plinnum = GetLastRealCodeLNum(lnum - 1) + if (last_line !~ '^\s*}\%(}}\)\@!') + let plinnum = GetLastRealCodeLNum(lnum - 1) + else + let plinnum = GetLastRealCodeLNum(FindOpenBracket(lnum, 1) - 1) + endif + let AntepenultimateLine = getline(plinnum) let last_line = substitute(last_line,"\\(//\\|#\\)\\(\\(\\([^\"']*\\([\"']\\)[^\"']*\\5\\)\\+[^\"']*$\\)\\|\\([^\"']*$\\)\\)",'','') if ind == b:PHP_default_indenting - if last_line =~ terminated + if last_line =~ terminated && last_line !~# s:defaultORcase let LastLineClosed = 1 endif endif @@ -755,10 +774,10 @@ function! GetPhpIndent() if !LastLineClosed - if last_line =~# '[{(\[]'.endline || last_line =~? '\h\w*\s*(.*,$' && AntepenultimateLine !~ '[,(]'.endline + if last_line =~# '[{(\[]'.endline || last_line =~? '\h\w*\s*(.*,$' && AntepenultimateLine !~ '[,(\[]'.endline let dontIndent = 0 - if last_line =~ '\S\+\s*{'.endline && last_line !~ '^\s*\%(' . s:blockstart . '\)\|'. s:functionDecl . s:endline + if last_line =~ '\S\+\s*{'.endline && last_line !~ '^\s*)\s*{'.endline && last_line !~ '^\s*\%(' . s:blockstart . '\)\|'. s:functionDecl . s:endline let dontIndent = 1 endif @@ -774,7 +793,7 @@ function! GetPhpIndent() elseif last_line =~ '\S\+\s*),'.endline call cursor(lnum, 1) - call search('),'.endline, 'W') " line never begins with ) so no need for 'c' flag + call search('),'.endline, 'W') let openedparent = searchpair('(', '', ')', 'bW', 'Skippmatch()') if openedparent != lnum let ind = indent(openedparent) @@ -784,7 +803,7 @@ function! GetPhpIndent() let ind = ind + &sw - elseif AntepenultimateLine =~ '\%(;\%(\s*\%(?>\|}\)\)\=\|<<<''\=\a\w*''\=$\|^\s*}\|{\)'.endline . '\|' . s:defaultORcase + elseif AntepenultimateLine =~ '{'.endline || AntepenultimateLine =~ terminated || AntepenultimateLine =~# s:defaultORcase let ind = ind + &sw endif diff --git a/runtime/macros/editexisting.vim b/runtime/macros/editexisting.vim index c18f22569f..3530e29dc4 100644 --- a/runtime/macros/editexisting.vim +++ b/runtime/macros/editexisting.vim @@ -1,6 +1,6 @@ " Vim Plugin: Edit the file with an existing Vim if possible " Maintainer: Bram Moolenaar -" Last Change: 2013 Feb 24 +" Last Change: 2014 Dec 06 " This is a plugin, drop it in your (Unix) ~/.vim/plugin or (Win32) " $VIM/vimfiles/plugin directory. Or make a symbolic link, so that you @@ -112,7 +112,7 @@ func! EditExisting(fname, command) endif if a:command != '' - exe "normal " . a:command + exe "normal! " . a:command endif redraw diff --git a/runtime/plugin/tutor.vim b/runtime/plugin/tutor.vim new file mode 100644 index 0000000000..1411b1ac63 --- /dev/null +++ b/runtime/plugin/tutor.vim @@ -0,0 +1,6 @@ +if exists('g:loaded_tutor_mode_plugin') || &compatible + finish +endif +let g:loaded_tutor_mode_plugin = 1 + +command! -nargs=? -complete=custom,tutor#TutorCmdComplete Tutor call tutor#TutorCmd(<q-args>) diff --git a/runtime/syntax/tutor.vim b/runtime/syntax/tutor.vim new file mode 100644 index 0000000000..99bcb78270 --- /dev/null +++ b/runtime/syntax/tutor.vim @@ -0,0 +1,83 @@ +if exists("b:current_syntax") + finish +endif + +syn include @VIM syntax/vim.vim +unlet b:current_syntax +syn include @TUTORSHELL syntax/sh.vim +unlet b:current_syntax +syn include @VIMNORMAL syntax/vimnormal.vim + +syn match tutorLink /\[.\{-}\](.\{-})/ contains=tutorInlineNormal +syn match tutorLinkBands /\[\|\]\|(\|)/ contained containedin=tutorLink,tutorLinkAnchor conceal +syn match tutorLinkAnchor /(.\{-})/ contained containedin=tutorLink conceal +syn match tutorURL /\(https\?\|file\):\/\/[[:graph:]]\+\>\/\?/ +syn match tutorEmail /\<[[:graph:]]\+@[[:graph:]]\+\>/ +syn match tutorInternalAnchor /\*[[:alnum:]-]\+\*/ contained conceal containedin=tutorSection + +syn match tutorSection /^#\{1,6}\s.\+$/ fold +syn match tutorSectionBullet /#/ contained containedin=tutorSection + +syn match tutorTOC /\ctable of contents:/ + +syn match tutorConcealedEscapes /\\[`*!\[\]()«»:$-]\@=/ conceal + +syn region tutorEmphasis matchgroup=Delimiter start=/[\*]\@<!\*\*\@!/ end=/[\*]\@<!\*\*\@!/ + \ concealends contains=tutorInlineCommand,tutorInlineNormal +syn region tutorBold matchgroup=Delimiter start=/\*\{2}/ end=/\*\{2}/ + \ concealends contains=tutorInlineCommand,tutorInlineNormal + +syn keyword tutorMarks TODO NOTE IMPORTANT TIP ATTENTION EXERCISE +syn keyword tutorMarks todo note tip attention exercise +syn keyword tutorMarks Todo Note Tip Excersise + +syn match tutorTextMark /\\\@<!--->/ conceal cchar=→ +syn region tutorSampleText start=/^\(--->\)\@=/ end=/$/ keepend contains=@SPELL +syn match tutorSampleTextMark /^--->/ contained containedin=tutorSampleText conceal cchar=→ +syn match tutorSampleTextExpect /\}\@<! {expect:.\+}\s*$/ contained containedin=tutorSampleText conceal +syn match tutorSampleTextExpect /|\@<! |expect:.\+|\s*$/ contained containedin=tutorSampleText conceal + +syn region tutorCodeblock matchgroup=Delimiter start=/^\~\{3}.*$/ end=/^\~\{3}/ + +syn region tutorShell matchgroup=Delimiter start=/^\~\{3} sh\s*$/ end=/^\~\{3}/ keepend contains=@TUTORSHELL concealends +syn match tutorShellPrompt /\(^\s*\)\@<=[$#]/ contained containedin=tutorShell + +syn region tutorInlineCode matchgroup=Delimiter start=/\\\@<!`/ end=/\\\@<!\(`{\@!\|`\s\)/ concealends + +syn region tutorCommand matchgroup=Delimiter start=/^\~\{3} cmd\( :\)\?\s*$/ end=/^\~\{3}/ keepend contains=@VIM concealends +syn region tutorInlineCommand matchgroup=Delimiter start=/\\\@<!`\(.*{vim}\)\@=/ end=/\\\@<!`\({vim}\)\@=/ nextgroup=tutorInlineType contains=@VIM concealends + +syn region tutorNormal matchgroup=Delimiter start=/^\~\{3} norm\(al\?\)\?\s*$/ end=/^\~\{3}/ contains=@VIMNORMAL concealends +syn region tutorInlineNormal matchgroup=Delimiter start=/\\\@<!`\(\S*{normal}\)\@=/ end=/\\\@<!`\({normal}\)\@=/ nextgroup=tutorInlineType contains=@VIMNORMAL concealends + +syn match tutorInlineType /{\(normal\|vim\)}/ contained conceal + +syn match tutorInlineOK /✓/ +syn match tutorInlineX /✗/ + +hi! tutorLink cterm=underline gui=underline ctermfg=lightblue guifg=#0088ff +hi! link tutorLinkBands Delimiter +hi! link tutorLinkAnchor Underlined +hi! link tutorInternalAnchor Underlined +hi! link tutorURL tutorLink +hi! link tutorEmail tutorLink + +hi! link tutorSection Title +hi! link tutorSectionBullet Delimiter + +hi! link tutorTOC Directory + +hi! tutorMarks cterm=bold gui=bold + +hi! tutorEmphasis gui=italic cterm=italic +hi! tutorBold gui=bold cterm=bold + +hi! link tutorSampleText Special +hi! tutorOK ctermfg=green guifg=#00ff88 cterm=bold gui=bold +hi! tutorX ctermfg=red guifg=#ff2000 cterm=bold gui=bold +hi! link tutorInlineOK tutorOK +hi! link tutorInlineX tutorX + +hi! link tutorShellPrompt Delimiter + +let b:current_syntax = "tutor" diff --git a/runtime/syntax/vimnormal.vim b/runtime/syntax/vimnormal.vim new file mode 100644 index 0000000000..df672c5236 --- /dev/null +++ b/runtime/syntax/vimnormal.vim @@ -0,0 +1,16 @@ +syn match normalOp /[dcrypoaxv!"#%&.-\/:<>=?@ABCDGHIJKLMNOPQRSUVWXYZgmqstz~iu]/ nextgroup=normalMod +syn match normalMod /m\@<![ia]/ +syn match normalObject /["'()<>BW\[\]`bstweE{}ftFT;,$]/ +syn match normalCount /[0-9]/ +syn region normalSearch start=/[/?]\@<=./ end=/.<CR>\@=/ contains=normalKey keepend +syn region normalChange start=/\([cr][wWbBeE()\[\]{}pst]\)\@<=./ end=/.\@=/ contains=normalKey keepend +syn match normalCharSearch /\c[ftr]\@<=\w/ +syn match normalMark /\(f\@<!m\)\@<=[a-zA-Z0-9]/ +syn match normalKey /<'\@!.\{-}>'\@!/ + +hi! link normalOp Operator +hi! link normalMod PreProc +hi! link normalObject Structure +hi! link normalCount Number +hi! link normalMark Identifier +hi! link normalKey Special diff --git a/runtime/syntax/zimbu.vim b/runtime/syntax/zimbu.vim index c859a2f815..1a7a485e6f 100644 --- a/runtime/syntax/zimbu.vim +++ b/runtime/syntax/zimbu.vim @@ -1,7 +1,7 @@ " Vim syntax file " Language: Zimbu " Maintainer: Bram Moolenaar -" Last Change: 2012 Jun 01 +" Last Change: 2014 Nov 23 if exists("b:current_syntax") finish @@ -12,7 +12,10 @@ syn include @Ccode syntax/c.vim syn keyword zimbuTodo TODO FIXME XXX contained syn match zimbuNoBar "|" contained syn match zimbuParam "|[^| ]\+|" contained contains=zimbuNoBar -syn match zimbuComment "#.*$" contains=zimbuTodo,zimbuParam,@Spell +syn match zimbuNoBacktick "`" contained +syn match zimbuCode "`[^`]\+`" contained contains=zimbuNoBacktick +syn match zimbuComment "#.*$" contains=zimbuTodo,zimbuParam,zimbuCode,@Spell +syn match zimbuComment "/\*.\{-}\*/" contains=zimbuTodo,zimbuParam,zimbuCode,@Spell syn match zimbuChar "'\\\=.'" @@ -28,27 +31,32 @@ syn keyword zimbuBasicType fixed1 fixed2 fixed3 fixed4 fixed5 fixed6 syn keyword zimbuBasicType fixed7 fixed8 fixed9 fixed10 fixed11 fixed12 syn keyword zimbuBasicType fixed13 fixed14 fixed15 -syn keyword zimbuCompType string stringval cstring varstring -syn keyword zimbuCompType bytes varbytes -syn keyword zimbuCompType tuple array list dict multiDict set multiSet +syn keyword zimbuCompType string varString +syn keyword zimbuCompType byteString varByteString +syn keyword zimbuCompType tuple array list dict dictList set callback +syn keyword zimbuCompType sortedList multiDict multiDictList multiSet syn keyword zimbuCompType complex complex32 complex64 complex80 complex128 syn keyword zimbuCompType proc func def thread evalThread lock cond pipe -syn keyword zimbuType VAR ANY USE GET +syn keyword zimbuType VAR dyn type USE GET syn match zimbuType "IO.File" syn match zimbuType "IO.Stat" -syn keyword zimbuStatement IF ELSE ELSEIF WHILE REPEAT FOR IN TO STEP +syn keyword zimbuStatement IF ELSE ELSEIF IFNIL WHILE REPEAT FOR IN TO STEP syn keyword zimbuStatement DO UNTIL SWITCH WITH syn keyword zimbuStatement TRY CATCH FINALLY syn keyword zimbuStatement GENERATE_IF GENERATE_ELSE GENERATE_ELSEIF +syn keyword zimbuStatement GENERATE_ERROR +syn keyword zimbuStatement BUILD_IF BUILD_ELSE BUILD_ELSEIF syn keyword zimbuStatement CASE DEFAULT FINAL ABSTRACT VIRTUAL DEFINE REPLACE syn keyword zimbuStatement IMPLEMENTS EXTENDS PARENT LOCAL -syn keyword zimbuStatement PART ALIAS CONNECT WRAP +syn keyword zimbuStatement PART ALIAS TYPE CONNECT WRAP syn keyword zimbuStatement BREAK CONTINUE PROCEED -syn keyword zimbuStatement RETURN EXIT THROW +syn keyword zimbuStatement RETURN EXIT THROW DEFER syn keyword zimbuStatement IMPORT AS OPTIONS MAIN -syn keyword zimbuStatement INTERFACE MODULE ENUM BITS SHARED +syn keyword zimbuStatement INTERFACE PIECE INCLUDE MODULE ENUM BITS +syn keyword zimbuStatement SHARED STATIC +syn keyword zimbuStatement LAMBDA syn match zimbuStatement "\<\(FUNC\|PROC\|DEF\)\>" syn match zimbuStatement "\<CLASS\>" syn match zimbuStatement "}" @@ -61,10 +69,13 @@ syn match zimbuAttribute "@default\>" syn match zimbuAttribute "@define\>" syn match zimbuAttribute "@replace\>" syn match zimbuAttribute "@final\>" +syn match zimbuAttribute "@primitive\>" +syn match zimbuAttribute "@notOnExit\>" syn match zimbuAttribute "@private\>" syn match zimbuAttribute "@protected\>" syn match zimbuAttribute "@public\>" +syn match zimbuAttribute "@local\>" syn match zimbuAttribute "@file\>" syn match zimbuAttribute "@directory\>" syn match zimbuAttribute "@read=private\>" @@ -78,15 +89,22 @@ syn match zimbuAttribute "@items=public\>" syn match zimbuAttribute "@items=file\>" syn match zimbuAttribute "@items=directory\>" -syn keyword zimbuMethod NEW EQUAL COPY COMPARE SIZE GET SET +syn keyword zimbuMethod NEW EQUAL COPY COMPARE SIZE GET SET INIT EARLYINIT syn keyword zimbuOperator IS ISNOT ISA ISNOTA -syn keyword zimbuModule ARG CHECK E IO PROTO SYS HTTP ZC ZWT TIME THREAD +syn keyword zimbuModule ARG CHECK E GC IO LOG PROTO SYS HTTP ZC ZWT T TIME THREAD -syn match zimbuString +"\([^"\\]\|\\.\)*\("\|$\)+ +syn match zimbuImport "\.\zsPROTO" +syn match zimbuImport "\.\zsCHEADER" + +"syn match zimbuString +"\([^"\\]\|\\.\)*\("\|$\)+ contains=zimbuStringExpr +syn region zimbuString start=+"+ skip=+[^"\\]\|\\.+ end=+"\|$+ contains=zimbuStringExpr syn match zimbuString +R"\([^"]\|""\)*\("\|$\)+ -syn region zimbuString start=+'''+ end=+'''+ +syn region zimbuLongString start=+''"+ end=+"''+ +syn match zimbuStringExpr +\\([^)]*)+hs=s+2,he=e-1 contained contains=zimbuString,zimbuParenPairOuter +syn region zimbuParenPairOuter start=+(+ms=s+1 end=+)+me=e-1 contained contains=zimbuString,zimbuParenPair +syn region zimbuParenPair start=+(+ end=+)+ contained contains=zimbuString,zimbuParenPair syn keyword zimbuFixed TRUE FALSE NIL THIS THISTYPE FAIL OK syn keyword zimbuError NULL @@ -97,12 +115,18 @@ syn match zimbuSpaceError display excludenl "\S\s\+$"ms=s+1 syn match zimbuSpaceError display " \+\t" syn match zimbuSpaceError display "\t\+ " -syn match zimbuUses contained "uses([a-zA-Z_ ,]*)" +syn match zimbuUses contained "\<uses([a-zA-Z_ ,]*)" +syn match zimbuBlockgc contained "blockgc" syn match zimbuBlockComment contained " #.*" -syn region zimbuCregion matchgroup=zimbuCblock start="^>>>" end="^<<<.*" contains=@Ccode,zimbuUses,zimbuBlockComment keepend +syn region zimbuCregion matchgroup=zimbuCblock start="^>>>" end="^<<<.*" contains=@Ccode,zimbuUses,zimbuBlockgc,zimbuBlockComment keepend + +" Assume long strings and C regions don't take more than 200 lines. +syn sync minlines=200 -syn sync minlines=2000 +" When we find the start of a long string, without a # or " before it, we are +" sure to be inside a long string. +syn sync match zimbuLongStringSync grouphere zimbuLongString +^[^"#]*''\"+ hi def link zimbuBasicType Type hi def link zimbuCompType Type @@ -111,17 +135,23 @@ hi def link zimbuStatement Statement hi def link zimbuOperator Statement hi def link zimbuMethod PreProc hi def link zimbuModule PreProc +hi def link zimbuImport PreProc hi def link zimbuUses PreProc +hi def link zimbuBlockgc PreProc hi def link zimbuAttribute PreProc hi def link zimbuString Constant +hi def link zimbuLongString Special hi def link zimbuChar Constant hi def link zimbuFixed Constant hi def link zimbuComment Comment +hi def link zimbuCommentStart zimbuComment hi def link zimbuBlockComment Comment hi def link zimbuCblock Comment hi def link zimbuTodo Todo hi def link zimbuParam Constant +hi def link zimbuCode Statement hi def link zimbuNoBar Ignore +hi def link zimbuNoBacktick Ignore hi def link zimbuSpaceError Error hi def link zimbuError Error diff --git a/runtime/tutor/en/vim-01-beginner.tutor b/runtime/tutor/en/vim-01-beginner.tutor new file mode 100644 index 0000000000..bda4e3537e --- /dev/null +++ b/runtime/tutor/en/vim-01-beginner.tutor @@ -0,0 +1,955 @@ +# Welcome to the VIM Tutor + +Vim is a very powerful editor that has many commands, too many to explain in a +tutor such as this. This tutor is designed to describe enough of the commands +that you will be able to easily use Vim as an all-purpose editor. It is +IMPORTANT to remember that this tutor is set up to teach by use. That means +that you need to do the exercises to learn them properly. If you only read +the text, you will soon forget what is most important! + +NOTE: The commands in the lessons will modify the text, but those changes won't +be saved. Don't worry about messing things up; just remember that pressing +[<Esc>](<Esc>) and then [u](u) will undo the latest change. + +This tutorial is interactive, and there are a few things you should know. +Pressing [<Enter>](<Enter>) over text highlighted [like this](holy-grail) will take you to some relevant +help (hopefully), and pressing K over any word will try to do so too. Sometimes +you will be required to modify text like +---> this here {expect:this here} +Once you have done the changes correctly, the ✗ sign at the left will change +to ✓. I imagine you can already see how neat Vim can be ;) +Other times, you'll be prompted to run a command (I'll explain this later): +~~~ cmd + :help <Enter> +~~~ +or press a sequence of keys +~~~ normal + <Esc>0f<Space>d3wP$P +~~~ + +Text within <'s and >'s (like `<Enter>`{normal}) describes a key to press instead of text +to type. + +Now, make sure that your Shift-Lock key is NOT depressed and press the `j`{normal} +key enough times to move the cursor so that Lesson 1.1 completely fills the +screen. + +## Lesson 1.1: MOVING THE CURSOR + +** To move the cursor, press the `h`, `j`, `k`, `l` keys as indicated. ** + + ↑ + k Hint: The h key is at the left and moves left. + ← h l → The l key is at the right and moves right. + j The j key looks like a down arrow. + ↓ + + 1. Move the cursor around the screen until you are comfortable. + + 2. Hold down the down key (j) until it repeats. + Now you know how to move to the next lesson. + + 3. Using the down key, move to Lesson 1.2. + +NOTE: If you are ever unsure about something you typed, press <Esc> to place + you in Normal mode. Then retype the command you wanted. + +NOTE: The cursor keys should also work. But using hjkl you will be able to + move around much faster, once you get used to it. Really! + +# Lesson 1.2: EXITING VIM + +!! NOTE: Before executing any of the steps below, read this entire lesson !! + + 1. Press the <Esc> key (to make sure you are in [Normal mode](). + + 2. Type: + + `:q!`{vim} `<Enter>`{normal}. + + This exits the editor, DISCARDING any changes you have made. + + 3. Open vim and get back here by executing the command that got you into this + tutor. That might be: + + :Tutor <Enter> + + 4. If you have these steps memorized and are confident, execute steps + 1 through 3 to exit and re-enter the editor. + +NOTE: [:q!](:q) <Enter> discards any changes you made. In a few lessons you + will learn how to save the changes to a file. + + 5. Move the cursor down to Lesson 1.3. + +## Lesson 1.3: TEXT EDITING - DELETION + +** Press `x`{normal} to delete the character under the cursor. ** + +1. Move the cursor to the line below marked --->. + +2. To fix the errors, move the cursor until it is on top of the + character to be deleted. + +3. Press [the x key](x) to delete the unwanted character. + +4. Repeat steps 2 through 4 until the sentence is correct. + +---> The ccow jumpedd ovverr thhe mooon. {expect:The cow jumped over the moon.} + +5. Now that the line is correct, go on to Lesson 1.4. + +NOTE: As you go through this tutor, do not try to memorize, learn by usage. + +# Lesson 1.4: TEXT EDITING: INSERTION + +** Press `i`{normal} to insert text. ** + + 1. Move the cursor to the first line below marked --->. + + 2. To make the first line the same as the second, move the cursor on top + of the first character AFTER where the text is to be inserted. + + 3. Press i and type in the necessary additions. + + 4. As each error is fixed press <Esc> to return to Normal mode. + Repeat steps 2 through 4 to correct the sentence. + +---> There is text misng this . {expect:There is some text missing from this line.} +---> There is some text missing from this line. {expect:There is some text missing from this line.} + + 5. When you are comfortable inserting text move to lesson 1.5. + +# Lesson 1.5: TEXT EDITING: APPENDING + +** Press `A`{normal} to append text. ** + + 1. Move the cursor to the first line below marked --->. + It does not matter on what character the cursor is in that line. + + 2. Press [A](A) and type in the necessary additions. + + 3. As the text has been appended press <Esc> to return to Normal mode. + + 4. Move the cursor to the second line marked ---> and repeat + steps 2 and 3 to correct this sentence. + +---> There is some text missing from th {expect:There is some text missing from this line.} +---> There is some text missing from this line. {expect:There is some text missing from this line.} +---> There is also some text miss {expect:There is also some text missing here.} +---> There is also some text missing here. {expect:There is also some text missing here.} + + 5. When you are comfortable appending text move to lesson 1.6. + +# Lesson 1.6: EDITING A FILE + +** Use `:wq`{vim} to save a file and exit. ** + +!! NOTE: Before executing any of the steps below, read this entire lesson !! + + 1. Exit this tutor as you did in lesson 1.2: :q! + Or, if you have access to another terminal, do the following there. + + 2. At the shell prompt type this command: +~~~ sh + $ vim tutor +~~~ + 'vim' is the command to start the Vim editor, 'tutor' is the name of the + file you wish to edit. Use a file that may be changed. + + 3. Insert and delete text as you learned in the previous lessons. + + 4. Save the file with changes and exit Vim with: +~~~ cmd + :wq +~~~ + + Note you'll need to press `<Enter>` to execute the command. + + 5. If you have quit vimtutor in step 1 restart the vimtutor and move down to + the following summary. + + 6. After reading the above steps and understanding them: do it. + +# Lesson 1 SUMMARY + + 1. The cursor is moved using either the arrow keys or the hjkl keys. + h (left) j (down) k (up) l (right) + + 2. To start Vim from the shell prompt type: + +~~~ sh + $ vim FILENAME +~~~ + + 3. To exit Vim type: <Esc> :q! <Enter> to trash all changes. + OR type: <Esc> :wq <Enter> to save the changes. + + 4. To delete the character at the cursor type: `x`{normal} + + 5. To insert or append text type: + `i`{normal} type inserted text `<Esc>`{normal} insert before the cursor + `A`{normal} type appended text `<Esc>`{normal} append after the line + +NOTE: Pressing `<Esc>`{normal} will place you in Normal mode or will cancel + an unwanted and partially completed command. + +Now continue with Lesson 2. + +# Lesson 2.1: DELETION COMMANDS + +** Type `dw`{normal} to delete a word. ** + + 1. Press `<Esc>`{normal} to make sure you are in Normal mode. + + 2. Move the cursor to the line below marked --->. + + 3. Move the cursor to the beginning of a word that needs to be deleted. + + 4. Type [d](d)[w](w) to make the word disappear. + +---> There are a some words fun that don't belong paper in this sentence. {expect:There are some words that don't belong in this sentence.} + + 5. Repeat steps 3 and 4 until the sentence is correct and go to Lesson 2.2. + +# Lesson 2.2: MORE DELETION COMMANDS + +** Type `d$`{normal} to delete to the end of the line. ** + + 1. Press <Esc> to make sure you are in Normal mode. + + 2. Move the cursor to the line below marked --->. + + 3. Move the cursor to the end of the correct line (AFTER the first . ). + + 4. Type `d$`{normal} to delete to the end of the line. + +---> Somebody typed the end of this line twice. end of this line twice. {expect:ANYTHING} + + 5. Move on to Lesson 2.3 to understand what is happening. + +# Lesson 2.3: ON OPERATORS AND MOTIONS + + +Many commands that change text are made from an [operator](operator) and a [motion](navigation). +The format for a delete command with the [d](d) delete operator is as follows: + + d motion + + Where: + d - is the delete operator. + motion - is what the operator will operate on (listed below). + + A short list of motions: + [w](w) - until the start of the next word, EXCLUDING its first character. + [e](e) - to the end of the current word, INCLUDING the last character. + [$]($) - to the end of the line, INCLUDING the last character. + + Thus typing `de`{normal} will delete from the cursor to the end of the word. + +NOTE: Pressing just the motion while in Normal mode without an operator will + move the cursor as specified. + +# Lesson 2.4: USING A COUNT FOR A MOTION + +** Typing a number before a motion repeats it that many times. ** + + 1. Move the cursor to the start of the line marked ---> below. + + 2. Type `2w`{normal} to move the cursor two words forward. + + 3. Type `3e`{normal} to move the cursor to the end of the third word forward. + + 4. Type `0`{normal} ([zero](0)) to move to the start of the line. + + 5. Repeat steps 2 and 3 with different numbers. + +---> This is just a line with words you can move around in. {expect:ANYTHING} + + 6. Move on to Lesson 2.5. + +# Lesson 2.5: USING A COUNT TO DELETE MORE + +** Typing a number with an operator repeats it that many times. ** + +In the combination of the delete operator and a motion mentioned above you +insert a count before the motion to delete more: + d number motion + + 1. Move the cursor to the first UPPER CASE word in the line marked --->. + + 2. Type `d2w`{normal} to delete the two UPPER CASE words + + 3. Repeat steps 1 and 2 with a different count to delete the consecutive + UPPER CASE words with one command + +---> this ABC DE line FGHI JK LMN OP of words is Q RS TUV cleaned up. {expect:this line of words is cleaned up.} + +# Lesson 2.6: OPERATING ON LINES + +** Type dd to delete a whole line. ** + + Due to the frequency of whole line deletion, the designers of Vi decided + it would be easier to simply type two d's to delete a line. + + 1. Move the cursor to the second line in the phrase below. + 2. Type [dd](dd) to delete the line. + 3. Now move to the fourth line. + 4. Type `2dd`{normal} to delete two lines. + +---> 1) Roses are red, {expect:ANYTHING} +---> 2) Mud is fun, {expect:ANYTHING} +---> 3) Violets are blue, {expect:ANYTHING} +---> 4) I have a car, {expect:ANYTHING} +---> 5) Clocks tell time, {expect:ANYTHING} +---> 6) Sugar is sweet {expect:ANYTHING} +---> 7) And so are you. {expect:ANYTHING} + +# Lesson 2.7: THE UNDO COMMAND + +** Press u to undo the last commands, U to fix a whole line. ** + + 1. Move the cursor to the line below marked ---> and place it on the + first error. + 2. Type `x`{normal} to delete the first unwanted character. + 3. Now type `u`{normal} to undo the last command executed. + 4. This time fix all the errors on the line using the `x`{normal} command. + 5. Now type a capital `U`{normal} to return the line to its original state. + 6. Now type `u`{normal} a few times to undo the U and preceding commands. + 7. Now type `<Ctrl-r>`{normal} a few times to redo the commands (undo the undo's). + +---> Fiix the errors oon thhis line and reeplace them witth undo. {expect:Fix the errors on this line and replace them with undo.} + + 8. These are very useful commands. Now move on to the Lesson 2 Summary. + +# Lesson 2 SUMMARY + + 1. To delete from the cursor up to the next word type: `dw`{normal} + 2. To delete from the cursor to the end of a line type: `d$`{normal} + 3. To delete a whole line type: `dd`{normal} + 4. To repeat a motion prepend it with a number: `2w`{normal} + + 5. The format for a change command is: + operator [number] motion + where: + operator - is what to do, such as [d](d) for delete + [number] - is an optional count to repeat the motion + motion - moves over the text to operate on, such as: + [w](w) (word), + [$]($) (to the end of line), etc. + + 6. To move to the start of the line use a zero: [0](0) + + 7. To undo previous actions, type: `u`{normal} (lowercase u) + To undo all the changes on a line, type: `U`{normal} (capital U) + To undo the undo's, type: `<Ctrl-r>`{normal} + +# Lesson 3.1: THE PUT COMMAND + +** Type p to put previously deleted text after the cursor. ** + + 1. Move the cursor to the first ---> line below. + + 2. Type `dd`{normal} to delete the line and store it in a Vim register. + + 3. Move the cursor to the c) line, ABOVE where the deleted line should go. + + 4. Type `p`{normal} to put the line below the cursor. + + 5. Repeat steps 2 through 4 to put all the lines in correct order. + +---> d) Can you learn too? {expect:ANYTHING} +---> b) Violets are blue, {expect:ANYTHING} +---> c) Intelligence is learned, {expect:ANYTHING} +---> a) Roses are red, {expect:ANYTHING} + +# Lesson 3.2: THE REPLACE COMMAND + +** Type `rx`{normal} to replace the character at the cursor with x. ** + + 1. Move the cursor to the first line below marked --->. + + 2. Move the cursor so that it is on top of the first error. + + 3. Type `r`{normal} and then the character which should be there. + + 4. Repeat steps 2 and 3 until the first line is equal to the second one. + +---> Whan this lime was tuoed in, someone presswd some wrojg keys! {expect:When this line was typed in, someone pressed some wrong keys!} +---> When this line was typed in, someone pressed some wrong keys! {expect:When this line was typed in, someone pressed some wrong keys!} + + 5. Now move on to Lesson 3.3. + +NOTE: Remember that you should be learning by doing, not memorization. + +# Lesson 3.3: THE CHANGE OPERATOR + +** To change until the end of a word, type `ce`{normal} ** + + 1. Move the cursor to the first line below marked --->. + + 2. Place the cursor on the "u" in "lubw". + + 3. Type `ce`{normal} and the correct word (in this case, type "ine" ). + + 4. Press `<Esc>`{normal} and move to the next character that needs to be changed. + + 5. Repeat steps 3 and 4 until the first sentence is the same as the second. + +---> This lubw has a few wptfd that mrrf changing usf the change operator. {expect:This line has a few words that need changing using the change operator.} +---> This line has a few words that need changing using the change operator. {expect:This line has a few words that need changing using the change operator.} + +Notice that [c](c)e deletes the word and places you in Insert mode. + +# Lesson 3.4: MORE CHANGES USING c + +** The change operator is used with the same motions as delete. ** + + 1. The change operator works in the same way as delete. The format is: + + c [number] motion + + 2. The motions are the same, such as `w`{normal} (word) and `$`{normal} (end of line). + + 3. Move to the first line below marked --->. + + 4. Move the cursor to the first error. + + 5. Type `c$`{normal} and type the rest of the line like the second and press `<Esc>`{normal}. + +---> The end of this line needs some help to make it like the second. {expect:The end of this line needs to be corrected using the c$ command.} +---> The end of this line needs to be corrected using the c$ command. {expect:The end of this line needs to be corrected using the c$ command.} + +NOTE: You can use the Backspace key to correct mistakes while typing. + +# Lesson 3 SUMMARY + + 1. To put back text that has just been deleted, type [p](p). This puts the + deleted text AFTER the cursor (if a line was deleted it will go on the + line below the cursor). + + 2. To replace the character under the cursor, type [r](r) and then the + character you want to have there. + + 3. The [change operator](c) allows you to change from the cursor to where the + motion takes you. eg. Type `ce`{normal} to change from the cursor to the end of + the word, `c$`{normal} to change to the end of a line. + + 4. The format for change is: + + c [number] motion + +Now go on to the next lesson. + +# Lesson 4.1: CURSOR LOCATION AND FILE STATUS + +** Type `<Ctrl-g>`{normal} to show your location in the file and the file status. + Type `G`{normal} to move to a line in the file. ** + +NOTE: Read this entire lesson before executing any of the steps!! + + 1. Hold down the `<Ctrl>`{normal} key and press `g`{normal}. We call this `<Ctrl-g>`{normal}. + A message will appear at the bottom of the page with the filename and the + position in the file. Remember the line number for Step 3. + +NOTE: You may see the cursor position in the lower right corner of the screen + This happens when the ['ruler']('ruler') option is set (see :help 'ruler' ) + + 2. Press [G](G) to move you to the bottom of the file. + Type [gg](gg) to move you to the start of the file. + + 3. Type the number of the line you were on and then `G`{normal} . This will + return you to the line you were on when you first pressed `<Ctrl-g>`{normal}. + + 4. If you feel confident to do this, execute steps 1 through 3. + +# Lesson 4.2: THE SEARCH COMMAND + +** Type `/`{normal} followed by a phrase to search for the phrase. ** + + 1. In Normal mode type the `/`{normal} character. Notice that it and the cursor + appear at the bottom of the screen as with the : command. + + 2. Now type 'errroor' `<Enter>`{normal}. This is the word you want to search for. + + 3. To search for the same phrase again, simply type [n](n) . + To search for the same phrase in the opposite direction, type [N](N) . + + 4. To search for a phrase in the backward direction, use [?](?) instead of / . + + 5. To go back to where you came from press `<Ctrl-o>`{normal} (Keep Ctrl down while + pressing the letter o). Repeat to go back further. `<Ctrl-i>`{normal} goes forward. + +---> "errroor" is not the way to spell error; errroor is an error. {expect:ANYTHING} + +NOTE: When the search reaches the end of the file it will continue at the + start, unless the ['wrapscan']('wrapscan') option has been reset. + +# Lesson 4.3: MATCHING PARENTHESES SEARCH + +** Type `%`{normal} to find a matching ),], or }. ** + + 1. Place the cursor on any (, [, or { in the line below marked --->. + + 2. Now type the [%](%) character. + + 3. The cursor will move to the matching parenthesis or bracket. + + 4. Type `%`{normal} to move the cursor to the other matching bracket. + + 5. Move the cursor to another (,),[,],{ or } and see what `%`{normal} does. + +---> This ( is a test line with ('s, ['s ] and {'s } in it. )) {expect:ANYTHING} + +NOTE: This is very useful in debugging a program with unmatched parentheses! + +# Lesson 4.4: THE SUBSTITUTE COMMAND + +** Type `:s/old/new/g` to substitute "new" for "old". ** + + 1. Move the cursor to the line below marked --->. + + 2. Type +~~~ cmd + :s/thee/the/ +~~~ + + NOTE that the [:s](:s) command only changed the first occurrence of "thee" in the line. + + 3. Now type +~~~ cmd + :s/thee/the/g +~~~ + + Adding the g [flag](:s_flags) means to substitute globally in the line, change + all occurrences of "thee" in the line. + +---> thee best time to see thee flowers is in thee spring. {expect:the best time to see the flowers is in the spring.} + + 4. To change every occurrence of a character string between two lines, type +~~~ cmd + :#,#s/old/new/g +~~~ + where #,# are the line numbers of the range of lines where the substitution is to be done. + + Type +~~~ cmd + :%s/old/new/g +~~~ + to change every occurrence in the whole file. + + Type +~~~ cmd + :%s/old/new/gc +~~~ + to find every occurrence in the whole file, with a prompt whether to substitute or not. + +# Lesson 4 SUMMARY + + 1. `<Ctrl-g>`{normal} displays your location in the file and the file status. + `G`{normal} moves to the end of the file. + number `G`{normal} moves to that line number. + `gg`{normal} moves to the first line. + + 2. Typing `/`{normal} followed by a phrase searches FORWARD for the phrase. + Typing `?`{normal} followed by a phrase searches BACKWARD for the phrase. + After a search type `n`{normal} to find the next occurrence in the same direction + or `N`{normal} to search in the opposite direction. + `<Ctrl-o>`{normal} takes you back to older positions, `<Ctrl-i>`{normal} to newer positions. + + 3. Typing `%`{normal} while the cursor is on a (,),[,],{, or } goes to its match. + + 4. To substitute new for the first old in a line type +~~~ cmd + :s/old/new +~~~ + To substitute new for all 'old's on a line type +~~~ cmd + :s/old/new/g +~~~ + To substitute phrases between two line #'s type +~~~ cmd + :#,#s/old/new/g +~~~ + To substitute all occurrences in the file type +~~~ cmd + :%s/old/new/g +~~~ + To ask for confirmation each time add 'c' +~~~ cmd + :%s/old/new/gc +~~~ + +# Lesson 5.1: HOW TO EXECUTE AN EXTERNAL COMMAND + +** Type `:!`{vim} followed by an external command to execute that command. ** + + 1. Type the familiar command `:`{normal} to set the cursor at the bottom of the + screen. This allows you to enter a command-line command. + + 2. Now type the [!](!cmd) (exclamation point) character. This allows you to + execute any external shell command. + + 3. As an example type "ls" following the "!" and then hit `<Enter>`{normal}. This + will show you a listing of your directory, just as if you were at the + shell prompt. + +NOTE: It is possible to execute any external command this way, also with + arguments. + +NOTE: All : commands must be finished by hitting <Enter> + From here on we will not always mention it. + +# Lesson 5.2: MORE ON WRITING FILES + +** To save the changes made to the text, type `:w`{vim} FILENAME. ** + + 1. Type `:!ls`{vim} to get a listing of your directory. + You already know you must hit `<Enter>`{normal} after this. + + 2. Choose a filename that does not exist yet, such as TEST. + + 3. Now type: +~~~ cmd + :w TEST +~~~ + (where TEST is the filename you chose.) + + 4. This saves the whole file (the Vim Tutor) under the name TEST. + To verify this, type `:!ls`{vim} again to see your directory. + +NOTE: If you were to exit Vim and start it again with vim TEST , the file + would be an exact copy of the tutor when you saved it. + + 5. Now remove the file by typing: +~~~ cmd + :!rm TEST +~~~ + +# Lesson 5.3: SELECTING TEXT TO WRITE + +** To save part of the file, type `v`{normal} motion `:w FILENAME`{vim} ** + + 1. Move the cursor to this line. + + 2. Press [v](v) and move the cursor to the fifth item below. Notice that the + text is highlighted. + + 3. Press the `:`{normal} character. At the bottom of the screen + + :'<,'> + + will appear. + + 4. Type + + `:w TEST`{vim} + + where TEST is a filename that does not exist yet. Verify that you see + + `:'<,'>w TEST`{vim} + + before you press `<Enter>`{normal}. + + 5. Vim will write the selected lines to the file TEST. Use `:!ls`{vim} to see it. + Do not remove it yet! We will use it in the next lesson. + +NOTE: Pressing [v](v) starts [Visual selection](visual-mode). You can move + the cursor around to make the selection bigger or smaller. Then you can + use an operator to do something with the text. For example, `d`{normal} deletes + the text. + +# Lesson 5.4: RETRIEVING AND MERGING FILES + +** To insert the contents of a file, type `:r FILENAME`{vim} ** + + 1. Place the cursor just above this line. + +NOTE: After executing Step 2 you will see text from Lesson 5.3. Then move + DOWN to see this lesson again. + + 2. Now retrieve your TEST file using the command + + `:r TEST`{vim} + + where TEST is the name of the file you used. + The file you retrieve is placed below the cursor line. + + 3. To verify that a file was retrieved, cursor back and notice that there + are now two copies of Lesson 5.3, the original and the file version. + +NOTE: You can also read the output of an external command. For example, + + `:r !ls`{vim} + + reads the output of the `ls` command and puts it below the cursor. + +# Lesson 5 SUMMARY + + 1. [:!command](:!cmd) executes an external command. + + Some useful examples are: + `:!ls`{vim} - shows a directory listing + `:!rm FILENAME`{vim} - removes file FILENAME + + 2. [:w](:w) FILENAME writes the current Vim file to disk with + name FILENAME. + + 3. [v](v) motion :w FILENAME saves the Visually selected lines in file + FILENAME. + + 4. [:r](:r) FILENAME retrieves disk file FILENAME and puts it + below the cursor position. + + 5. [:r !dir](:r!) reads the output of the dir command and + puts it below the cursor position. + +# Lesson 6.1: THE OPEN COMMAND + +** Type `o`{normal} to open a line below the cursor and place you in Insert mode. ** + + 1. Move the cursor to the line below marked --->. + + 2. Type the lowercase letter `o`{normal} to [open](o) up a line BELOW the cursor and place + you in Insert mode. + + 3. Now type some text and press `<Esc>`{normal} to exit Insert mode. + +---> After typing o the cursor is placed on the open line in Insert mode. {expect:ANYTHING} + + 4. To open up a line ABOVE the cursor, simply type a [capital O](O), rather + than a lowercase `o`{normal}. Try this on the line below. + +---> Open up a line above this by typing O while the cursor is on this line. {expect:ANYTHING} + +# Lesson 6.2: THE APPEND COMMAND + +** Type `a`{normal} to insert text AFTER the cursor. ** + + 1. Move the cursor to the start of the line below marked --->. + + 2. Press `e`{normal} until the cursor is on the end of "li". + + 3. Type an `a`{normal} (lowercase) to [append](a) text AFTER the cursor. + + 4. Complete the word like the line below it. Press `<Esc>`{normal} to exit Insert + mode. + + 5. Use `e`{normal} to move to the next incomplete word and repeat steps 3 and 4. + +---> This li will allow you to pract appendi text to a line. {expect:This line will allow you to practice appending text to a line.} +---> This line will allow you to practice appending text to a line. {expect:This line will allow you to practice appending text to a line.} + +NOTE: [a](a), [i](i) and [A](A) all go to the same Insert mode, the only difference is where + the characters are inserted. + +# Lesson 6.3: ANOTHER WAY TO REPLACE + +** Type a capital `R`{normal} to replace more than one character. ** + + 1. Move the cursor to the first line below marked --->. Move the cursor to + the beginning of the first "xxx". + + 2. Now press `R`{normal} ([capital R](R)) and type the number below it in the second line, so that it + replaces the "xxx". + + 3. Press `<Esc>`{normal} to leave [Replace mode](mode-replace). Notice that the rest of the line + remains unmodified. + + 4. Repeat the steps to replace the remaining "xxx". + +---> Adding 123 to xxx gives you xxx. {expect:Adding 123 to 456 gives you 579.} +---> Adding 123 to 456 gives you 579. {expect:Adding 123 to 456 gives you 579.} + +NOTE: Replace mode is like Insert mode, but every typed character deletes an + existing character. + +# Lesson 6.4: COPY AND PASTE TEXT + +** Use the `y`{normal} operator to copy text and `p`{normal} to paste it ** + + 1. Go to the line marked with ---> below and place the cursor after "a)". + + 2. Start Visual mode with `v`{normal} and move the cursor to just before "first". + + 3. Type `y`{normal} to [yank](yank) (copy) the highlighted text. + + 4. Move the cursor to the end of the next line: `j$`{normal} + + 5. Type `p`{normal} to [put](put) (paste) the text. Then type: "a second"`<Esc>`{normal}. + + 6. Use Visual mode to select " item.", yank it with `y`{normal}, move to the end of + the next line with `j$`{normal} and put the text there with `p`{normal}. + +---> a) this is the first item. +---> b) {expect: b) this is the second item} + + NOTE: you can also use `y`{normal} as an operator; `yw`{normal} yanks one word. + +# Lesson 6.5: SET OPTION + +** Set an option so a search or substitute ignores case ** + + 1. Search for 'ignore' by entering: `/ignore` + Repeat several times by pressing `n`{normal}. + + 2. Set the 'ic' (Ignore case) option by entering: +~~~ cmd + :set ic +~~~ + 3. Now search for 'ignore' again by pressing n + Notice that Ignore and IGNORE are now also found. + + 4. Set the 'hlsearch' and 'incsearch' options: +~~~ cmd + :set hls is +~~~ + 5. Now type the search command again and see what happens: /ignore <Enter> + + 6. To disable ignoring case enter: +~~~ cmd + :set noic +~~~ + 7. To toggle the value of a setting, prepend it with "inv": +~~~ cmd + :set invic +~~~ +NOTE: To remove the highlighting of matches enter: +~~~ cmd + :nohlsearch +~~~ +NOTE: If you want to ignore case for just one search command, use [\c](/\c) + in the phrase: /ignore\c <Enter> + +# Lesson 6 SUMMARY + + 1. Type `o`{normal} to open a line BELOW the cursor and start Insert mode. + Type `O`{normal} to open a line ABOVE the cursor. + + 2. Type `a`{normal} to insert text AFTER the cursor. + Type `A`{normal} to insert text after the end of the line. + + 3. The `e`{normal} command moves to the end of a word. + + 4. The `y`{normal} operator yanks (copies) text, `p`{normal} puts (pastes) it. + + 5. Typing a capital `R`{normal} enters Replace mode until `<Esc>`{normal} is pressed. + + 6. Typing "[:set](:set) xxx" sets the option "xxx". Some options are: + + 'ic' 'ignorecase' ignore upper/lower case when searching + 'is' 'incsearch' show partial matches for a search phrase + 'hls' 'hlsearch' highlight all matching phrases + + You can either use the long or the short option name. + + 7. Prepend "no" to switch an option off: +~~~ cmd + :set noic +~~~ + 8. Prepend "inv" to toggle an option: +~~~ cmd + :set invic +~~~ + +# Lesson 7.1: GETTING HELP + +** Use the on-line help system ** + +Vim has a comprehensive on-line help system. To get started, try one of +these three: + - press the `<HELP>`{normal} key (if you have one) + - press the `<F1>`{normal} key (if you have one) + - type + `:help`{vim} + +Read the text in the help window to find out how the help works. +Type `<Ctrl-w><Ctrl-w>`{normal} to jump from one window to another. +Type `:q`{vim} to close the help window. + +You can find help on just about any subject, by giving an argument to the +":help" command. Try these (don't forget pressing <Enter>): +~~~ cmd + :help w + :help c_CTRL-D + :help insert-index + :help user-manual +~~~ +# Lesson 7.2: CREATE A STARTUP SCRIPT + +** Enable Vim features ** + +Vim has many more features than Vi, but most of them are disabled by +default. To start using more features you have to create a "vimrc" file. + + 1. Start editing the "vimrc" file. This depends on your system: + `:e ~/.vimrc`{vim} for Unix-like systems + `:e $VIM/_vimrc`{vim} for Microsoft Windows + + 2. Now read the example "vimrc" file contents: + `:r $VIMRUNTIME/vimrc_example.vim`{vim} + + 3. Write the file with: + `:w`{vim} + + The next time you start Vim it will use syntax highlighting. + You can add all your preferred settings to this "vimrc" file. + For more information type :help vimrc-intro + +# Lesson 7.3: COMPLETION + +** Command line completion with `<Ctrl-d>`{normal} and `<Tab>`{normal} ** + + 1. Look what files exist in the directory: `:!ls`{vim} + + 2. Type the start of a command: `:e`{vim} + + 3. Press `<Ctrl-d>`{normal} and Vim will show a list of commands that start with "e". + + 4. Press `<Tab>`{normal} and Vim will complete the command name to ":edit". + + 5. Now add a space and the start of an existing file name: `:edit FIL`{vim} + + 6. Press `<Tab>`{normal}. Vim will complete the name (if it is unique). + +NOTE: Completion works for many commands. It is especially useful for `:help`{vim}. + +# Lesson 7 SUMMARY + + 1. Type `:help`{vim} + or press `<F1>`{normal} or `<Help>`{normal} to open a help window. + + 2. Type `:help TOPIC`{vim} to find help on TOPIC. + + 3. Type `<Ctrl-w><Ctrl-w>`{normal} to jump to another window + + 4. Type `:q`{vim} to close the help window + + 5. Create a vimrc startup script to keep your preferred settings. + + 6. While in command mode, press `<Ctrl-d>`{normal} to see possible completions. + Press `<Tab>`{normal} to use one completion. + +# CONCLUSION + +This was intended to give a brief overview of the Vim editor, just enough to +allow you to use the editor fairly easily. It is far from complete as Vim has +many many more commands. Consult the help often. + +There are many resources online to learn more about vim. Here's a bunch of them: + +- *Learn Vim Progressively*: http://yannesposito.com/Scratch/en/blog/Learn-Vim-Progressively/ +- *Learning Vim in 2014*: http://benmccormick.org/learning-vim-in-2014/ +- Vimcasts: http://vimcasts.org/ +- Vim Video-Tutorials by Derek Wyatt: http://derekwyatt.org/vim/tutorials/ +- *Learn Vimscript the Hard Way*: http://learnvimscriptthehardway.stevelosh.com/ +- *7 Habits of Effective Text Editing*: http://www.moolenaar.net/habits.html + +If you prefer a book, *Practival Vim* by Drew Neil is recommended often. + +This tutorial was written by Michael C. Pierce and Robert K. Ware, Colorado +School of Mines using ideas supplied by Charles Smith, Colorado State +University. E-mail: bware@mines.colorado.edu. + +Modified for Vim by Bram Moolenaar. +Modified for vim-tutor-mode by Felipe Morales. diff --git a/runtime/tutor/tutor.tutor b/runtime/tutor/tutor.tutor new file mode 100644 index 0000000000..852c1ea74a --- /dev/null +++ b/runtime/tutor/tutor.tutor @@ -0,0 +1,259 @@ +# CREATING A VIM TUTORIAL WITH VIM-TUTOR-MODE + +This tutorial will guide you through the steps required to create a tutorial +file for vim-tutor-mode. It is also meant as a demo of vim-tutor-mode +capabilities. + +Table of contents: + +- [Setting up](*setting-up*) +- [vim-tutor-mode's markup](*markup*) + - [emphasis](*emphasis*) + - [headers](*headers*) + - [links](*links*) + - [codeblocks](*codeblocks*) +- [Interactive elements](*interactive*) + - [expect](*expect*) + +## SETTING UP *setting-up* + +First, you'll need to enable "debug" mode +~~~ cmd + :let g:tutor_debug = 1 +~~~ +This will allow saving changes to the tutor files and will disable conceals, so +you can more easily check your changes. + +After this, create a new .tutor file (we will be practicing on this very file, so you +don't need to do this now): +~~~ cmd + :e new-tutorial.tutor +~~~ + +## VIM-TUTOR-MODE's MARKDOWN *markup* + +vim-tutor-mode uses a subset of markdown's syntax to format the tutorials. The +subset supported should be enough for most tutorials and the maintainers will +try to keep it as small as possible (if regular markdown allows for several +ways to do the same thing, tutor markdown will only provide the one the +maintainers think is easier to handle). + +### Emphasis *emphasis* + +For emphasized text (italics), as in normal markdown, you use \*. E.g.: + + \*text\* + +is displayed like + + *text* + +Note: The underscores variant is not supported. + +For strong emphasis (bold), you use \*\*. E.g.: + + \*\*this\*\* + +is displayed like + + **this** + +1. Format the line below so it becomes a lesson description: + +---> This is text with importat information {expect:This is text with **important information**} +---> This is text with **important information** {expect:This is text with **important information**} + +Note: Some words (e.g., NOTE, IMPORTANT, tip, ATTENTION, etc.) will also be +highlighted. You don't need to mark them specially. + +2. Turn the line below into a TODO item: + +---> Document '&variable' {expect:TODO: Document '&variable'} +---> TODO: Document '&variable' {expect:TODO: Document '&variable'} + +### Headers *headers* + +3. Practice fixing the lines below: + +---> This is a level 1 header {expect:# This is a level 1 header} +---> # This is a level 1 header {expect:# This is a level 1 header} +---> This is a level 3 header {expect:### This is a level 3 header} +---> ### This is a level 3 header {expect:### This is a level 3 header} +---> This is a header with a label {expect:# This is a header with a label {*label*}} +---> # This is a header with a label {*label*} {expect:# This is a header with a label {*label*}} + +4. Now, create a 4th level section here, and add a label like in the previous +exercise: + + + + ATTENTION We will use this label later, so remember it. + +### Links *links* + +It is good practice to include links in your tutorials to reference materials, +like vim's own help or external documents. You can also link to other parts of +the document. + +Links have the syntax + + \[label\]\(target\) + +#### Help links + +If the target of a link matches a help topic, opening it will open it. + +5. Fix the following line: + +---> A link to help for the 'breakindent' option {expect:A link to help for the ['breakindent']('breakindent') option} +---> A link to help for the ['breakindent']('breakindent') option {expect:A link to help for the ['breakindent']('breakindent') option} + +#### Anchor links + +A link can also lead to a place in the file itself. Anchors are written + + \*anchor\* + +and are hidden by default. Links to them look like + + \[label\]\(\*anchor\*\) + +6. Add the appropiate link: + +---> A link to the Links section {expect:A link to the [Links](*links*) section} +---> A link to the [Links](*links*) section {expect:A link to the [Links](*links*) section} + +7. Now, create a link to the section you created on exercise 4 + above. + + + +# Tutorial links + +You can also have links to other tutorials. For this, you'll write the wnchor in the format + + @tutor:TUTORIAL + +7. Create a link to this tutorial: + +---> A link to the vim-tutor-mode tutorial {expect:A link to [the vim-tutor-mode tutorial](@tutor:tutor)} +---> A link to [the vim-tutor-mode tutorial](@tutor:tutor) {expect:A link to [the vim-tutor-mode tutorial](@tutor:tutor)} + +### Codeblocks *codeblocks* + +vim-tutor-mode tutorials can include viml sections + + ~~~ cmd + echom "hello" + ~~~ + +is displayed as +~~~ cmd +echom "hello" +~~~ + +8. Copy the viml section below + +---> {expect:~~~ viml} +---> {expect:echom "the value of &number is".string(&number)} +---> {expect:~~~} + +---> ~~~ viml {expect:~~~ viml} +---> echom "the value of &number is".string(&number) {expect:echom "the value of &number is".string(&number)} +---> ~~~ {expect:~~~} + +You can inline viml code using "\`" and "\`{vim}": + + \`call myFunction()\`{vim} + +is displayed as + + `call myFunction()`{vim} + +[normal](Normal-mode) commands can also be embedded in tutorials. + + ~~~ normal + ftdaW + ~~~ + +is displayed as +~~~ normal +ftdaW +~~~ + +Note: you can also write `norm` or `normal`. + +9. Copy the normal section below + +---> {expect:~~~ normal} +---> {expect:d2w} +---> {expect:~~~} + +---> ~~~ normal {expect:~~~ normal} +---> d2w {expect:d2w} +---> ~~~ {expect:~~~} + +You can also inline normal commands by using "\`" and "\`{normal}": + + \`gq\`{normal} is very useful. + +is displayed: + + `gq`{normal} is very useful. + +10. Complete the line as shown + +---> d {expect:«d2w»} +---> «d2w» {expect:«d2w»} + +Commands to run in the system shell can be highlighted by indenting a line starting with "$". + +~~~ sh + $ vim --version +~~~ + +## INTERACTIVE ELEMENTS *interactive* + +As visible in this very document, vim-tutor-mode includes some interactive +elements, to provide feedback to the user about his progress. These elements +all have the syntax + + \---> TEXT {CLAUSE} + +where \---> must start at the beginning of the line. If TEXT satisfies CLAUSE, +a ✓ sign will appear to the left. A ✗ sign is displayed otherwise. The CLAUSE +itself is hidden unless debug mode is set or ['conceallevel']('conceallevel') +is 2. + +### expect *expect* + +The basic clause is "expect", which is satisfied if TEXT is the same as the +content of the clause. For example + + \---> TEXT {expect:TEXT} + +is satisfied, but + + \---> OTHER TEXT {expect:TEXT} + +is not. + +13. Make both lines the same: + +---> this is not right {expect:---> this is right} |expect:---> this is right {expect:---> this is right}| +---> ---> this is right {expect:---> this is right} |expect:---> this is right {expect:---> this is right}| + + +If the content of a expect clause is ANYTHING, no checks will be performed. This is +useful to create a line that is highlighted you want the user to play with. + + \---> TEXT {expect:ANYTHING} + +is displayed + +---> this is free text {expect:ANYTHING} + +14. Turn the line below into free text: + +---> this is some text |expect:---> this is some text {expect:ANYTHING}| +---> ---> this is some text {expect:ANYTHING} |expect:---> this is some text {expect:ANYTHING}| diff --git a/scripts/msgpack-gen.lua b/scripts/msgpack-gen.lua index bb37ae94da..581641ae06 100644 --- a/scripts/msgpack-gen.lua +++ b/scripts/msgpack-gen.lua @@ -34,8 +34,8 @@ c_params = Ct(c_void + c_param_list) c_proto = Ct( Cg(c_type, 'return_type') * Cg(c_id, 'name') * fill * P('(') * fill * Cg(c_params, 'parameters') * fill * P(')') * - Cg(Cc(false), 'deferred') * - (fill * Cg((P('FUNC_ATTR_DEFERRED') * Cc(true)), 'deferred') ^ -1) * + Cg(Cc(false), 'async') * + (fill * Cg((P('FUNC_ATTR_ASYNC') * Cc(true)), 'async') ^ -1) * fill * P(';') ) grammar = Ct((c_proto + c_comment + c_preproc + ws) ^ 1) @@ -279,7 +279,7 @@ for i = 1, #functions do '(String) {.data = "'..fn.name..'", '.. '.size = sizeof("'..fn.name..'") - 1}, '.. '(MsgpackRpcRequestHandler) {.fn = handle_'.. fn.name.. - ', .defer = '..tostring(fn.deferred)..'});\n') + ', .async = '..tostring(fn.async)..'});\n') if #fn.name > max_fname_len then max_fname_len = #fn.name diff --git a/scripts/vim-patch.sh b/scripts/vim-patch.sh index f17b23101d..9ae9377d8f 100755 --- a/scripts/vim-patch.sh +++ b/scripts/vim-patch.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -e +set -u set -o pipefail readonly NEOVIM_SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -12,21 +13,31 @@ usage() { echo "Helper script for porting Vim patches. For more information, see" echo "https://github.com/neovim/neovim/wiki/Merging-patches-from-upstream-vim" echo - echo "Usage: ${BASENAME} [option]" - echo " ${BASENAME} vim-revision" + echo "Usage: ${BASENAME} [-h | -l | -p vim-revision | -r pr-number]" echo echo "Options:" - echo " -h, --help Show this message." - echo " -l, --list Show list of Vim patches missing from Neovim." - echo - echo "vim-revision can be a version number of the format '7.4.xxx'" - echo "or a Mercurial commit hash." + echo " -h Show this message and exit." + echo " -l Show list of Vim patches missing from Neovim." + echo " -p {vim-revision} Download and apply the Vim patch vim-revision." + echo " vim-revision can be a version number of the " + echo " format '7.4.xxx' or a Mercurial commit hash." + echo " -r {pr-number} Review a vim-patch pull request to Neovim." echo echo "Set VIM_SOURCE_DIR to change where Vim's sources are stored." echo "The default is '${VIM_SOURCE_DIR_DEFAULT}'." } +# Checks if a program is in the user's PATH, and is executable. +check_executable() { + if [[ ! -x $(command -v "${1}") ]]; then + >&2 echo "${BASENAME}: '${1}' not found in PATH or not executable." + exit 1 + fi +} + get_vim_sources() { + check_executable hg + echo "Retrieving Vim sources." if [[ ! -d ${VIM_SOURCE_DIR} ]]; then echo "Cloning Vim sources into '${VIM_SOURCE_DIR}'." @@ -41,20 +52,39 @@ get_vim_sources() { fi } -get_vim_patch() { +commit_message() { + echo "vim-patch:${vim_version} + +${vim_message} + +${vim_commit_url}" +} + +assign_commit_details() { if [[ ${1} =~ [0-9]\.[0-9]\.[0-9]{3,4} ]]; then # Interpret parameter as version number. vim_version="${1}" vim_commit="v${1//./-}" - strip_commit_line=true + local strip_commit_line=true vim_commit_url="https://github.com/vim/vim/commit/${vim_commit}" else # Interpret parameter as commit hash. vim_version="${1:0:7}" vim_commit="${1}" - strip_commit_line=false + local strip_commit_line=false vim_commit_url="https://code.google.com/p/vim/source/detail?r=${vim_commit}" fi + vim_message="$(hg log --template "{desc}" --rev "${vim_commit}")" + if [[ ${strip_commit_line} == "true" ]]; then + # Remove first line of commit message. + vim_message="$(echo "${vim_message}" | sed -e '1d')" + fi +} + +get_vim_patch() { + get_vim_sources + + assign_commit_details "${1}" hg log --rev "${vim_commit}" >/dev/null 2>&1 || { >&2 echo "✘ Couldn't find Vim revision '${vim_commit}'." @@ -65,19 +95,9 @@ get_vim_patch() { # Collect patch details and store into variables. vim_full="$(hg log --patch --git --verbose --rev "${vim_commit}")" - vim_message="$(hg log --template "{desc}" --rev "${vim_commit}")" - if [[ ${strip_commit_line} == "true" ]]; then - # Remove first line of commit message. - vim_message="$(echo "${vim_message}" | sed -e '1d')" - fi vim_diff="$(hg diff --show-function --git --change "${vim_commit}" \ | sed -e 's/\( [ab]\/src\)/\1\/nvim/g')" # Change directory to src/nvim. - neovim_message=" -vim-patch:${vim_version} - -${vim_message} - -${vim_commit_url}" + neovim_message="$(commit_message)" neovim_pr=" \`\`\` ${vim_message} @@ -108,7 +128,7 @@ ${vim_diff} echo echo "Creating files." echo "${vim_diff}" > "${NEOVIM_SOURCE_DIR}/${neovim_branch}.diff" - echo "✔ Saved patch to '${NEOVIM_SOURCE_DIR}/${neovim_branch}.diff'." + echo "✔ Saved diff to '${NEOVIM_SOURCE_DIR}/${neovim_branch}.diff'." echo "${vim_full}" > "${NEOVIM_SOURCE_DIR}/${neovim_branch}.patch" echo "✔ Saved full commit details to '${NEOVIM_SOURCE_DIR}/${neovim_branch}.patch'." echo "${neovim_pr}" > "${NEOVIM_SOURCE_DIR}/${neovim_branch}.pr" @@ -132,6 +152,8 @@ ${vim_diff} } list_vim_patches() { + get_vim_sources + echo echo "Vim patches missing from Neovim:" @@ -184,24 +206,88 @@ list_vim_patches() { echo " '${BASENAME} 1e8ebf870720e7b671f98f22d653009826304c4f'" } -if [[ ${1} == "--help" || ${1} == "-h" ]]; then - usage - exit 0 -elif [[ ${#} != 1 ]]; then - usage - exit 1 -fi - -# Checks if mercurial is in the user's PATH, and is executable. -if [[ ! -x $(command -v hg) ]]; then - >&2 echo "${BASENAME}: 'hg' (mercurial) not found in PATH or not executable" - exit 1 -fi - -get_vim_sources - -if [[ ${1} == "--list" || ${1} == "-l" ]]; then - list_vim_patches -else - get_vim_patch "${1}" -fi +review_pr() { + check_executable curl + check_executable nvim + + get_vim_sources + + local pr="${1}" + echo + echo "Downloading data for pull request #${pr}." + + local git_patch_prefix='Subject: \[PATCH\] ' + local neovim_patch="$(curl -Ssf "https://patch-diff.githubusercontent.com/raw/neovim/neovim/pull/${pr}.patch")" + echo "${neovim_patch}" > a + local vim_version="$(head -n 4 <<< "${neovim_patch}" | sed -n "s/${git_patch_prefix}vim-patch:\([a-z0-9.]*\)$/\1/p")" + + if [[ -n "${vim_version}" ]]; then + echo "✔ Detected Vim patch '${vim_version}'." + else + echo "✘ Could not detect the Vim patch number." + echo " This script assumes that the PR contains a single commit" + echo " with 'vim-patch:XXX' as its title." + exit 1 + fi + + assign_commit_details "${vim_version}" + + local expected_commit_message="$(commit_message)" + local message_length="$(wc -l <<< "${expected_commit_message}")" + local commit_message="$(tail -n +4 <<< "${neovim_patch}" | head -n "${message_length}")" + if [[ "${commit_message#${git_patch_prefix}}" == "${expected_commit_message}" ]]; then + echo "✔ Found expected commit message." + else + echo "✘ Wrong commit message." + echo " Expected:" + echo "${expected_commit_message}" + echo " Actual:" + echo "${commit_message#${git_patch_prefix}}" + exit 1 + fi + + local base_name="vim-${vim_version}" + echo + echo "Creating files." + curl -Ssfo "${NEOVIM_SOURCE_DIR}/n${base_name}.diff" "https://patch-diff.githubusercontent.com/raw/neovim/neovim/pull/${pr}.diff" + echo "✔ Saved pull request diff to '${NEOVIM_SOURCE_DIR}/n${base_name}.diff'." + echo "${neovim_patch}" > "${NEOVIM_SOURCE_DIR}/n${base_name}.patch" + echo "✔ Saved full pull request commit details to '${NEOVIM_SOURCE_DIR}/n${base_name}.patch'." + hg diff --show-function --git --change "${vim_commit}" > "${NEOVIM_SOURCE_DIR}/${base_name}.diff" + echo "✔ Saved Vim diff to '${NEOVIM_SOURCE_DIR}/${base_name}.diff'." + hg log --patch --git --verbose --rev "${vim_commit}" > "${NEOVIM_SOURCE_DIR}/${base_name}.patch" + echo "✔ Saved full Vim commit details to '${NEOVIM_SOURCE_DIR}/${base_name}.patch'." + echo "You can use 'git clean' to remove these files when you're done." + + echo + echo "Launching nvim." + exec nvim -O "${NEOVIM_SOURCE_DIR}/${base_name}.diff" "${NEOVIM_SOURCE_DIR}/n${base_name}.diff" +} + +while getopts "hlp:r:" opt; do + case ${opt} in + h) + usage + exit 0 + ;; + l) + list_vim_patches + exit 0 + ;; + p) + get_vim_patch "${OPTARG}" + exit 0 + ;; + r) + review_pr "${OPTARG}" + exit 0 + ;; + *) + exit 1 + ;; + esac +done + +usage + +# vim: et sw=2 diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index 598f19d19d..6e6a890cd1 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -237,7 +237,7 @@ install_helper(TARGETS nvim) if(CLANG_ASAN_UBSAN) message(STATUS "Enabling Clang address sanitizer and undefined behavior sanitizer for nvim.") set_property(TARGET nvim APPEND_STRING PROPERTY COMPILE_FLAGS "-DEXITFREE ") - set_property(TARGET nvim APPEND_STRING PROPERTY COMPILE_FLAGS "-fno-sanitize-recover -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=address -fsanitize=undefined ") + set_property(TARGET nvim APPEND_STRING PROPERTY COMPILE_FLAGS "-fno-sanitize-recover -fno-omit-frame-pointer -fno-optimize-sibling-calls -fsanitize=address -fsanitize=undefined -fsanitize-blacklist=${PROJECT_SOURCE_DIR}/.asan-blacklist") set_property(TARGET nvim APPEND_STRING PROPERTY LINK_FLAGS "-fsanitize=address -fsanitize=undefined ") elseif(CLANG_MSAN) message(STATUS "Enabling Clang memory sanitizer for nvim.") diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index 915c5f74d7..12c97cfffb 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -70,7 +70,6 @@ String buffer_get_line(Buffer buffer, Integer index, Error *err) /// @param line The new line. /// @param[out] err Details of an error that may have occurred void buffer_set_line(Buffer buffer, Integer index, String line, Error *err) - FUNC_ATTR_DEFERRED { Object l = STRING_OBJ(line); Array array = {.items = &l, .size = 1}; @@ -83,7 +82,6 @@ void buffer_set_line(Buffer buffer, Integer index, String line, Error *err) /// @param index The line index /// @param[out] err Details of an error that may have occurred void buffer_del_line(Buffer buffer, Integer index, Error *err) - FUNC_ATTR_DEFERRED { Array array = ARRAY_DICT_INIT; buffer_set_line_slice(buffer, index, index, true, true, array, err); @@ -171,7 +169,6 @@ void buffer_set_line_slice(Buffer buffer, Boolean include_end, ArrayOf(String) replacement, Error *err) - FUNC_ATTR_DEFERRED { buf_T *buf = find_buffer_by_handle(buffer, err); @@ -339,7 +336,6 @@ Object buffer_get_var(Buffer buffer, String name, Error *err) /// @param[out] err Details of an error that may have occurred /// @return The old value Object buffer_set_var(Buffer buffer, String name, Object value, Error *err) - FUNC_ATTR_DEFERRED { buf_T *buf = find_buffer_by_handle(buffer, err); @@ -375,7 +371,6 @@ Object buffer_get_option(Buffer buffer, String name, Error *err) /// @param value The option value /// @param[out] err Details of an error that may have occurred void buffer_set_option(Buffer buffer, String name, Object value, Error *err) - FUNC_ATTR_DEFERRED { buf_T *buf = find_buffer_by_handle(buffer, err); @@ -426,7 +421,6 @@ String buffer_get_name(Buffer buffer, Error *err) /// @param name The buffer name /// @param[out] err Details of an error that may have occurred void buffer_set_name(Buffer buffer, String name, Error *err) - FUNC_ATTR_DEFERRED { buf_T *buf = find_buffer_by_handle(buffer, err); @@ -472,7 +466,6 @@ void buffer_insert(Buffer buffer, Integer lnum, ArrayOf(String) lines, Error *err) - FUNC_ATTR_DEFERRED { buffer_set_line_slice(buffer, lnum, lnum, false, true, lines, err); } diff --git a/src/nvim/api/tabpage.c b/src/nvim/api/tabpage.c index 1c958118e1..126ee4072d 100644 --- a/src/nvim/api/tabpage.c +++ b/src/nvim/api/tabpage.c @@ -62,7 +62,6 @@ Object tabpage_get_var(Tabpage tabpage, String name, Error *err) /// @param[out] err Details of an error that may have occurred /// @return The tab page handle Object tabpage_set_var(Tabpage tabpage, String name, Object value, Error *err) - FUNC_ATTR_DEFERRED { tabpage_T *tab = find_tab_by_handle(tabpage, err); diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index f5dadb00ea..b9900b5d5a 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -37,7 +37,6 @@ /// @param str The command str /// @param[out] err Details of an error that may have occurred void vim_command(String str, Error *err) - FUNC_ATTR_DEFERRED { // Run the command try_start(); @@ -54,7 +53,6 @@ void vim_command(String str, Error *err) /// @see feedkeys() /// @see vim_strsave_escape_csi void vim_feedkeys(String keys, String mode, Boolean escape_csi) - FUNC_ATTR_DEFERRED { bool remap = true; bool insert = false; @@ -100,6 +98,7 @@ void vim_feedkeys(String keys, String mode, Boolean escape_csi) /// @return The number of bytes actually written, which can be lower than /// requested if the buffer becomes full. Integer vim_input(String keys) + FUNC_ATTR_ASYNC { return (Integer)input_enqueue(keys); } @@ -143,7 +142,6 @@ String vim_command_output(String str, Error *err) /// @param[out] err Details of an error that may have occurred /// @return The expanded object Object vim_eval(String str, Error *err) - FUNC_ATTR_DEFERRED { Object rv = OBJECT_INIT; // Evaluate the expression @@ -171,7 +169,6 @@ Object vim_eval(String str, Error *err) /// @param[out] err Details of an error that may have occurred /// @return Result of the function call Object vim_call_function(String fname, Array args, Error *err) - FUNC_ATTR_DEFERRED { Object rv = OBJECT_INIT; if (args.size > MAX_FUNC_ARGS) { @@ -312,7 +309,6 @@ String vim_get_current_line(Error *err) /// @param line The line contents /// @param[out] err Details of an error that may have occurred void vim_set_current_line(String line, Error *err) - FUNC_ATTR_DEFERRED { buffer_set_line(curbuf->handle, curwin->w_cursor.lnum - 1, line, err); } @@ -321,7 +317,6 @@ void vim_set_current_line(String line, Error *err) /// /// @param[out] err Details of an error that may have occurred void vim_del_current_line(Error *err) - FUNC_ATTR_DEFERRED { buffer_del_line(curbuf->handle, curwin->w_cursor.lnum - 1, err); } @@ -343,7 +338,6 @@ Object vim_get_var(String name, Error *err) /// @param[out] err Details of an error that may have occurred /// @return the old value if any Object vim_set_var(String name, Object value, Error *err) - FUNC_ATTR_DEFERRED { return dict_set_value(&globvardict, name, value, err); } @@ -374,7 +368,6 @@ Object vim_get_option(String name, Error *err) /// @param value The new option value /// @param[out] err Details of an error that may have occurred void vim_set_option(String name, Object value, Error *err) - FUNC_ATTR_DEFERRED { set_option_to(NULL, SREQ_GLOBAL, name, value, err); } @@ -383,7 +376,6 @@ void vim_set_option(String name, Object value, Error *err) /// /// @param str The message void vim_out_write(String str) - FUNC_ATTR_DEFERRED { write_msg(str, false); } @@ -392,7 +384,6 @@ void vim_out_write(String str) /// /// @param str The message void vim_err_write(String str) - FUNC_ATTR_DEFERRED { write_msg(str, true); } @@ -402,7 +393,6 @@ void vim_err_write(String str) /// /// @param str The message void vim_report_error(String str) - FUNC_ATTR_DEFERRED { vim_err_write(str); vim_err_write((String) {.data = "\n", .size = 1}); @@ -442,7 +432,6 @@ Buffer vim_get_current_buffer(void) /// @param id The buffer handle /// @param[out] err Details of an error that may have occurred void vim_set_current_buffer(Buffer buffer, Error *err) - FUNC_ATTR_DEFERRED { buf_T *buf = find_buffer_by_handle(buffer, err); @@ -493,7 +482,6 @@ Window vim_get_current_window(void) /// /// @param handle The window handle void vim_set_current_window(Window window, Error *err) - FUNC_ATTR_DEFERRED { win_T *win = find_window_by_handle(window, err); @@ -545,7 +533,6 @@ Tabpage vim_get_current_tabpage(void) /// @param handle The tab page handle /// @param[out] err Details of an error that may have occurred void vim_set_current_tabpage(Tabpage tabpage, Error *err) - FUNC_ATTR_DEFERRED { tabpage_T *tp = find_tab_by_handle(tabpage, err); @@ -609,6 +596,7 @@ Dictionary vim_get_color_map(void) Array vim_get_api_info(uint64_t channel_id) + FUNC_ATTR_ASYNC { Array rv = ARRAY_DICT_INIT; diff --git a/src/nvim/api/window.c b/src/nvim/api/window.c index 5034c26c83..aad616c7bf 100644 --- a/src/nvim/api/window.c +++ b/src/nvim/api/window.c @@ -54,7 +54,6 @@ ArrayOf(Integer, 2) window_get_cursor(Window window, Error *err) /// @param pos the (row, col) tuple representing the new position /// @param[out] err Details of an error that may have occurred void window_set_cursor(Window window, ArrayOf(Integer, 2) pos, Error *err) - FUNC_ATTR_DEFERRED { win_T *win = find_window_by_handle(window, err); @@ -118,7 +117,6 @@ Integer window_get_height(Window window, Error *err) /// @param height the new height in rows /// @param[out] err Details of an error that may have occurred void window_set_height(Window window, Integer height, Error *err) - FUNC_ATTR_DEFERRED { win_T *win = find_window_by_handle(window, err); @@ -162,7 +160,6 @@ Integer window_get_width(Window window, Error *err) /// @param width the new width in columns /// @param[out] err Details of an error that may have occurred void window_set_width(Window window, Integer width, Error *err) - FUNC_ATTR_DEFERRED { win_T *win = find_window_by_handle(window, err); @@ -208,7 +205,6 @@ Object window_get_var(Window window, String name, Error *err) /// @param[out] err Details of an error that may have occurred /// @return The old value Object window_set_var(Window window, String name, Object value, Error *err) - FUNC_ATTR_DEFERRED { win_T *win = find_window_by_handle(window, err); @@ -244,7 +240,6 @@ Object window_get_option(Window window, String name, Error *err) /// @param value The option value /// @param[out] err Details of an error that may have occurred void window_set_option(Window window, String name, Object value, Error *err) - FUNC_ATTR_DEFERRED { win_T *win = find_window_by_handle(window, err); diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 390d62210b..6bcf5e804a 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -254,7 +254,7 @@ edit ( ) { if (curbuf->terminal) { - terminal_enter(true); + terminal_enter(); return false; } @@ -601,15 +601,15 @@ edit ( * Get a character for Insert mode. Ignore K_IGNORE. */ lastc = c; /* remember previous char for CTRL-D */ - loop_enable_deferred_events(&loop); + input_enable_events(); do { c = safe_vgetc(); } while (c == K_IGNORE); - loop_disable_deferred_events(&loop); + input_disable_events(); if (c == K_EVENT) { c = lastc; - loop_process_event(&loop); + queue_process_events(loop.events); continue; } diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 18188696b9..bf9a219e28 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -20,6 +20,7 @@ #include <stdbool.h> #include <math.h> #include <limits.h> +#include <msgpack.h> #include "nvim/assert.h" #include "nvim/vim.h" @@ -90,11 +91,13 @@ #include "nvim/os/time.h" #include "nvim/msgpack_rpc/channel.h" #include "nvim/msgpack_rpc/server.h" +#include "nvim/msgpack_rpc/helpers.h" #include "nvim/api/private/helpers.h" #include "nvim/api/vim.h" #include "nvim/os/dl.h" #include "nvim/os/input.h" #include "nvim/event/loop.h" +#include "nvim/lib/kvec.h" #define DICT_MAXNEST 100 /* maximum nesting of lists and dicts */ @@ -160,6 +163,13 @@ typedef struct lval_S { char_u *ll_newkey; /* New key for Dict in alloc. mem or NULL. */ } lval_T; +/// Structure defining state for read_from_list() +typedef struct { + const listitem_T *li; ///< Item currently read. + size_t offset; ///< Byte offset inside the read item. + size_t li_length; ///< Length of the string inside the read item. +} ListReaderState; + static char *e_letunexp = N_("E18: Unexpected characters in :let"); static char *e_listidx = N_("E684: list index out of range: %" PRId64); @@ -429,6 +439,7 @@ static struct vimvar { {VV_NAME("progpath", VAR_STRING), VV_RO}, {VV_NAME("command_output", VAR_STRING), 0}, {VV_NAME("completed_item", VAR_DICT), VV_RO}, + {VV_NAME("msgpack_types", VAR_DICT), VV_RO}, }; /* shorthand */ @@ -457,17 +468,32 @@ typedef struct { dict_T *self; int *status_ptr; uint64_t id; + Queue *events; } TerminalJobData; +/// Structure representing current VimL to messagepack conversion state +typedef struct { + enum { + kMPConvDict, ///< Convert dict_T *dictionary. + kMPConvList, ///< Convert list_T *list. + kMPConvPairs, ///< Convert mapping represented as a list_T* of pairs. + } type; + union { + struct { + const dict_T *dict; ///< Currently converted dictionary. + const hashitem_T *hi; ///< Currently converted dictionary item. + size_t todo; ///< Amount of items left to process. + } d; ///< State of dictionary conversion. + struct { + const list_T *list; ///< Currently converted list. + const listitem_T *li; ///< Currently converted list item. + } l; ///< State of list or generic mapping conversion. + } data; ///< Data to convert. +} MPConvStackVal; + +/// Stack used to convert VimL values to messagepack. +typedef kvec_t(MPConvStackVal) MPConvStack; -#ifdef INCLUDE_GENERATED_DECLARATIONS -# include "eval.c.generated.h" -#endif - -#define FNE_INCL_BR 1 /* find_name_end(): include [] in name */ -#define FNE_CHECK_START 2 /* find_name_end(): check name starts with - valid character */ -// Memory pool for reusing JobEvent structures typedef struct { TerminalJobData *data; ufunc_T *callback; @@ -475,10 +501,51 @@ typedef struct { list_T *received; int status; } JobEvent; -static int disable_job_defer = 0; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "eval.c.generated.h" +#endif + +#define FNE_INCL_BR 1 /* find_name_end(): include [] in name */ +#define FNE_CHECK_START 2 /* find_name_end(): check name starts with + valid character */ static uint64_t current_job_id = 1; static PMap(uint64_t) *jobs = NULL; +typedef enum { + kMPNil, + kMPBoolean, + kMPInteger, + kMPFloat, + kMPString, + kMPBinary, + kMPArray, + kMPMap, + kMPExt, +} MessagePackType; +static const char *const msgpack_type_names[] = { + [kMPNil] = "nil", + [kMPBoolean] = "boolean", + [kMPInteger] = "integer", + [kMPFloat] = "float", + [kMPString] = "string", + [kMPBinary] = "binary", + [kMPArray] = "array", + [kMPMap] = "map", + [kMPExt] = "ext", +}; +static const list_T *msgpack_type_lists[] = { + [kMPNil] = NULL, + [kMPBoolean] = NULL, + [kMPInteger] = NULL, + [kMPFloat] = NULL, + [kMPString] = NULL, + [kMPBinary] = NULL, + [kMPArray] = NULL, + [kMPMap] = NULL, + [kMPExt] = NULL, +}; + /* * Initialize the global and v: variables. */ @@ -511,6 +578,27 @@ void eval_init(void) /* add to compat scope dict */ hash_add(&compat_hashtab, p->vv_di.di_key); } + + dict_T *const msgpack_types_dict = dict_alloc(); + for (size_t i = 0; i < ARRAY_SIZE(msgpack_type_names); i++) { + list_T *const type_list = list_alloc(); + type_list->lv_lock = VAR_FIXED; + type_list->lv_refcount = 1; + dictitem_T *const di = dictitem_alloc((char_u *) msgpack_type_names[i]); + di->di_flags = DI_FLAGS_RO | DI_FLAGS_FIX; + di->di_tv = (typval_T) { + .v_type = VAR_LIST, + .vval = { .v_list = type_list, }, + }; + msgpack_type_lists[i] = type_list; + if (dict_add(msgpack_types_dict, di) == FAIL) { + // There must not be duplicate items in this dictionary by definition. + assert(false); + } + } + msgpack_types_dict->dv_lock = VAR_FIXED; + + set_vim_var_dict(VV_MSGPACK_TYPES, msgpack_types_dict); set_vim_var_dict(VV_COMPLETED_ITEM, dict_alloc()); set_vim_var_nr(VV_SEARCHFORWARD, 1L); set_vim_var_nr(VV_HLSEARCH, 1L); @@ -5156,24 +5244,40 @@ void list_append_dict(list_T *list, dict_T *dict) ++dict->dv_refcount; } -/* - * Make a copy of "str" and append it as an item to list "l". - * When "len" >= 0 use "str[len]". - */ -void list_append_string(list_T *l, char_u *str, int len) +/// Make a copy of "str" and append it as an item to list "l" +/// +/// @param[out] l List to append to. +/// @param[in] str String to append. +/// @param[in] len Length of the appended string. May be negative, in this +/// case string is considered to be usual zero-terminated +/// string. +void list_append_string(list_T *l, const char_u *str, int len) + FUNC_ATTR_NONNULL_ARG(1) +{ + if (str == NULL) { + list_append_allocated_string(l, NULL); + } else { + list_append_allocated_string(l, (len >= 0 + ? xmemdupz((char *) str, len) + : xstrdup((char *) str))); + } +} + +/// Append given string to the list +/// +/// Unlike list_append_string this function does not copy the string. +/// +/// @param[out] l List to append to. +/// @param[in] str String to append. +void list_append_allocated_string(list_T *l, char *const str) + FUNC_ATTR_NONNULL_ARG(1) { listitem_T *li = listitem_alloc(); list_append(l, li); li->li_tv.v_type = VAR_STRING; li->li_tv.v_lock = 0; - - if (str == NULL) { - li->li_tv.vval.v_string = NULL; - } else { - li->li_tv.vval.v_string = (len >= 0) ? vim_strnsave(str, len) - : vim_strsave(str); - } + li->li_tv.vval.v_string = (char_u *) str; } /* @@ -6582,6 +6686,8 @@ static struct fst { {"min", 1, 1, f_min}, {"mkdir", 1, 3, f_mkdir}, {"mode", 0, 1, f_mode}, + {"msgpackdump", 1, 1, f_msgpackdump}, + {"msgpackparse", 1, 1, f_msgpackparse}, {"nextnonblank", 1, 1, f_nextnonblank}, {"nr2char", 1, 2, f_nr2char}, {"or", 2, 2, f_or}, @@ -8456,27 +8562,12 @@ static void f_feedkeys(typval_T *argvars, typval_T *rettv) } } -/* - * "filereadable()" function - */ +/// "filereadable()" function static void f_filereadable(typval_T *argvars, typval_T *rettv) { - int fd; - char_u *p; - int n; - -#ifndef O_NONBLOCK -# define O_NONBLOCK 0 -#endif - p = get_tv_string(&argvars[0]); - if (*p && !os_isdir(p) && (fd = os_open((char *)p, - O_RDONLY | O_NONBLOCK, 0)) >= 0) { - n = TRUE; - close(fd); - } else - n = FALSE; - - rettv->vval.v_number = n; + char_u *p = get_tv_string(&argvars[0]); + rettv->vval.v_number = + (*p && !os_isdir(p) && os_file_is_readable((char*)p)); } /* @@ -10671,7 +10762,7 @@ static void f_jobclose(typval_T *argvars, typval_T *rettv) return; } - TerminalJobData *data = pmap_get(uint64_t)(jobs, argvars[0].vval.v_number); + TerminalJobData *data = find_job(argvars[0].vval.v_number); if (!data) { EMSG(_(e_invjob)); return; @@ -10712,7 +10803,7 @@ static void f_jobsend(typval_T *argvars, typval_T *rettv) return; } - TerminalJobData *data = pmap_get(uint64_t)(jobs, argvars[0].vval.v_number); + TerminalJobData *data = find_job(argvars[0].vval.v_number); if (!data) { EMSG(_(e_invjob)); return; @@ -10753,7 +10844,7 @@ static void f_jobresize(typval_T *argvars, typval_T *rettv) } - TerminalJobData *data = pmap_get(uint64_t)(jobs, argvars[0].vval.v_number); + TerminalJobData *data = find_job(argvars[0].vval.v_number); if (!data) { EMSG(_(e_invjob)); return; @@ -10900,8 +10991,8 @@ static void f_jobstop(typval_T *argvars, typval_T *rettv) } - TerminalJobData *data = pmap_get(uint64_t)(jobs, argvars[0].vval.v_number); - if (!data || data->stopped) { + TerminalJobData *data = find_job(argvars[0].vval.v_number); + if (!data) { EMSG(_(e_invjob)); return; } @@ -10931,28 +11022,24 @@ static void f_jobwait(typval_T *argvars, typval_T *rettv) list_T *rv = list_alloc(); ui_busy_start(); - // disable breakchecks, which could result in job callbacks being executed - // at unexpected places - disable_breakcheck++; - // disable job event deferring so the callbacks are processed while waiting. - if (!disable_job_defer++) { - // process any pending job events in the deferred queue, but only do this if - // deferred is not disabled(at the top-level `jobwait()` call) - loop_process_event(&loop); - } + Queue *waiting_jobs = queue_new_parent(loop_on_put, &loop); // For each item in the input list append an integer to the output list. -3 // is used to represent an invalid job id, -2 is for a interrupted job and // -1 for jobs that were skipped or timed out. for (listitem_T *arg = args->lv_first; arg != NULL; arg = arg->li_next) { TerminalJobData *data = NULL; if (arg->li_tv.v_type != VAR_NUMBER - || !(data = pmap_get(uint64_t)(jobs, arg->li_tv.vval.v_number))) { + || !(data = find_job(arg->li_tv.vval.v_number))) { list_append_number(rv, -3); } else { // append the list item and set the status pointer so we'll collect the // status code when the job exits list_append_number(rv, -1); data->status_ptr = &rv->lv_last->li_tv.vval.v_number; + // Process any pending events for the job because we'll temporarily + // replace the parent queue + queue_process_events(data->events); + queue_replace_parent(data->events, waiting_jobs); } } @@ -10970,10 +11057,10 @@ static void f_jobwait(typval_T *argvars, typval_T *rettv) break; } if (arg->li_tv.v_type != VAR_NUMBER - || !(data = pmap_get(uint64_t)(jobs, arg->li_tv.vval.v_number))) { + || !(data = find_job(arg->li_tv.vval.v_number))) { continue; } - int status = process_wait((Process *)&data->proc, remaining); + int status = process_wait((Process *)&data->proc, remaining, waiting_jobs); if (status < 0) { // interrupted or timed out, skip remaining jobs. if (status == -2) { @@ -10993,23 +11080,31 @@ static void f_jobwait(typval_T *argvars, typval_T *rettv) } } - // poll to ensure any pending callbacks from the last job are invoked - loop_poll_events(&loop, 0); - for (listitem_T *arg = args->lv_first; arg != NULL; arg = arg->li_next) { TerminalJobData *data = NULL; if (arg->li_tv.v_type != VAR_NUMBER - || !(data = pmap_get(uint64_t)(jobs, arg->li_tv.vval.v_number))) { + || !(data = find_job(arg->li_tv.vval.v_number))) { continue; } // remove the status pointer because the list may be freed before the // job exits data->status_ptr = NULL; } - disable_job_defer--; - disable_breakcheck--; - ui_busy_stop(); + // restore the parent queue for any jobs still alive + for (listitem_T *arg = args->lv_first; arg != NULL; arg = arg->li_next) { + TerminalJobData *data = NULL; + if (arg->li_tv.v_type != VAR_NUMBER + || !(data = pmap_get(uint64_t)(jobs, arg->li_tv.vval.v_number))) { + continue; + } + // restore the parent queue for the job + queue_process_events(data->events); + queue_replace_parent(data->events, loop.events); + } + + queue_free(waiting_jobs); + ui_busy_stop(); rv->lv_refcount++; rettv->v_type = VAR_LIST; rettv->vval.v_list = rv; @@ -11782,6 +11877,827 @@ static void f_mode(typval_T *argvars, typval_T *rettv) rettv->v_type = VAR_STRING; } +/// Msgpack callback for writing to readfile()-style list +static int msgpack_list_write(void *data, const char *buf, size_t len) +{ + if (len == 0) { + return 0; + } + list_T *const list = (list_T *) data; + const char *const end = buf + len; + const char *line_end = buf; + if (list->lv_last == NULL) { + list_append_string(list, NULL, 0); + } + listitem_T *li = list->lv_last; + do { + const char *line_start = line_end; + line_end = xmemscan(line_start, NL, end - line_start); + if (line_end == line_start) { + list_append_allocated_string(list, NULL); + } else { + const size_t line_length = line_end - line_start; + char *str; + if (li == NULL) { + str = xmemdupz(line_start, line_length); + } else { + const size_t li_len = (li->li_tv.vval.v_string == NULL + ? 0 + : STRLEN(li->li_tv.vval.v_string)); + li->li_tv.vval.v_string = xrealloc(li->li_tv.vval.v_string, + li_len + line_length + 1); + str = (char *) li->li_tv.vval.v_string + li_len; + memmove(str, line_start, line_length); + str[line_length] = 0; + } + for (size_t i = 0; i < line_length; i++) { + if (str[i] == NUL) { + str[i] = NL; + } + } + if (li == NULL) { + list_append_allocated_string(list, str); + } else { + li = NULL; + } + if (line_end == end - 1) { + list_append_allocated_string(list, NULL); + } + } + line_end++; + } while (line_end < end); + return 0; +} + +/// Convert readfile()-style list to a char * buffer with length +/// +/// @param[in] list Converted list. +/// @param[out] ret_len Resulting buffer length. +/// @param[out] ret_buf Allocated buffer with the result or NULL if ret_len is +/// zero. +/// +/// @return true in case of success, false in case of failure. +static inline bool vim_list_to_buf(const list_T *const list, + size_t *const ret_len, char **const ret_buf) + FUNC_ATTR_NONNULL_ARG(2,3) FUNC_ATTR_WARN_UNUSED_RESULT +{ + size_t len = 0; + if (list != NULL) { + for (const listitem_T *li = list->lv_first; + li != NULL; + li = li->li_next) { + if (li->li_tv.v_type != VAR_STRING) { + return false; + } + len++; + if (li->li_tv.vval.v_string != 0) { + len += STRLEN(li->li_tv.vval.v_string); + } + } + if (len) { + len--; + } + } + *ret_len = len; + if (len == 0) { + *ret_buf = NULL; + return true; + } + ListReaderState lrstate = init_lrstate(list); + char *const buf = xmalloc(len); + size_t read_bytes; + if (read_from_list(&lrstate, buf, len, &read_bytes) != OK) { + assert(false); + } + assert(len == read_bytes); + *ret_buf = buf; + return true; +} + +/// Convert one VimL value to msgpack +/// +/// @param packer Messagepack packer. +/// @param[out] mpstack Stack with values to convert. Only used for pushing +/// values to it. +/// @param[in] tv Converted value. +/// +/// @return OK in case of success, FAIL otherwise. +static int convert_one_value(msgpack_packer *const packer, + MPConvStack *const mpstack, + const typval_T *const tv) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + switch (tv->v_type) { +#define CHECK_SELF_REFERENCE(conv_type, vval_name, ptr) \ + do { \ + for (size_t i = 0; i < kv_size(*mpstack); i++) { \ + if (kv_A(*mpstack, i).type == conv_type \ + && kv_A(*mpstack, i).data.vval_name == ptr) { \ + EMSG2(_(e_invarg2), "container references itself"); \ + return FAIL; \ + } \ + } \ + } while (0) + case VAR_STRING: { + if (tv->vval.v_string == NULL) { + msgpack_pack_bin(packer, 0); + } else { + const size_t len = STRLEN(tv->vval.v_string); + msgpack_pack_bin(packer, len); + msgpack_pack_bin_body(packer, tv->vval.v_string, len); + } + break; + } + case VAR_NUMBER: { + msgpack_pack_int64(packer, (int64_t) tv->vval.v_number); + break; + } + case VAR_FLOAT: { + msgpack_pack_double(packer, (double) tv->vval.v_float); + break; + } + case VAR_FUNC: { + EMSG2(_(e_invarg2), "attempt to dump function reference"); + return FAIL; + } + case VAR_LIST: { + if (tv->vval.v_list == NULL) { + msgpack_pack_array(packer, 0); + break; + } + CHECK_SELF_REFERENCE(kMPConvList, l.list, tv->vval.v_list); + msgpack_pack_array(packer, tv->vval.v_list->lv_len); + kv_push(MPConvStackVal, *mpstack, ((MPConvStackVal) { + .type = kMPConvList, + .data = { + .l = { + .list = tv->vval.v_list, + .li = tv->vval.v_list->lv_first, + }, + }, + })); + break; + } + case VAR_DICT: { + if (tv->vval.v_dict == NULL) { + msgpack_pack_map(packer, 0); + break; + } + const dictitem_T *type_di; + const dictitem_T *val_di; + if (tv->vval.v_dict->dv_hashtab.ht_used == 2 + && (type_di = dict_find((dict_T *) tv->vval.v_dict, + (char_u *) "_TYPE", -1)) != NULL + && type_di->di_tv.v_type == VAR_LIST + && (val_di = dict_find((dict_T *) tv->vval.v_dict, + (char_u *) "_VAL", -1)) != NULL) { + size_t i; + for (i = 0; i < ARRAY_SIZE(msgpack_type_lists); i++) { + if (type_di->di_tv.vval.v_list == msgpack_type_lists[i]) { + break; + } + } + if (i == ARRAY_SIZE(msgpack_type_lists)) { + goto vim_to_msgpack_regural_dict; + } + switch ((MessagePackType) i) { + case kMPNil: { + msgpack_pack_nil(packer); + break; + } + case kMPBoolean: { + if (val_di->di_tv.v_type != VAR_NUMBER) { + goto vim_to_msgpack_regural_dict; + } + if (val_di->di_tv.vval.v_number) { + msgpack_pack_true(packer); + } else { + msgpack_pack_false(packer); + } + break; + } + case kMPInteger: { + const list_T *val_list; + varnumber_T sign; + varnumber_T highest_bits; + varnumber_T high_bits; + varnumber_T low_bits; + // List of 4 integers; first is signed (should be 1 or -1, but this + // is not checked), second is unsigned and have at most one (sign is + // -1) or two (sign is 1) non-zero bits (number of bits is not + // checked), other unsigned and have at most 31 non-zero bits + // (number of bits is not checked). + if (val_di->di_tv.v_type != VAR_LIST + || (val_list = val_di->di_tv.vval.v_list) == NULL + || val_list->lv_len != 4 + || val_list->lv_first->li_tv.v_type != VAR_NUMBER + || (sign = val_list->lv_first->li_tv.vval.v_number) == 0 + || val_list->lv_first->li_next->li_tv.v_type != VAR_NUMBER + || (highest_bits = + val_list->lv_first->li_next->li_tv.vval.v_number) < 0 + || val_list->lv_last->li_prev->li_tv.v_type != VAR_NUMBER + || (high_bits = + val_list->lv_last->li_prev->li_tv.vval.v_number) < 0 + || val_list->lv_last->li_tv.v_type != VAR_NUMBER + || (low_bits = val_list->lv_last->li_tv.vval.v_number) < 0) { + goto vim_to_msgpack_regural_dict; + } + uint64_t number = ((uint64_t) (((uint64_t) highest_bits) << 62) + | (uint64_t) (((uint64_t) high_bits) << 31) + | (uint64_t) low_bits); + if (sign > 0) { + msgpack_pack_uint64(packer, number); + } else { + msgpack_pack_int64(packer, (int64_t) (-number)); + } + break; + } + case kMPFloat: { + if (val_di->di_tv.v_type != VAR_FLOAT) { + goto vim_to_msgpack_regural_dict; + } + msgpack_pack_double(packer, val_di->di_tv.vval.v_float); + break; + } + case kMPString: + case kMPBinary: { + const bool is_string = ((MessagePackType) i == kMPString); + if (val_di->di_tv.v_type != VAR_LIST) { + goto vim_to_msgpack_regural_dict; + } + size_t len; + char *buf; + if (!vim_list_to_buf(val_di->di_tv.vval.v_list, &len, &buf)) { + goto vim_to_msgpack_regural_dict; + } + if (is_string) { + msgpack_pack_str(packer, len); + } else { + msgpack_pack_bin(packer, len); + } + if (len == 0) { + break; + } + if (is_string) { + msgpack_pack_str_body(packer, buf, len); + } else { + msgpack_pack_bin_body(packer, buf, len); + } + xfree(buf); + break; + } + case kMPArray: { + if (val_di->di_tv.v_type != VAR_LIST) { + goto vim_to_msgpack_regural_dict; + } + CHECK_SELF_REFERENCE(kMPConvList, l.list, + val_di->di_tv.vval.v_list); + msgpack_pack_array(packer, val_di->di_tv.vval.v_list->lv_len); + kv_push(MPConvStackVal, *mpstack, ((MPConvStackVal) { + .type = kMPConvList, + .data = { + .l = { + .list = val_di->di_tv.vval.v_list, + .li = val_di->di_tv.vval.v_list->lv_first, + }, + }, + })); + break; + } + case kMPMap: { + if (val_di->di_tv.v_type != VAR_LIST) { + goto vim_to_msgpack_regural_dict; + } + if (val_di->di_tv.vval.v_list == NULL) { + msgpack_pack_map(packer, 0); + break; + } + const list_T *const val_list = val_di->di_tv.vval.v_list; + for (const listitem_T *li = val_list->lv_first; li != NULL; + li = li->li_next) { + if (li->li_tv.v_type != VAR_LIST + || li->li_tv.vval.v_list->lv_len != 2) { + goto vim_to_msgpack_regural_dict; + } + } + CHECK_SELF_REFERENCE(kMPConvPairs, l.list, val_list); + msgpack_pack_map(packer, val_list->lv_len); + kv_push(MPConvStackVal, *mpstack, ((MPConvStackVal) { + .type = kMPConvPairs, + .data = { + .l = { + .list = val_list, + .li = val_list->lv_first, + }, + }, + })); + break; + } + case kMPExt: { + const list_T *val_list; + varnumber_T type; + if (val_di->di_tv.v_type != VAR_LIST + || (val_list = val_di->di_tv.vval.v_list) == NULL + || val_list->lv_len != 2 + || (val_list->lv_first->li_tv.v_type != VAR_NUMBER) + || (type = val_list->lv_first->li_tv.vval.v_number) > INT8_MAX + || type < INT8_MIN + || (val_list->lv_last->li_tv.v_type != VAR_LIST)) { + goto vim_to_msgpack_regural_dict; + } + size_t len; + char *buf; + if (!vim_list_to_buf(val_list->lv_last->li_tv.vval.v_list, + &len, &buf)) { + goto vim_to_msgpack_regural_dict; + } + msgpack_pack_ext(packer, len, (int8_t) type); + msgpack_pack_ext_body(packer, buf, len); + xfree(buf); + break; + } + } + break; + } +vim_to_msgpack_regural_dict: + CHECK_SELF_REFERENCE(kMPConvDict, d.dict, tv->vval.v_dict); + msgpack_pack_map(packer, tv->vval.v_dict->dv_hashtab.ht_used); + kv_push(MPConvStackVal, *mpstack, ((MPConvStackVal) { + .type = kMPConvDict, + .data = { + .d = { + .dict = tv->vval.v_dict, + .hi = tv->vval.v_dict->dv_hashtab.ht_array, + .todo = tv->vval.v_dict->dv_hashtab.ht_used, + }, + }, + })); + break; + } + } +#undef CHECK_SELF_REFERENCE + return OK; +} + +/// Convert typval_T to messagepack +static int vim_to_msgpack(msgpack_packer *const packer, + const typval_T *const tv) + FUNC_ATTR_WARN_UNUSED_RESULT +{ + MPConvStack mpstack; + kv_init(mpstack); + if (convert_one_value(packer, &mpstack, tv) == FAIL) { + goto vim_to_msgpack_error_ret; + } + while (kv_size(mpstack)) { + MPConvStackVal *cur_mpsv = &kv_A(mpstack, kv_size(mpstack) - 1); + const typval_T *cur_tv = NULL; + switch (cur_mpsv->type) { + case kMPConvDict: { + if (!cur_mpsv->data.d.todo) { + (void) kv_pop(mpstack); + continue; + } + while (HASHITEM_EMPTY(cur_mpsv->data.d.hi)) { + cur_mpsv->data.d.hi++; + } + const dictitem_T *const di = HI2DI(cur_mpsv->data.d.hi); + cur_mpsv->data.d.todo--; + cur_mpsv->data.d.hi++; + const size_t key_len = STRLEN(&di->di_key[0]); + msgpack_pack_str(packer, key_len); + msgpack_pack_str_body(packer, &di->di_key[0], key_len); + cur_tv = &di->di_tv; + break; + } + case kMPConvList: { + if (cur_mpsv->data.l.li == NULL) { + (void) kv_pop(mpstack); + continue; + } + cur_tv = &cur_mpsv->data.l.li->li_tv; + cur_mpsv->data.l.li = cur_mpsv->data.l.li->li_next; + break; + } + case kMPConvPairs: { + if (cur_mpsv->data.l.li == NULL) { + (void) kv_pop(mpstack); + continue; + } + const list_T *const kv_pair = cur_mpsv->data.l.li->li_tv.vval.v_list; + if (convert_one_value(packer, &mpstack, &kv_pair->lv_first->li_tv) + == FAIL) { + goto vim_to_msgpack_error_ret; + } + cur_tv = &kv_pair->lv_last->li_tv; + cur_mpsv->data.l.li = cur_mpsv->data.l.li->li_next; + break; + } + } + if (convert_one_value(packer, &mpstack, cur_tv) == FAIL) { + goto vim_to_msgpack_error_ret; + } + } + kv_destroy(mpstack); + return OK; +vim_to_msgpack_error_ret: + kv_destroy(mpstack); + return FAIL; +} + +/// "msgpackdump()" function +static void f_msgpackdump(typval_T *argvars, typval_T *rettv) + FUNC_ATTR_NONNULL_ALL +{ + if (argvars[0].v_type != VAR_LIST) { + EMSG2(_(e_listarg), "msgpackdump()"); + return; + } + list_T *ret_list = rettv_list_alloc(rettv); + const list_T *list = argvars[0].vval.v_list; + if (list == NULL) { + return; + } + msgpack_packer *lpacker = msgpack_packer_new(ret_list, &msgpack_list_write); + for (const listitem_T *li = list->lv_first; li != NULL; li = li->li_next) { + if (vim_to_msgpack(lpacker, &li->li_tv) == FAIL) { + break; + } + } + msgpack_packer_free(lpacker); +} + +/// Read bytes from list +/// +/// @param[in,out] state Structure describing position in list from which +/// reading should start. Is updated to reflect position +/// at which reading ended. +/// @param[out] buf Buffer to write to. +/// @param[in] nbuf Buffer length. +/// @param[out] read_bytes Is set to amount of bytes read. +/// +/// @return OK when reading was finished, FAIL in case of error (i.e. list item +/// was not a string), NOTDONE if reading was successfull, but there are +/// more bytes to read. +static int read_from_list(ListReaderState *const state, char *const buf, + const size_t nbuf, size_t *const read_bytes) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + char *const buf_end = buf + nbuf; + char *p = buf; + while (p < buf_end) { + for (size_t i = state->offset; i < state->li_length && p < buf_end; i++) { + const char ch = state->li->li_tv.vval.v_string[state->offset++]; + *p++ = (ch == NL ? NUL : ch); + } + if (p < buf_end) { + state->li = state->li->li_next; + if (state->li == NULL) { + *read_bytes = (size_t) (p - buf); + return OK; + } + *p++ = NL; + if (state->li->li_tv.v_type != VAR_STRING) { + *read_bytes = (size_t) (p - buf); + return FAIL; + } + state->offset = 0; + state->li_length = (state->li->li_tv.vval.v_string == NULL + ? 0 + : STRLEN(state->li->li_tv.vval.v_string)); + } + } + *read_bytes = nbuf; + return (state->offset < state->li_length || state->li->li_next != NULL + ? NOTDONE + : OK); +} + +/// Initialize ListReaderState structure +static inline ListReaderState init_lrstate(const list_T *const list) + FUNC_ATTR_NONNULL_ALL +{ + return (ListReaderState) { + .li = list->lv_first, + .offset = 0, + .li_length = (list->lv_first->li_tv.vval.v_string == NULL + ? 0 + : STRLEN(list->lv_first->li_tv.vval.v_string)), + }; +} + +/// Convert msgpack object to a VimL one +static int msgpack_to_vim(const msgpack_object mobj, typval_T *const rettv) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ +#define INIT_SPECIAL_DICT(tv, type, val) \ + do { \ + dict_T *const dict = dict_alloc(); \ + dictitem_T *const type_di = dictitem_alloc((char_u *) "_TYPE"); \ + type_di->di_tv.v_type = VAR_LIST; \ + type_di->di_tv.v_lock = 0; \ + type_di->di_tv.vval.v_list = (list_T *) msgpack_type_lists[type]; \ + type_di->di_tv.vval.v_list->lv_refcount++; \ + dict_add(dict, type_di); \ + dictitem_T *const val_di = dictitem_alloc((char_u *) "_VAL"); \ + val_di->di_tv = val; \ + dict_add(dict, val_di); \ + tv->v_type = VAR_DICT; \ + dict->dv_refcount++; \ + tv->vval.v_dict = dict; \ + } while (0) + switch (mobj.type) { + case MSGPACK_OBJECT_NIL: { + INIT_SPECIAL_DICT(rettv, kMPNil, ((typval_T) { + .v_type = VAR_NUMBER, + .v_lock = 0, + .vval = { .v_number = 0 }, + })); + break; + } + case MSGPACK_OBJECT_BOOLEAN: { + INIT_SPECIAL_DICT(rettv, kMPBoolean, + ((typval_T) { + .v_type = VAR_NUMBER, + .v_lock = 0, + .vval = { + .v_number = (varnumber_T) mobj.via.boolean, + }, + })); + break; + } + case MSGPACK_OBJECT_POSITIVE_INTEGER: { + if (mobj.via.u64 <= VARNUMBER_MAX) { + *rettv = (typval_T) { + .v_type = VAR_NUMBER, + .v_lock = 0, + .vval = { .v_number = (varnumber_T) mobj.via.u64 }, + }; + } else { + list_T *const list = list_alloc(); + list->lv_refcount++; + INIT_SPECIAL_DICT(rettv, kMPInteger, + ((typval_T) { + .v_type = VAR_LIST, + .v_lock = 0, + .vval = { .v_list = list }, + })); + uint64_t n = mobj.via.u64; + list_append_number(list, 1); + list_append_number(list, (varnumber_T) ((n >> 62) & 0x3)); + list_append_number(list, (varnumber_T) ((n >> 31) & 0x7FFFFFFF)); + list_append_number(list, (varnumber_T) (n & 0x7FFFFFFF)); + } + break; + } + case MSGPACK_OBJECT_NEGATIVE_INTEGER: { + if (mobj.via.i64 >= VARNUMBER_MIN) { + *rettv = (typval_T) { + .v_type = VAR_NUMBER, + .v_lock = 0, + .vval = { .v_number = (varnumber_T) mobj.via.i64 }, + }; + } else { + list_T *const list = list_alloc(); + list->lv_refcount++; + INIT_SPECIAL_DICT(rettv, kMPInteger, + ((typval_T) { + .v_type = VAR_LIST, + .v_lock = 0, + .vval = { .v_list = list }, + })); + uint64_t n = -((uint64_t) mobj.via.i64); + list_append_number(list, -1); + list_append_number(list, (varnumber_T) ((n >> 62) & 0x3)); + list_append_number(list, (varnumber_T) ((n >> 31) & 0x7FFFFFFF)); + list_append_number(list, (varnumber_T) (n & 0x7FFFFFFF)); + } + break; + } + case MSGPACK_OBJECT_FLOAT: { + *rettv = (typval_T) { + .v_type = VAR_FLOAT, + .v_lock = 0, + .vval = { .v_float = mobj.via.f64 }, + }; + break; + } + case MSGPACK_OBJECT_STR: { + list_T *const list = list_alloc(); + list->lv_refcount++; + INIT_SPECIAL_DICT(rettv, kMPString, + ((typval_T) { + .v_type = VAR_LIST, + .v_lock = 0, + .vval = { .v_list = list }, + })); + if (msgpack_list_write((void *) list, mobj.via.str.ptr, mobj.via.str.size) + == -1) { + return FAIL; + } + break; + } + case MSGPACK_OBJECT_BIN: { + if (memchr(mobj.via.bin.ptr, NUL, mobj.via.bin.size) == NULL) { + *rettv = (typval_T) { + .v_type = VAR_STRING, + .v_lock = 0, + .vval = { .v_string = xmemdupz(mobj.via.bin.ptr, mobj.via.bin.size) }, + }; + break; + } + list_T *const list = list_alloc(); + list->lv_refcount++; + INIT_SPECIAL_DICT(rettv, kMPBinary, + ((typval_T) { + .v_type = VAR_LIST, + .v_lock = 0, + .vval = { .v_list = list }, + })); + if (msgpack_list_write((void *) list, mobj.via.bin.ptr, mobj.via.bin.size) + == -1) { + return FAIL; + } + break; + } + case MSGPACK_OBJECT_ARRAY: { + list_T *const list = list_alloc(); + list->lv_refcount++; + *rettv = (typval_T) { + .v_type = VAR_LIST, + .v_lock = 0, + .vval = { .v_list = list }, + }; + for (size_t i = 0; i < mobj.via.array.size; i++) { + listitem_T *const li = listitem_alloc(); + li->li_tv.v_type = VAR_UNKNOWN; + list_append(list, li); + if (msgpack_to_vim(mobj.via.array.ptr[i], &li->li_tv) == FAIL) { + return FAIL; + } + } + break; + } + case MSGPACK_OBJECT_MAP: { + for (size_t i = 0; i < mobj.via.map.size; i++) { + if (mobj.via.map.ptr[i].key.type != MSGPACK_OBJECT_STR + || mobj.via.map.ptr[i].key.via.str.size == 0 + || memchr(mobj.via.map.ptr[i].key.via.str.ptr, NUL, + mobj.via.map.ptr[i].key.via.str.size) != NULL) { + goto msgpack_to_vim_generic_map; + } + } + dict_T *const dict = dict_alloc(); + dict->dv_refcount++; + *rettv = (typval_T) { + .v_type = VAR_DICT, + .v_lock = 0, + .vval = { .v_dict = dict }, + }; + for (size_t i = 0; i < mobj.via.map.size; i++) { + dictitem_T *const di = xmallocz(offsetof(dictitem_T, di_key) + + mobj.via.map.ptr[i].key.via.str.size); + memcpy(&di->di_key[0], mobj.via.map.ptr[i].key.via.str.ptr, + mobj.via.map.ptr[i].key.via.str.size); + di->di_tv.v_type = VAR_UNKNOWN; + if (dict_add(dict, di) == FAIL) { + // Duplicate key: fallback to generic map + clear_tv(rettv); + xfree(di); + goto msgpack_to_vim_generic_map; + } + if (msgpack_to_vim(mobj.via.map.ptr[i].val, &di->di_tv) == FAIL) { + return FAIL; + } + } + break; +msgpack_to_vim_generic_map: {} + list_T *const list = list_alloc(); + list->lv_refcount++; + INIT_SPECIAL_DICT(rettv, kMPMap, + ((typval_T) { + .v_type = VAR_LIST, + .v_lock = 0, + .vval = { .v_list = list }, + })); + for (size_t i = 0; i < mobj.via.map.size; i++) { + list_T *const kv_pair = list_alloc(); + list_append_list(list, kv_pair); + listitem_T *const key_li = listitem_alloc(); + key_li->li_tv.v_type = VAR_UNKNOWN; + list_append(kv_pair, key_li); + listitem_T *const val_li = listitem_alloc(); + val_li->li_tv.v_type = VAR_UNKNOWN; + list_append(kv_pair, val_li); + if (msgpack_to_vim(mobj.via.map.ptr[i].key, &key_li->li_tv) == FAIL) { + return FAIL; + } + if (msgpack_to_vim(mobj.via.map.ptr[i].val, &val_li->li_tv) == FAIL) { + return FAIL; + } + } + break; + } + case MSGPACK_OBJECT_EXT: { + list_T *const list = list_alloc(); + list->lv_refcount++; + list_append_number(list, mobj.via.ext.type); + list_T *const ext_val_list = list_alloc(); + list_append_list(list, ext_val_list); + INIT_SPECIAL_DICT(rettv, kMPExt, + ((typval_T) { + .v_type = VAR_LIST, + .v_lock = 0, + .vval = { .v_list = list }, + })); + if (msgpack_list_write((void *) ext_val_list, mobj.via.ext.ptr, + mobj.via.ext.size) == -1) { + return FAIL; + } + break; + } + } +#undef INIT_SPECIAL_DICT + return OK; +} + +/// "msgpackparse" function +static void f_msgpackparse(typval_T *argvars, typval_T *rettv) + FUNC_ATTR_NONNULL_ALL +{ + if (argvars[0].v_type != VAR_LIST) { + EMSG2(_(e_listarg), "msgpackparse()"); + } + list_T *ret_list = rettv_list_alloc(rettv); + const list_T *list = argvars[0].vval.v_list; + if (list == NULL || list->lv_first == NULL) { + return; + } + if (list->lv_first->li_tv.v_type != VAR_STRING) { + EMSG2(_(e_invarg2), "List item is not a string"); + return; + } + ListReaderState lrstate = init_lrstate(list); + msgpack_unpacker *const unpacker = msgpack_unpacker_new(IOSIZE); + if (unpacker == NULL) { + EMSG(_(e_outofmem)); + return; + } + msgpack_unpacked unpacked; + msgpack_unpacked_init(&unpacked); + do { + if (!msgpack_unpacker_reserve_buffer(unpacker, IOSIZE)) { + EMSG(_(e_outofmem)); + goto f_msgpackparse_exit; + } + size_t read_bytes; + const int rlret = read_from_list( + &lrstate, msgpack_unpacker_buffer(unpacker), IOSIZE, &read_bytes); + if (rlret == FAIL) { + EMSG2(_(e_invarg2), "List item is not a string"); + goto f_msgpackparse_exit; + } + msgpack_unpacker_buffer_consumed(unpacker, read_bytes); + if (read_bytes == 0) { + break; + } + while (unpacker->off < unpacker->used) { + const msgpack_unpack_return result = msgpack_unpacker_next(unpacker, + &unpacked); + if (result == MSGPACK_UNPACK_PARSE_ERROR) { + EMSG2(_(e_invarg2), "Failed to parse msgpack string"); + goto f_msgpackparse_exit; + } + if (result == MSGPACK_UNPACK_NOMEM_ERROR) { + EMSG(_(e_outofmem)); + goto f_msgpackparse_exit; + } + if (result == MSGPACK_UNPACK_SUCCESS) { + listitem_T *li = listitem_alloc(); + li->li_tv.v_type = VAR_UNKNOWN; + list_append(ret_list, li); + if (msgpack_to_vim(unpacked.data, &li->li_tv) == FAIL) { + EMSG2(_(e_invarg2), "Failed to convert msgpack string"); + goto f_msgpackparse_exit; + } + } + if (result == MSGPACK_UNPACK_CONTINUE) { + if (rlret == OK) { + EMSG2(_(e_invarg2), "Incomplete msgpack string"); + } + break; + } + } + if (rlret == OK) { + break; + } + } while (true); + +f_msgpackparse_exit: + msgpack_unpacked_destroy(&unpacked); + msgpack_unpacker_free(unpacker); + return; +} /* * "nextnonblank()" function @@ -20125,17 +21041,21 @@ static inline TerminalJobData *common_job_init(char **argv, ufunc_T *on_stdout, data->on_stderr = on_stderr; data->on_exit = on_exit; data->self = self; + data->events = queue_new_child(loop.events); if (pty) { - data->proc.pty = pty_process_init(data); + data->proc.pty = pty_process_init(&loop, data); } else { - data->proc.uv = uv_process_init(data); + data->proc.uv = uv_process_init(&loop, data); } Process *proc = (Process *)&data->proc; proc->argv = argv; proc->in = &data->in; proc->out = &data->out; - proc->err = &data->err; + if (!pty) { + proc->err = &data->err; + } proc->cb = on_process_exit; + proc->events = data->events; return data; } @@ -20166,8 +21086,12 @@ static inline bool common_job_start(TerminalJobData *data, typval_T *rettv) { data->refcount++; Process *proc = (Process *)&data->proc; - if (!process_spawn(&loop, proc)) { + if (!process_spawn(proc)) { EMSG(_(e_jobexe)); + if (proc->type == kProcessTypePty) { + xfree(data->proc.pty.term_name); + free_term_job_data(data); + } return false; } @@ -20186,7 +21110,9 @@ static inline bool common_job_start(TerminalJobData *data, typval_T *rettv) return true; } -static inline void free_term_job_data(TerminalJobData *data) { +static inline void free_term_job_data_event(void **argv) +{ + TerminalJobData *data = argv[0]; if (data->on_stdout) { user_func_unref(data->on_stdout); } @@ -20201,17 +21127,25 @@ static inline void free_term_job_data(TerminalJobData *data) { data->self->internal_refcount--; dict_unref(data->self); } + queue_free(data->events); xfree(data); } +static inline void free_term_job_data(TerminalJobData *data) +{ + // data->queue may still be used after this function returns(process_wait), so + // only free in the next event loop iteration + queue_put(loop.fast_events, free_term_job_data_event, 1, data); +} + // vimscript job callbacks must be executed on Nvim main loop -static inline void push_job_event(TerminalJobData *data, ufunc_T *callback, +static inline void process_job_event(TerminalJobData *data, ufunc_T *callback, const char *type, char *buf, size_t count, int status) { - JobEvent *event_data = xmalloc(sizeof(JobEvent)); - event_data->received = NULL; + JobEvent event_data; + event_data.received = NULL; if (buf) { - event_data->received = list_alloc(); + event_data.received = list_alloc(); char *ptr = buf; size_t remaining = count; size_t off = 0; @@ -20219,7 +21153,7 @@ static inline void push_job_event(TerminalJobData *data, ufunc_T *callback, while (off < remaining) { // append the line if (ptr[off] == NL) { - list_append_string(event_data->received, (uint8_t *)ptr, off); + list_append_string(event_data.received, (uint8_t *)ptr, off); size_t skip = off + 1; ptr += skip; remaining -= skip; @@ -20232,51 +21166,53 @@ static inline void push_job_event(TerminalJobData *data, ufunc_T *callback, } off++; } - list_append_string(event_data->received, (uint8_t *)ptr, off); + list_append_string(event_data.received, (uint8_t *)ptr, off); } else { - event_data->status = status; + event_data.status = status; } - event_data->data = data; - event_data->callback = callback; - event_data->type = type; - loop_push_event(&loop, (Event) { - .handler = on_job_event, - .data = event_data - }, !disable_job_defer); + event_data.data = data; + event_data.callback = callback; + event_data.type = type; + on_job_event(&event_data); } -static void on_job_stdout(Stream *stream, RBuffer *buf, void *job, bool eof) +static void on_job_stdout(Stream *stream, RBuffer *buf, size_t count, + void *job, bool eof) { TerminalJobData *data = job; - on_job_output(stream, job, buf, eof, data->on_stdout, "stdout"); + on_job_output(stream, job, buf, count, eof, data->on_stdout, "stdout"); } -static void on_job_stderr(Stream *stream, RBuffer *buf, void *job, bool eof) +static void on_job_stderr(Stream *stream, RBuffer *buf, size_t count, + void *job, bool eof) { TerminalJobData *data = job; - on_job_output(stream, job, buf, eof, data->on_stderr, "stderr"); + on_job_output(stream, job, buf, count, eof, data->on_stderr, "stderr"); } static void on_job_output(Stream *stream, TerminalJobData *data, RBuffer *buf, - bool eof, ufunc_T *callback, const char *type) + size_t count, bool eof, ufunc_T *callback, const char *type) { if (eof) { return; } - RBUFFER_UNTIL_EMPTY(buf, ptr, len) { - // The order here matters, the terminal must receive the data first because - // push_job_event will modify the read buffer(convert NULs into NLs) - if (data->term) { - terminal_receive(data->term, ptr, len); - } + // stub variable, to keep reading consistent with the order of events, only + // consider the count parameter. + size_t r; + char *ptr = rbuffer_read_ptr(buf, &r); - if (callback) { - push_job_event(data, callback, type, ptr, len, 0); - } + // The order here matters, the terminal must receive the data first because + // process_job_event will modify the read buffer(convert NULs into NLs) + if (data->term) { + terminal_receive(data->term, ptr, count); + } - rbuffer_consumed(buf, len); + if (callback) { + process_job_event(data, callback, type, ptr, count, 0); } + + rbuffer_consumed(buf, count); } static void on_process_exit(Process *proc, int status, void *d) @@ -20292,7 +21228,7 @@ static void on_process_exit(Process *proc, int status, void *d) *data->status_ptr = status; } - push_job_event(data, data->on_exit, "exit", NULL, 0, status); + process_job_event(data, data->on_exit, "exit", NULL, 0, status); } static void term_write(char *buf, size_t size, void *d) @@ -20326,10 +21262,8 @@ static void term_job_data_decref(TerminalJobData *data) } } -static void on_job_event(Event event) +static void on_job_event(JobEvent *ev) { - JobEvent *ev = event.data; - if (!ev->callback) { goto end; } @@ -20374,7 +21308,15 @@ end: pmap_del(uint64_t)(jobs, ev->data->id); term_job_data_decref(ev->data); } - xfree(ev); +} + +static TerminalJobData *find_job(uint64_t id) +{ + TerminalJobData *data = pmap_get(uint64_t)(jobs, id); + if (!data || data->stopped) { + return NULL; + } + return data; } static void script_host_eval(char *name, typval_T *argvars, typval_T *rettv) diff --git a/src/nvim/eval.h b/src/nvim/eval.h index bf48bc74b8..8f065eda33 100644 --- a/src/nvim/eval.h +++ b/src/nvim/eval.h @@ -65,6 +65,7 @@ enum { VV_PROGPATH, VV_COMMAND_OUTPUT, VV_COMPLETED_ITEM, + VV_MSGPACK_TYPES, VV_LEN, /* number of v: vars */ }; diff --git a/src/nvim/eval_defs.h b/src/nvim/eval_defs.h index 34a36004d6..0faf860588 100644 --- a/src/nvim/eval_defs.h +++ b/src/nvim/eval_defs.h @@ -1,11 +1,16 @@ #ifndef NVIM_EVAL_DEFS_H #define NVIM_EVAL_DEFS_H +#include <limits.h> + #include "nvim/hashtab.h" typedef int varnumber_T; typedef double float_T; +#define VARNUMBER_MAX INT_MAX +#define VARNUMBER_MIN INT_MIN + typedef struct listvar_S list_T; typedef struct dictvar_S dict_T; diff --git a/src/nvim/event/defs.h b/src/nvim/event/defs.h new file mode 100644 index 0000000000..5126d52241 --- /dev/null +++ b/src/nvim/event/defs.h @@ -0,0 +1,39 @@ +#ifndef NVIM_EVENT_DEFS_H +#define NVIM_EVENT_DEFS_H + +#include <assert.h> +#include <stdarg.h> + +#define EVENT_HANDLER_MAX_ARGC 4 + +typedef void (*argv_callback)(void **argv); +typedef struct message { + int priority; + argv_callback handler; + void *argv[EVENT_HANDLER_MAX_ARGC]; +} Event; + +#define VA_EVENT_INIT(event, p, h, a) \ + do { \ + assert(a <= EVENT_HANDLER_MAX_ARGC); \ + (event)->priority = p; \ + (event)->handler = h; \ + if (a) { \ + va_list args; \ + va_start(args, a); \ + for (int i = 0; i < a; i++) { \ + (event)->argv[i] = va_arg(args, void *); \ + } \ + va_end(args); \ + } \ + } while (0) + +static inline Event event_create(int priority, argv_callback cb, int argc, ...) +{ + assert(argc <= EVENT_HANDLER_MAX_ARGC); + Event event; + VA_EVENT_INIT(&event, priority, cb, argc); + return event; +} + +#endif // NVIM_EVENT_DEFS_H diff --git a/src/nvim/event/loop.c b/src/nvim/event/loop.c index d90565002e..3d3288f858 100644 --- a/src/nvim/event/loop.c +++ b/src/nvim/event/loop.c @@ -1,3 +1,4 @@ +#include <stdarg.h> #include <stdint.h> #include <uv.h> @@ -9,17 +10,23 @@ # include "event/loop.c.generated.h" #endif +typedef struct idle_event { + uv_idle_t idle; + Event event; +} IdleEvent; + void loop_init(Loop *loop, void *data) { uv_loop_init(&loop->uv); loop->uv.data = loop; - loop->deferred_events = kl_init(Event); - loop->immediate_events = kl_init(Event); loop->children = kl_init(WatcherPtr); loop->children_stop_requests = 0; + loop->events = queue_new_parent(loop_on_put, loop); + loop->fast_events = queue_new_child(loop->events); uv_signal_init(&loop->uv, &loop->children_watcher); uv_timer_init(&loop->uv, &loop->children_kill_timer); + uv_timer_init(&loop->uv, &loop->poll_timer); } void loop_poll_events(Loop *loop, int ms) @@ -30,89 +37,36 @@ void loop_poll_events(Loop *loop, int ms) abort(); // Should not re-enter uv_run } - bool wait = true; - uv_timer_t timer; + uv_run_mode mode = UV_RUN_ONCE; if (ms > 0) { - uv_timer_init(&loop->uv, &timer); // Use a repeating timeout of ms milliseconds to make sure // we do not block indefinitely for I/O. - uv_timer_start(&timer, timer_cb, (uint64_t)ms, (uint64_t)ms); + uv_timer_start(&loop->poll_timer, timer_cb, (uint64_t)ms, (uint64_t)ms); } else if (ms == 0) { // For ms == 0, we need to do a non-blocking event poll by // setting the run mode to UV_RUN_NOWAIT. - wait = false; + mode = UV_RUN_NOWAIT; } - if (wait) { - loop_run_once(loop); - } else { - loop_run_nowait(loop); - } + uv_run(&loop->uv, mode); if (ms > 0) { - // Ensure the timer handle is closed and run the event loop - // once more to let libuv perform it's cleanup - uv_timer_stop(&timer); - uv_close((uv_handle_t *)&timer, NULL); - loop_run_nowait(loop); + uv_timer_stop(&loop->poll_timer); } recursive--; // Can re-enter uv_run now - process_events_from(loop->immediate_events); -} - -bool loop_has_deferred_events(Loop *loop) -{ - return loop->deferred_events_allowed && !kl_empty(loop->deferred_events); + queue_process_events(loop->fast_events); } -void loop_enable_deferred_events(Loop *loop) -{ - ++loop->deferred_events_allowed; -} - -void loop_disable_deferred_events(Loop *loop) -{ - --loop->deferred_events_allowed; -} - -// Queue an event -void loop_push_event(Loop *loop, Event event, bool deferred) +void loop_on_put(Queue *queue, void *data) { + Loop *loop = data; // Sometimes libuv will run pending callbacks(timer for example) before // blocking for a poll. If this happens and the callback pushes a event to one // of the queues, the event would only be processed after the poll // returns(user hits a key for example). To avoid this scenario, we call // uv_stop when a event is enqueued. - loop_stop(loop); - kl_push(Event, deferred ? loop->deferred_events : loop->immediate_events, - event); -} - -void loop_process_event(Loop *loop) -{ - process_events_from(loop->deferred_events); -} - - -void loop_run(Loop *loop) -{ - uv_run(&loop->uv, UV_RUN_DEFAULT); -} - -void loop_run_once(Loop *loop) -{ - uv_run(&loop->uv, UV_RUN_ONCE); -} - -void loop_run_nowait(Loop *loop) -{ - uv_run(&loop->uv, UV_RUN_NOWAIT); -} - -void loop_stop(Loop *loop) -{ uv_stop(&loop->uv); } @@ -120,25 +74,12 @@ void loop_close(Loop *loop) { uv_close((uv_handle_t *)&loop->children_watcher, NULL); uv_close((uv_handle_t *)&loop->children_kill_timer, NULL); + uv_close((uv_handle_t *)&loop->poll_timer, NULL); do { uv_run(&loop->uv, UV_RUN_DEFAULT); } while (uv_loop_close(&loop->uv)); } -void loop_process_all_events(Loop *loop) -{ - process_events_from(loop->immediate_events); - process_events_from(loop->deferred_events); -} - -static void process_events_from(klist_t(Event) *queue) -{ - while (!kl_empty(queue)) { - Event event = kl_shift(Event, queue); - event.handler(event); - } -} - static void timer_cb(uv_timer_t *handle) { } diff --git a/src/nvim/event/loop.h b/src/nvim/event/loop.h index 5eb4d32ca8..9212a45aa4 100644 --- a/src/nvim/event/loop.h +++ b/src/nvim/event/loop.h @@ -7,38 +7,39 @@ #include "nvim/lib/klist.h" #include "nvim/os/time.h" - -typedef struct event Event; -typedef void (*event_handler)(Event event); - -struct event { - void *data; - event_handler handler; -}; +#include "nvim/event/queue.h" typedef void * WatcherPtr; #define _noop(x) KLIST_INIT(WatcherPtr, WatcherPtr, _noop) -KLIST_INIT(Event, Event, _noop) typedef struct loop { uv_loop_t uv; - klist_t(Event) *deferred_events, *immediate_events; - int deferred_events_allowed; + Queue *events, *fast_events; klist_t(WatcherPtr) *children; uv_signal_t children_watcher; - uv_timer_t children_kill_timer; + uv_timer_t children_kill_timer, poll_timer; size_t children_stop_requests; } Loop; +#define CREATE_EVENT(queue, handler, argc, ...) \ + do { \ + if (queue) { \ + queue_put((queue), (handler), argc, __VA_ARGS__); \ + } else { \ + void *argv[argc] = {__VA_ARGS__}; \ + (handler)(argv); \ + } \ + } while (0) + // Poll for events until a condition or timeout -#define LOOP_POLL_EVENTS_UNTIL(loop, timeout, condition) \ +#define LOOP_PROCESS_EVENTS_UNTIL(loop, queue, timeout, condition) \ do { \ int remaining = timeout; \ uint64_t before = (remaining > 0) ? os_hrtime() : 0; \ while (!(condition)) { \ - loop_poll_events(loop, remaining); \ + LOOP_PROCESS_EVENTS(loop, queue, remaining); \ if (remaining == 0) { \ break; \ } else if (remaining > 0) { \ @@ -52,6 +53,16 @@ typedef struct loop { } \ } while (0) +#define LOOP_PROCESS_EVENTS(loop, queue, timeout) \ + do { \ + if (queue && !queue_empty(queue)) { \ + queue_process_events(queue); \ + } else { \ + loop_poll_events(loop, timeout); \ + } \ + } while (0) + + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "event/loop.h.generated.h" #endif diff --git a/src/nvim/event/process.c b/src/nvim/event/process.c index 2b1f1ae096..81d4e690c3 100644 --- a/src/nvim/event/process.c +++ b/src/nvim/event/process.c @@ -22,7 +22,7 @@ #define TERM_TIMEOUT 1000000000 #define KILL_TIMEOUT (TERM_TIMEOUT * 2) -#define CLOSE_PROC_STREAM(proc, stream) \ +#define CLOSE_PROC_STREAM(proc, stream) \ do { \ if (proc->stream && !proc->stream->closed) { \ stream_close(proc->stream, NULL); \ @@ -30,19 +30,18 @@ } while (0) -bool process_spawn(Loop *loop, Process *proc) FUNC_ATTR_NONNULL_ALL +bool process_spawn(Process *proc) FUNC_ATTR_NONNULL_ALL { - proc->loop = loop; if (proc->in) { - uv_pipe_init(&loop->uv, &proc->in->uv.pipe, 0); + uv_pipe_init(&proc->loop->uv, &proc->in->uv.pipe, 0); } if (proc->out) { - uv_pipe_init(&loop->uv, &proc->out->uv.pipe, 0); + uv_pipe_init(&proc->loop->uv, &proc->out->uv.pipe, 0); } if (proc->err) { - uv_pipe_init(&loop->uv, &proc->err->uv.pipe, 0); + uv_pipe_init(&proc->loop->uv, &proc->err->uv.pipe, 0); } bool success; @@ -77,6 +76,7 @@ bool process_spawn(Loop *loop, Process *proc) FUNC_ATTR_NONNULL_ALL if (proc->in) { stream_init(NULL, proc->in, -1, (uv_stream_t *)&proc->in->uv.pipe, data); + proc->in->events = proc->events; proc->in->internal_data = proc; proc->in->internal_close_cb = on_process_stream_close; proc->refcount++; @@ -84,6 +84,7 @@ bool process_spawn(Loop *loop, Process *proc) FUNC_ATTR_NONNULL_ALL if (proc->out) { stream_init(NULL, proc->out, -1, (uv_stream_t *)&proc->out->uv.pipe, data); + proc->out->events = proc->events; proc->out->internal_data = proc; proc->out->internal_close_cb = on_process_stream_close; proc->refcount++; @@ -91,6 +92,7 @@ bool process_spawn(Loop *loop, Process *proc) FUNC_ATTR_NONNULL_ALL if (proc->err) { stream_init(NULL, proc->err, -1, (uv_stream_t *)&proc->err->uv.pipe, data); + proc->err->events = proc->events; proc->err->internal_data = proc; proc->err->internal_close_cb = on_process_stream_close; proc->refcount++; @@ -99,7 +101,7 @@ bool process_spawn(Loop *loop, Process *proc) FUNC_ATTR_NONNULL_ALL proc->internal_exit_cb = on_process_exit; proc->internal_close_cb = decref; proc->refcount++; - kl_push(WatcherPtr, loop->children, proc); + kl_push(WatcherPtr, proc->loop->children, proc); return true; } @@ -113,7 +115,7 @@ void process_teardown(Loop *loop) FUNC_ATTR_NONNULL_ALL } // Wait until all children exit - LOOP_POLL_EVENTS_UNTIL(loop, -1, kl_empty(loop->children)); + LOOP_PROCESS_EVENTS_UNTIL(loop, loop->events, -1, kl_empty(loop->children)); pty_process_teardown(loop); } @@ -150,16 +152,24 @@ void process_close_err(Process *proc) FUNC_ATTR_NONNULL_ALL /// indistinguishable from the process returning -1 by itself. Which /// is possible on some OS. Returns -2 if an user has interruped the /// wait. -int process_wait(Process *proc, int ms) FUNC_ATTR_NONNULL_ALL +int process_wait(Process *proc, int ms, Queue *events) FUNC_ATTR_NONNULL_ARG(1) { // The default status is -1, which represents a timeout int status = -1; bool interrupted = false; + if (!proc->refcount) { + LOOP_PROCESS_EVENTS(proc->loop, proc->events, 0); + return proc->status; + } + + if (!events) { + events = proc->events; + } // Increase refcount to stop the exit callback from being called(and possibly // being freed) before we have a chance to get the status. proc->refcount++; - LOOP_POLL_EVENTS_UNTIL(proc->loop, ms, + LOOP_PROCESS_EVENTS_UNTIL(proc->loop, events, ms, // Until... got_int || // interrupted by the user proc->refcount == 1); // job exited @@ -171,12 +181,12 @@ int process_wait(Process *proc, int ms) FUNC_ATTR_NONNULL_ALL got_int = false; process_stop(proc); if (ms == -1) { - // We can only return, if all streams/handles are closed and the job - + // We can only return if all streams/handles are closed and the job // exited. - LOOP_POLL_EVENTS_UNTIL(proc->loop, -1, proc->refcount == 1); + LOOP_PROCESS_EVENTS_UNTIL(proc->loop, events, -1, + proc->refcount == 1); } else { - loop_poll_events(proc->loop, 0); + LOOP_PROCESS_EVENTS(proc->loop, events, 0); } } @@ -185,6 +195,10 @@ int process_wait(Process *proc, int ms) FUNC_ATTR_NONNULL_ALL // resources status = interrupted ? -2 : proc->status; decref(proc); + if (events) { + // the decref call created an exit event, process it now + queue_process_events(events); + } } else { proc->refcount--; } @@ -250,6 +264,18 @@ static void children_kill_cb(uv_timer_t *handle) } } +static void process_close_event(void **argv) +{ + Process *proc = argv[0]; + shell_free_argv(proc->argv); + if (proc->type == kProcessTypePty) { + xfree(((PtyProcess *)proc)->term_name); + } + if (proc->cb) { + proc->cb(proc, proc->status, proc->data); + } +} + static void decref(Process *proc) { if (--proc->refcount != 0) { @@ -264,16 +290,9 @@ static void decref(Process *proc) break; } } - assert(node); kl_shift_at(WatcherPtr, loop->children, node); - shell_free_argv(proc->argv); - if (proc->type == kProcessTypePty) { - xfree(((PtyProcess *)proc)->term_name); - } - if (proc->cb) { - proc->cb(proc, proc->status, proc->data); - } + CREATE_EVENT(proc->events, process_close_event, 1, proc); } static void process_close(Process *proc) @@ -293,28 +312,27 @@ static void process_close(Process *proc) } } -static void on_process_exit(Process *proc) +static void process_close_handles(void **argv) { - if (exiting) { - on_process_exit_event((Event) {.data = proc}); - } else { - loop_push_event(proc->loop, - (Event) {.handler = on_process_exit_event, .data = proc}, false); - } + Process *proc = argv[0]; + process_close_streams(proc); + process_close(proc); +} +static void on_process_exit(Process *proc) +{ Loop *loop = proc->loop; - if (loop->children_stop_requests && !--loop->children_stop_requests) { + if (proc->stopped_time && loop->children_stop_requests + && !--loop->children_stop_requests) { // Stop the timer if no more stop requests are pending DLOG("Stopping process kill timer"); uv_timer_stop(&loop->children_kill_timer); } -} - -static void on_process_exit_event(Event event) -{ - Process *proc = event.data; - process_close_streams(proc); - process_close(proc); + // Process handles are closed in the next event loop tick. This is done to + // give libuv more time to read data from the OS after the process exits(If + // process_close_streams is called with data still in the OS buffer, we lose + // it) + CREATE_EVENT(proc->events, process_close_handles, 1, proc); } static void on_process_stream_close(Stream *stream, void *data) diff --git a/src/nvim/event/process.h b/src/nvim/event/process.h index 5c84a7d1d0..45edc46b95 100644 --- a/src/nvim/event/process.h +++ b/src/nvim/event/process.h @@ -26,14 +26,16 @@ struct process { process_exit_cb cb; internal_process_cb internal_exit_cb, internal_close_cb; bool closed, term_sent; + Queue *events; }; -static inline Process process_init(ProcessType type, void *data) +static inline Process process_init(Loop *loop, ProcessType type, void *data) { return (Process) { .type = type, .data = data, - .loop = NULL, + .loop = loop, + .events = NULL, .pid = 0, .status = 0, .refcount = 0, diff --git a/src/nvim/event/pty_process.c b/src/nvim/event/pty_process.c index 1e24d7c919..8eef72f12f 100644 --- a/src/nvim/event/pty_process.c +++ b/src/nvim/event/pty_process.c @@ -33,17 +33,18 @@ # include "event/pty_process.c.generated.h" #endif -static const unsigned int KILL_RETRIES = 5; -static const unsigned int KILL_TIMEOUT = 2; // seconds - bool pty_process_spawn(PtyProcess *ptyproc) FUNC_ATTR_NONNULL_ALL { + static struct termios termios; + if (!termios.c_cflag) { + init_termios(&termios); + } + Process *proc = (Process *)ptyproc; + assert(!proc->err); uv_signal_start(&proc->loop->children_watcher, chld_handler, SIGCHLD); ptyproc->winsize = (struct winsize){ptyproc->height, ptyproc->width, 0, 0}; - struct termios termios; - init_termios(&termios); uv_disable_stdio_inheritance(); int master; int pid = forkpty(&master, NULL, &termios, &ptyproc->winsize); @@ -73,9 +74,6 @@ bool pty_process_spawn(PtyProcess *ptyproc) if (proc->out && !set_duplicating_descriptor(master, &proc->out->uv.pipe)) { goto error; } - if (proc->err && !set_duplicating_descriptor(master, &proc->err->uv.pipe)) { - goto error; - } ptyproc->tty_fd = master; proc->pid = pid; @@ -83,19 +81,8 @@ bool pty_process_spawn(PtyProcess *ptyproc) error: close(master); - - // terminate spawned process - kill(pid, SIGTERM); - int status, child; - unsigned int try = 0; - while (try++ < KILL_RETRIES && !(child = waitpid(pid, &status, WNOHANG))) { - sleep(KILL_TIMEOUT); - } - if (child != pid) { - kill(pid, SIGKILL); - waitpid(pid, NULL, 0); - } - + kill(pid, SIGKILL); + waitpid(pid, NULL, 0); return false; } @@ -152,7 +139,6 @@ static void init_child(PtyProcess *ptyproc) FUNC_ATTR_NONNULL_ALL static void init_termios(struct termios *termios) FUNC_ATTR_NONNULL_ALL { - memset(termios, 0, sizeof(struct termios)); // Taken from pangoterm termios->c_iflag = ICRNL|IXON; termios->c_oflag = OPOST|ONLCR; diff --git a/src/nvim/event/pty_process.h b/src/nvim/event/pty_process.h index a12b5489c5..446d7fd3c8 100644 --- a/src/nvim/event/pty_process.h +++ b/src/nvim/event/pty_process.h @@ -13,10 +13,10 @@ typedef struct pty_process { int tty_fd; } PtyProcess; -static inline PtyProcess pty_process_init(void *data) +static inline PtyProcess pty_process_init(Loop *loop, void *data) { PtyProcess rv; - rv.process = process_init(kProcessTypePty, data); + rv.process = process_init(loop, kProcessTypePty, data); rv.term_name = NULL; rv.width = 80; rv.height = 24; diff --git a/src/nvim/event/queue.c b/src/nvim/event/queue.c new file mode 100644 index 0000000000..19eca14144 --- /dev/null +++ b/src/nvim/event/queue.c @@ -0,0 +1,208 @@ +// Queue for selective async event processing. Instances of this queue support a +// parent/child relationship with the following properties: +// +// - pushing a node to a child queue will push a corresponding link node to the +// parent queue +// - removing a link node from a parent queue will remove the next node +// in the linked child queue +// - removing a node from a child queue will remove the corresponding link node +// in the parent queue +// +// These properties allow neovim to organize and process events from different +// sources with a certain degree of control. Here's how the queue is used: +// +// +----------------+ +// | Main loop | +// +----------------+ +// ^ +// | +// +----------------+ +// +-------------->| Event loop |<------------+ +// | +--+-------------+ | +// | ^ ^ | +// | | | | +// +-----------+ +-----------+ +---------+ +---------+ +// | Channel 1 | | Channel 2 | | Job 1 | | Job 2 | +// +-----------+ +-----------+ +---------+ +---------+ +// +// +// In the above diagram, the lower boxes represents event emitters, each with +// it's own private queue that have the event loop queue as the parent. +// +// When idle, the main loop spins the event loop which queues events from many +// sources(channels, jobs, user...). Each event emitter pushes events to its own +// private queue which is propagated to the event loop queue. When the main loop +// consumes an event, the corresponding event is removed from the emitter's +// queue. +// +// The main reason for this queue hierarchy is to allow focusing on a single +// event emitter while blocking the main loop. For example, if the `jobwait` +// vimscript function is called on job1, the main loop will temporarily stop +// polling the event loop queue and poll job1 queue instead. Same with channels, +// when calling `rpcrequest`, we want to temporarily stop processing events from +// other sources and focus on a specific channel. + +#include <assert.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> + + +#include <uv.h> + +#include "nvim/event/queue.h" +#include "nvim/memory.h" +#include "nvim/os/time.h" + +typedef struct queue_item QueueItem; +struct queue_item { + union { + Queue *queue; + struct { + Event event; + QueueItem *parent; + } item; + } data; + bool link; // this is just a link to a node in a child queue + QUEUE node; +}; + +struct queue { + Queue *parent; + QUEUE headtail; + put_callback put_cb; + void *data; +}; + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "event/queue.c.generated.h" +#endif + +static Event NILEVENT = {.handler = NULL, .argv = {NULL}}; + +Queue *queue_new_parent(put_callback put_cb, void *data) +{ + return queue_new(NULL, put_cb, data); +} + +Queue *queue_new_child(Queue *parent) + FUNC_ATTR_NONNULL_ALL +{ + assert(!parent->parent); + return queue_new(parent, NULL, NULL); +} + +static Queue *queue_new(Queue *parent, put_callback put_cb, void *data) +{ + Queue *rv = xmalloc(sizeof(Queue)); + QUEUE_INIT(&rv->headtail); + rv->parent = parent; + rv->put_cb = put_cb; + rv->data = data; + return rv; +} + +void queue_free(Queue *queue) +{ + assert(queue); + if (queue->parent) { + while (!QUEUE_EMPTY(&queue->headtail)) { + QUEUE *q = QUEUE_HEAD(&queue->headtail); + QueueItem *item = queue_node_data(q); + assert(!item->link); + QUEUE_REMOVE(&item->data.item.parent->node); + xfree(item->data.item.parent); + QUEUE_REMOVE(q); + xfree(item); + } + } + + xfree(queue); +} + +Event queue_get(Queue *queue) +{ + return queue_empty(queue) ? NILEVENT : queue_remove(queue); +} + +void queue_put_event(Queue *queue, Event event) +{ + assert(queue); + assert(queue->parent); // don't push directly to the parent queue + queue_push(queue, event); + if (queue->parent->put_cb) { + queue->parent->put_cb(queue->parent, queue->parent->data); + } +} + +void queue_process_events(Queue *queue) +{ + assert(queue); + while (!queue_empty(queue)) { + Event event = queue_get(queue); + if (event.handler) { + event.handler(event.argv); + } + } +} + +bool queue_empty(Queue *queue) +{ + assert(queue); + return QUEUE_EMPTY(&queue->headtail); +} + +void queue_replace_parent(Queue *queue, Queue *new_parent) +{ + assert(queue_empty(queue)); + queue->parent = new_parent; +} + +static Event queue_remove(Queue *queue) +{ + assert(!queue_empty(queue)); + QUEUE *h = QUEUE_HEAD(&queue->headtail); + QUEUE_REMOVE(h); + QueueItem *item = queue_node_data(h); + Event rv; + + if (item->link) { + assert(!queue->parent); + // remove the next node in the linked queue + Queue *linked = item->data.queue; + assert(!queue_empty(linked)); + QueueItem *child = + queue_node_data(QUEUE_HEAD(&linked->headtail)); + QUEUE_REMOVE(&child->node); + rv = child->data.item.event; + xfree(child); + } else { + assert(queue->parent); + assert(!queue_empty(queue->parent)); + // remove the corresponding link node in the parent queue + QUEUE_REMOVE(&item->data.item.parent->node); + xfree(item->data.item.parent); + rv = item->data.item.event; + } + + xfree(item); + return rv; +} + +static void queue_push(Queue *queue, Event event) +{ + QueueItem *item = xmalloc(sizeof(QueueItem)); + item->link = false; + item->data.item.event = event; + QUEUE_INSERT_TAIL(&queue->headtail, &item->node); + // push link node to the parent queue + item->data.item.parent = xmalloc(sizeof(QueueItem)); + item->data.item.parent->link = true; + item->data.item.parent->data.queue = queue; + QUEUE_INSERT_TAIL(&queue->parent->headtail, &item->data.item.parent->node); +} + +static QueueItem *queue_node_data(QUEUE *q) +{ + return QUEUE_DATA(q, QueueItem, node); +} diff --git a/src/nvim/event/queue.h b/src/nvim/event/queue.h new file mode 100644 index 0000000000..85fc59f8b2 --- /dev/null +++ b/src/nvim/event/queue.h @@ -0,0 +1,19 @@ +#ifndef NVIM_EVENT_QUEUE_H +#define NVIM_EVENT_QUEUE_H + +#include <uv.h> + +#include "nvim/event/defs.h" +#include "nvim/lib/queue.h" + +typedef struct queue Queue; +typedef void (*put_callback)(Queue *queue, void *data); + +#define queue_put(q, h, ...) \ + queue_put_event(q, event_create(1, h, __VA_ARGS__)); + + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "event/queue.h.generated.h" +#endif +#endif // NVIM_EVENT_QUEUE_H diff --git a/src/nvim/event/rstream.c b/src/nvim/event/rstream.c index 9d2439ac2b..9f3fbc25ff 100644 --- a/src/nvim/event/rstream.c +++ b/src/nvim/event/rstream.c @@ -49,6 +49,7 @@ void rstream_init(Stream *stream, size_t bufsize) /// /// @param stream The `Stream` instance void rstream_start(Stream *stream, stream_read_cb cb) + FUNC_ATTR_NONNULL_ARG(1) { stream->read_cb = cb; if (stream->uvstream) { @@ -62,6 +63,7 @@ void rstream_start(Stream *stream, stream_read_cb cb) /// /// @param stream The `Stream` instance void rstream_stop(Stream *stream) + FUNC_ATTR_NONNULL_ALL { if (stream->uvstream) { uv_read_stop(stream->uvstream); @@ -112,7 +114,7 @@ static void read_cb(uv_stream_t *uvstream, ssize_t cnt, const uv_buf_t *buf) // Read error or EOF, either way stop the stream and invoke the callback // with eof == true uv_read_stop(uvstream); - invoke_read_cb(stream, true); + invoke_read_cb(stream, 0, true); } return; } @@ -122,7 +124,7 @@ static void read_cb(uv_stream_t *uvstream, ssize_t cnt, const uv_buf_t *buf) // Data was already written, so all we need is to update 'wpos' to reflect // the space actually used in the buffer. rbuffer_produced(stream->buffer, nread); - invoke_read_cb(stream, false); + invoke_read_cb(stream, nread, false); } // Called by the by the 'idle' handle to emulate a reading event @@ -156,7 +158,7 @@ static void fread_idle_cb(uv_idle_t *handle) if (req.result <= 0) { uv_idle_stop(&stream->uv.idle); - invoke_read_cb(stream, true); + invoke_read_cb(stream, 0, true); return; } @@ -164,12 +166,36 @@ static void fread_idle_cb(uv_idle_t *handle) size_t nread = (size_t) req.result; rbuffer_produced(stream->buffer, nread); stream->fpos += nread; - invoke_read_cb(stream, false); + invoke_read_cb(stream, nread, false); } -static void invoke_read_cb(Stream *stream, bool eof) +static void read_event(void **argv) { + Stream *stream = argv[0]; if (stream->read_cb) { - stream->read_cb(stream, stream->buffer, stream->data, eof); + size_t count = (uintptr_t)argv[1]; + bool eof = (uintptr_t)argv[2]; + stream->read_cb(stream, stream->buffer, count, stream->data, eof); + } + stream->pending_reqs--; + if (stream->closed && !stream->pending_reqs) { + stream_close_handle(stream); } } + +static void invoke_read_cb(Stream *stream, size_t count, bool eof) +{ + if (stream->closed) { + return; + } + + // Don't let the stream be closed before the event is processed. + stream->pending_reqs++; + + CREATE_EVENT(stream->events, + read_event, + 3, + stream, + (void *)(uintptr_t *)count, + (void *)(uintptr_t)eof); +} diff --git a/src/nvim/event/signal.c b/src/nvim/event/signal.c index 63133b4f57..11ce15a882 100644 --- a/src/nvim/event/signal.c +++ b/src/nvim/event/signal.c @@ -15,6 +15,7 @@ void signal_watcher_init(Loop *loop, SignalWatcher *watcher, void *data) watcher->uv.data = watcher; watcher->data = data; watcher->cb = NULL; + watcher->events = loop->fast_events; } void signal_watcher_start(SignalWatcher *watcher, signal_cb cb, int signum) @@ -37,10 +38,16 @@ void signal_watcher_close(SignalWatcher *watcher, signal_close_cb cb) uv_close((uv_handle_t *)&watcher->uv, close_cb); } +static void signal_event(void **argv) +{ + SignalWatcher *watcher = argv[0]; + watcher->cb(watcher, watcher->uv.signum, watcher->data); +} + static void signal_watcher_cb(uv_signal_t *handle, int signum) { SignalWatcher *watcher = handle->data; - watcher->cb(watcher, signum, watcher->data); + CREATE_EVENT(watcher->events, signal_event, 1, watcher); } static void close_cb(uv_handle_t *handle) diff --git a/src/nvim/event/signal.h b/src/nvim/event/signal.h index c269fa9d95..e32608acc0 100644 --- a/src/nvim/event/signal.h +++ b/src/nvim/event/signal.h @@ -14,6 +14,7 @@ struct signal_watcher { void *data; signal_cb cb; signal_close_cb close_cb; + Queue *events; }; #ifdef INCLUDE_GENERATED_DECLARATIONS diff --git a/src/nvim/event/socket.c b/src/nvim/event/socket.c index bdc632abf0..347e464d25 100644 --- a/src/nvim/event/socket.c +++ b/src/nvim/event/socket.c @@ -77,6 +77,7 @@ void socket_watcher_init(Loop *loop, SocketWatcher *watcher, watcher->stream->data = watcher; watcher->cb = NULL; watcher->close_cb = NULL; + watcher->events = NULL; } int socket_watcher_start(SocketWatcher *watcher, int backlog, socket_cb cb) @@ -113,6 +114,7 @@ int socket_watcher_start(SocketWatcher *watcher, int backlog, socket_cb cb) } int socket_watcher_accept(SocketWatcher *watcher, Stream *stream, void *data) + FUNC_ATTR_NONNULL_ARG(1) FUNC_ATTR_NONNULL_ARG(2) { uv_stream_t *client; @@ -142,10 +144,18 @@ void socket_watcher_close(SocketWatcher *watcher, socket_close_cb cb) uv_close((uv_handle_t *)watcher->stream, close_cb); } +static void connection_event(void **argv) +{ + SocketWatcher *watcher = argv[0]; + int status = (int)(uintptr_t)(argv[1]); + watcher->cb(watcher, status, watcher->data); +} + static void connection_cb(uv_stream_t *handle, int status) { SocketWatcher *watcher = handle->data; - watcher->cb(watcher, status, watcher->data); + CREATE_EVENT(watcher->events, connection_event, 2, watcher, + (void *)(uintptr_t)status); } static void close_cb(uv_handle_t *handle) diff --git a/src/nvim/event/socket.h b/src/nvim/event/socket.h index 17fd39f33b..ad59fdbe3a 100644 --- a/src/nvim/event/socket.h +++ b/src/nvim/event/socket.h @@ -30,6 +30,7 @@ struct socket_watcher { void *data; socket_cb cb; socket_close_cb close_cb; + Queue *events; }; #ifdef INCLUDE_GENERATED_DECLARATIONS diff --git a/src/nvim/event/stream.c b/src/nvim/event/stream.c index 959b532146..6caad6fdcc 100644 --- a/src/nvim/event/stream.c +++ b/src/nvim/event/stream.c @@ -32,6 +32,7 @@ int stream_set_blocking(int fd, bool blocking) void stream_init(Loop *loop, Stream *stream, int fd, uv_stream_t *uvstream, void *data) + FUNC_ATTR_NONNULL_ARG(2) { stream->uvstream = uvstream; @@ -55,6 +56,7 @@ void stream_init(Loop *loop, Stream *stream, int fd, uv_stream_t *uvstream, if (stream->uvstream) { stream->uvstream->data = stream; + loop = stream->uvstream->loop->data; } stream->data = data; @@ -69,16 +71,13 @@ void stream_init(Loop *loop, Stream *stream, int fd, uv_stream_t *uvstream, stream->internal_close_cb = NULL; stream->closed = false; stream->buffer = NULL; + stream->events = NULL; } void stream_close(Stream *stream, stream_close_cb on_stream_close) + FUNC_ATTR_NONNULL_ARG(1) { assert(!stream->closed); - - if (stream->buffer) { - rbuffer_free(stream->buffer); - } - stream->closed = true; stream->close_cb = on_stream_close; @@ -88,6 +87,7 @@ void stream_close(Stream *stream, stream_close_cb on_stream_close) } void stream_close_handle(Stream *stream) + FUNC_ATTR_NONNULL_ALL { if (stream->uvstream) { uv_close((uv_handle_t *)stream->uvstream, close_cb); @@ -99,6 +99,9 @@ void stream_close_handle(Stream *stream) static void close_cb(uv_handle_t *handle) { Stream *stream = handle->data; + if (stream->buffer) { + rbuffer_free(stream->buffer); + } if (stream->close_cb) { stream->close_cb(stream, stream->data); } diff --git a/src/nvim/event/stream.h b/src/nvim/event/stream.h index 37410b2036..c6baac0db7 100644 --- a/src/nvim/event/stream.h +++ b/src/nvim/event/stream.h @@ -14,10 +14,14 @@ typedef struct stream Stream; /// /// @param stream The Stream instance /// @param rbuffer The associated RBuffer instance +/// @param count Number of bytes to read. This must be respected if keeping +/// the order of events is a requirement. This is because events +/// may be queued and only processed later when more data is copied +/// into to the buffer, so one read may starve another. /// @param data User-defined data /// @param eof If the stream reached EOF. -typedef void (*stream_read_cb)(Stream *stream, RBuffer *buf, void *data, - bool eof); +typedef void (*stream_read_cb)(Stream *stream, RBuffer *buf, size_t count, + void *data, bool eof); /// Type of function called when the Stream has information about a write /// request. @@ -47,6 +51,7 @@ struct stream { size_t pending_reqs; void *data, *internal_data; bool closed; + Queue *events; }; #ifdef INCLUDE_GENERATED_DECLARATIONS diff --git a/src/nvim/event/time.c b/src/nvim/event/time.c index ce33cdfc10..7bf333bcea 100644 --- a/src/nvim/event/time.c +++ b/src/nvim/event/time.c @@ -16,6 +16,7 @@ void time_watcher_init(Loop *loop, TimeWatcher *watcher, void *data) uv_timer_init(&loop->uv, &watcher->uv); watcher->uv.data = watcher; watcher->data = data; + watcher->events = loop->fast_events; } void time_watcher_start(TimeWatcher *watcher, time_cb cb, uint64_t timeout, @@ -39,11 +40,17 @@ void time_watcher_close(TimeWatcher *watcher, time_cb cb) uv_close((uv_handle_t *)&watcher->uv, close_cb); } +static void time_event(void **argv) +{ + TimeWatcher *watcher = argv[0]; + watcher->cb(watcher, watcher->data); +} + static void time_watcher_cb(uv_timer_t *handle) FUNC_ATTR_NONNULL_ALL { TimeWatcher *watcher = handle->data; - watcher->cb(watcher, watcher->data); + CREATE_EVENT(watcher->events, time_event, 1, watcher); } static void close_cb(uv_handle_t *handle) diff --git a/src/nvim/event/time.h b/src/nvim/event/time.h index ee50e53d11..7882b2b627 100644 --- a/src/nvim/event/time.h +++ b/src/nvim/event/time.h @@ -12,6 +12,7 @@ struct time_watcher { uv_timer_t uv; void *data; time_cb cb, close_cb; + Queue *events; }; #ifdef INCLUDE_GENERATED_DECLARATIONS diff --git a/src/nvim/event/uv_process.h b/src/nvim/event/uv_process.h index a17f1446b3..5ee73044b5 100644 --- a/src/nvim/event/uv_process.h +++ b/src/nvim/event/uv_process.h @@ -12,10 +12,10 @@ typedef struct uv_process { uv_stdio_container_t uvstdio[3]; } UvProcess; -static inline UvProcess uv_process_init(void *data) +static inline UvProcess uv_process_init(Loop *loop, void *data) { UvProcess rv; - rv.process = process_init(kProcessTypeUv, data); + rv.process = process_init(loop, kProcessTypeUv, data); return rv; } diff --git a/src/nvim/event/wstream.c b/src/nvim/event/wstream.c index 5fcb724fe3..8028e35e6b 100644 --- a/src/nvim/event/wstream.c +++ b/src/nvim/event/wstream.c @@ -118,6 +118,7 @@ WBuffer *wstream_new_buffer(char *data, size_t size, size_t refcount, wbuffer_data_finalizer cb) + FUNC_ATTR_NONNULL_ARG(1) { WBuffer *rv = xmalloc(sizeof(WBuffer)); rv->size = size; @@ -151,6 +152,7 @@ static void write_cb(uv_write_t *req, int status) } void wstream_release_wbuffer(WBuffer *buffer) + FUNC_ATTR_NONNULL_ALL { if (!--buffer->refcount) { if (buffer->cb) { diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 2c92113404..802a378b07 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -2395,7 +2395,7 @@ static int check_readonly(int *forceit, buf_T *buf) * the file exists and permissions are read-only. */ if (!*forceit && (buf->b_p_ro || (os_file_exists(buf->b_ffname) - && os_file_is_readonly((char *)buf->b_ffname)))) { + && !os_file_is_writable((char *)buf->b_ffname)))) { if ((p_confirm || cmdmod.confirm) && buf->b_fname != NULL) { char_u buff[DIALOG_MSG_SIZE]; @@ -6125,7 +6125,7 @@ char_u * sign_typenr2name(int typenr) /* * Undefine/free all signs. */ -void free_signs() +void free_signs(void) { while (first_sign != NULL) sign_undefine(first_sign, NULL); diff --git a/src/nvim/ex_cmds2.c b/src/nvim/ex_cmds2.c index d610aa6d19..efa18aa681 100644 --- a/src/nvim/ex_cmds2.c +++ b/src/nvim/ex_cmds2.c @@ -2624,7 +2624,7 @@ char_u *get_scriptname(scid_T id) } # if defined(EXITFREE) -void free_scriptnames() +void free_scriptnames(void) { # define FREE_SCRIPTNAME(item) xfree((item)->sn_name) GA_DEEP_CLEAR(&script_items, scriptitem_T, FREE_SCRIPTNAME); diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index 785db1dbd1..03116d454f 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -62,6 +62,7 @@ #include "nvim/tag.h" #include "nvim/window.h" #include "nvim/ui.h" +#include "nvim/os/input.h" #include "nvim/os/os.h" #include "nvim/event/loop.h" @@ -298,14 +299,14 @@ getcmdline ( /* Get a character. Ignore K_IGNORE, it should not do anything, such * as stop completion. */ - loop_enable_deferred_events(&loop); + input_enable_events(); do { c = safe_vgetc(); } while (c == K_IGNORE); - loop_disable_deferred_events(&loop); + input_disable_events(); if (c == K_EVENT) { - loop_process_event(&loop); + queue_process_events(loop.events); continue; } diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index dee9680230..70cdac7947 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -415,14 +415,10 @@ readfile ( msg_scroll = TRUE; /* don't overwrite previous file message */ /* - * If the name ends in a path separator, we can't open it. Check here, - * because reading the file may actually work, but then creating the swap - * file may destroy it! Reported on MS-DOS and Win 95. * If the name is too long we might crash further on, quit here. */ if (fname != NULL && *fname != NUL) { - p = fname + STRLEN(fname); - if (after_pathsep((char *)fname, (char *)p) || STRLEN(fname) >= MAXPATHL) { + if (STRLEN(fname) >= MAXPATHL) { filemess(curbuf, fname, (char_u *)_("Illegal file name"), 0); msg_end(); msg_scroll = msg_save; @@ -2627,7 +2623,7 @@ buf_write ( * Check if the file is really writable (when renaming the file to * make a backup we won't discover it later). */ - file_readonly = os_file_is_readonly((char *)fname); + file_readonly = !os_file_is_writable((char *)fname); if (!forceit && file_readonly) { if (vim_strchr(p_cpo, CPO_FWRITE) != NULL) { diff --git a/src/nvim/func_attr.h b/src/nvim/func_attr.h index 519f61c763..c31d21ec6d 100644 --- a/src/nvim/func_attr.h +++ b/src/nvim/func_attr.h @@ -179,7 +179,7 @@ #endif #ifdef DEFINE_FUNC_ATTRIBUTES - #define FUNC_ATTR_DEFERRED + #define FUNC_ATTR_ASYNC #define FUNC_ATTR_MALLOC REAL_FATTR_MALLOC #define FUNC_ATTR_ALLOC_SIZE(x) REAL_FATTR_ALLOC_SIZE(x) #define FUNC_ATTR_ALLOC_SIZE_PROD(x,y) REAL_FATTR_ALLOC_SIZE_PROD(x,y) diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index bbeef376b0..6a6e4f2214 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -1758,7 +1758,7 @@ static int vgetorpeek(int advance) if (c1 == K_SPECIAL) nolmaplen = 2; else { - LANGMAP_ADJUST(c1, (State & INSERT) == 0); + LANGMAP_ADJUST(c1, (State & (CMDLINE | INSERT)) == 0); nolmaplen = 0; } /* First try buffer-local mappings. */ diff --git a/src/nvim/globals.h b/src/nvim/globals.h index e4dcad9afb..68cb923e42 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -898,14 +898,6 @@ EXTERN FILE *scriptout INIT(= NULL); /* stream to write script to */ /* volatile because it is used in signal handler catch_sigint(). */ EXTERN volatile int got_int INIT(= FALSE); /* set to TRUE when interrupt signal occurred */ -EXTERN int disable_breakcheck INIT(= 0); // > 0 if breakchecks should be - // ignored. FIXME(tarruda): Hacky - // way to run functions that would - // result in *_breakcheck calls - // while events that would normally - // be deferred are being processed - // immediately. Ref: - // neovim/neovim#2371 EXTERN int bangredo INIT(= FALSE); /* set to TRUE with ! command */ EXTERN int searchcmdlen; /* length of previous search cmd */ EXTERN int reg_do_extmatch INIT(= 0); /* Used when compiling regexp: diff --git a/src/nvim/lib/queue.h b/src/nvim/lib/queue.h new file mode 100644 index 0000000000..fe02b454ea --- /dev/null +++ b/src/nvim/lib/queue.h @@ -0,0 +1,92 @@ +/* Copyright (c) 2013, Ben Noordhuis <info@bnoordhuis.nl> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef QUEUE_H_ +#define QUEUE_H_ + +typedef void *QUEUE[2]; + +/* Private macros. */ +#define QUEUE_NEXT(q) (*(QUEUE **) &((*(q))[0])) +#define QUEUE_PREV(q) (*(QUEUE **) &((*(q))[1])) +#define QUEUE_PREV_NEXT(q) (QUEUE_NEXT(QUEUE_PREV(q))) +#define QUEUE_NEXT_PREV(q) (QUEUE_PREV(QUEUE_NEXT(q))) + +/* Public macros. */ +#define QUEUE_DATA(ptr, type, field) \ + ((type *) ((char *) (ptr) - ((char *) &((type *) 0)->field))) + +#define QUEUE_FOREACH(q, h) \ + for ((q) = QUEUE_NEXT(h); (q) != (h); (q) = QUEUE_NEXT(q)) + +#define QUEUE_EMPTY(q) \ + ((const QUEUE *) (q) == (const QUEUE *) QUEUE_NEXT(q)) + +#define QUEUE_HEAD(q) \ + (QUEUE_NEXT(q)) + +#define QUEUE_INIT(q) \ + do { \ + QUEUE_NEXT(q) = (q); \ + QUEUE_PREV(q) = (q); \ + } \ + while (0) + +#define QUEUE_ADD(h, n) \ + do { \ + QUEUE_PREV_NEXT(h) = QUEUE_NEXT(n); \ + QUEUE_NEXT_PREV(n) = QUEUE_PREV(h); \ + QUEUE_PREV(h) = QUEUE_PREV(n); \ + QUEUE_PREV_NEXT(h) = (h); \ + } \ + while (0) + +#define QUEUE_SPLIT(h, q, n) \ + do { \ + QUEUE_PREV(n) = QUEUE_PREV(h); \ + QUEUE_PREV_NEXT(n) = (n); \ + QUEUE_NEXT(n) = (q); \ + QUEUE_PREV(h) = QUEUE_PREV(q); \ + QUEUE_PREV_NEXT(h) = (h); \ + QUEUE_PREV(q) = (n); \ + } \ + while (0) + +#define QUEUE_INSERT_HEAD(h, q) \ + do { \ + QUEUE_NEXT(q) = QUEUE_NEXT(h); \ + QUEUE_PREV(q) = (h); \ + QUEUE_NEXT_PREV(q) = (q); \ + QUEUE_NEXT(h) = (q); \ + } \ + while (0) + +#define QUEUE_INSERT_TAIL(h, q) \ + do { \ + QUEUE_NEXT(q) = (h); \ + QUEUE_PREV(q) = QUEUE_PREV(h); \ + QUEUE_PREV_NEXT(q) = (q); \ + QUEUE_PREV(h) = (q); \ + } \ + while (0) + +#define QUEUE_REMOVE(q) \ + do { \ + QUEUE_PREV_NEXT(q) = QUEUE_NEXT(q); \ + QUEUE_NEXT_PREV(q) = QUEUE_PREV(q); \ + } \ + while (0) + +#endif /* QUEUE_H_ */ diff --git a/src/nvim/main.c b/src/nvim/main.c index bfaeada6de..e11db16c61 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -157,11 +157,11 @@ void event_init(void) void event_teardown(void) { - if (!loop.deferred_events) { + if (!loop.events) { return; } - loop_process_all_events(&loop); + queue_process_events(loop.events); input_stop(); channel_teardown(); process_teardown(&loop); diff --git a/src/nvim/map.c b/src/nvim/map.c index 5d83020619..ed7bda4cce 100644 --- a/src/nvim/map.c +++ b/src/nvim/map.c @@ -116,5 +116,5 @@ MAP_IMPL(cstr_t, uint64_t, DEFAULT_INITIALIZER) MAP_IMPL(cstr_t, ptr_t, DEFAULT_INITIALIZER) MAP_IMPL(ptr_t, ptr_t, DEFAULT_INITIALIZER) MAP_IMPL(uint64_t, ptr_t, DEFAULT_INITIALIZER) -#define MSGPACK_HANDLER_INITIALIZER {.fn = NULL, .defer = false} +#define MSGPACK_HANDLER_INITIALIZER {.fn = NULL, .async = false} MAP_IMPL(String, MsgpackRpcRequestHandler, MSGPACK_HANDLER_INITIALIZER) diff --git a/src/nvim/memline.c b/src/nvim/memline.c index 53f9a544b4..d90e91be5d 100644 --- a/src/nvim/memline.c +++ b/src/nvim/memline.c @@ -3934,8 +3934,9 @@ long ml_find_line_or_offset(buf_T *buf, linenr_T lnum, long *offp) size += lnum - 1; /* Don't count the last line break if 'bin' and 'noeol'. */ - if (buf->b_p_bin && !buf->b_p_eol) + if (buf->b_p_bin && !buf->b_p_eol && buf->b_ml.ml_line_count == lnum) { size -= ffdos + 1; + } } return size; diff --git a/src/nvim/msgpack_rpc/channel.c b/src/nvim/msgpack_rpc/channel.c index 861614f147..0e3b8200c9 100644 --- a/src/nvim/msgpack_rpc/channel.c +++ b/src/nvim/msgpack_rpc/channel.c @@ -68,6 +68,7 @@ typedef struct { uint64_t next_request_id; kvec_t(ChannelCallFrame *) call_stack; kvec_t(WBuffer *) delayed_notifications; + Queue *events; } Channel; typedef struct { @@ -123,14 +124,14 @@ void channel_teardown(void) uint64_t channel_from_process(char **argv) { Channel *channel = register_channel(kChannelTypeProc); - channel->data.process.uvproc = uv_process_init(channel); + channel->data.process.uvproc = uv_process_init(&loop, channel); Process *proc = &channel->data.process.uvproc.process; proc->argv = argv; proc->in = &channel->data.process.in; proc->out = &channel->data.process.out; proc->err = &channel->data.process.err; proc->cb = process_exit; - if (!process_spawn(&loop, proc)) { + if (!process_spawn(proc)) { loop_poll_events(&loop, 0); decref(channel); return 0; @@ -153,6 +154,9 @@ void channel_from_connection(SocketWatcher *watcher) { Channel *channel = register_channel(kChannelTypeSocket); socket_watcher_accept(watcher, &channel->data.stream, channel); + incref(channel); // close channel only after the stream is closed + channel->data.stream.internal_close_cb = close_cb; + channel->data.stream.internal_data = channel; wstream_init(&channel->data.stream, 0); rstream_init(&channel->data.stream, CHANNEL_BUFFER_SIZE); rstream_start(&channel->data.stream, parse_msgpack); @@ -221,7 +225,7 @@ Object channel_send_call(uint64_t id, ChannelCallFrame frame = {request_id, false, false, NIL}; kv_push(ChannelCallFrame *, channel->call_stack, &frame); channel->pending_requests++; - LOOP_POLL_EVENTS_UNTIL(&loop, -1, frame.returned); + LOOP_PROCESS_EVENTS_UNTIL(&loop, channel->events, -1, frame.returned); (void)kv_pop(channel->call_stack); channel->pending_requests--; @@ -324,7 +328,8 @@ static void channel_from_stdio(void) wstream_init_fd(&loop, &channel->data.std.out, 1, 0, NULL); } -static void forward_stderr(Stream *stream, RBuffer *rbuf, void *data, bool eof) +static void forward_stderr(Stream *stream, RBuffer *rbuf, size_t count, + void *data, bool eof) { while (rbuffer_size(rbuf)) { char buf[256]; @@ -339,7 +344,8 @@ static void process_exit(Process *proc, int status, void *data) decref(data); } -static void parse_msgpack(Stream *stream, RBuffer *rbuf, void *data, bool eof) +static void parse_msgpack(Stream *stream, RBuffer *rbuf, size_t c, void *data, + bool eof) { Channel *channel = data; incref(channel); @@ -447,31 +453,31 @@ static void handle_request(Channel *channel, msgpack_object *request) method->via.bin.size); } else { handler.fn = msgpack_rpc_handle_missing_method; - handler.defer = false; + handler.async = true; } Array args = ARRAY_DICT_INIT; if (!msgpack_rpc_to_array(msgpack_rpc_args(request), &args)) { handler.fn = msgpack_rpc_handle_invalid_arguments; - handler.defer = false; + handler.async = true; } - bool defer = (!kv_size(channel->call_stack) && handler.defer); RequestEvent *event_data = xmalloc(sizeof(RequestEvent)); event_data->channel = channel; event_data->handler = handler; event_data->args = args; event_data->request_id = request_id; incref(channel); - loop_push_event(&loop, (Event) { - .handler = on_request_event, - .data = event_data - }, defer); + if (handler.async) { + on_request_event((void **)&event_data); + } else { + queue_put(channel->events, on_request_event, 1, event_data); + } } -static void on_request_event(Event event) +static void on_request_event(void **argv) { - RequestEvent *e = event.data; + RequestEvent *e = argv[0]; Channel *channel = e->channel; MsgpackRpcRequestHandler handler = e->handler; Array args = e->args; @@ -636,7 +642,7 @@ static void close_channel(Channel *channel) switch (channel->type) { case kChannelTypeSocket: - stream_close(&channel->data.stream, close_cb); + stream_close(&channel->data.stream, NULL); break; case kChannelTypeProc: if (!channel->data.process.uvproc.process.closed) { @@ -646,9 +652,8 @@ static void close_channel(Channel *channel) case kChannelTypeStdio: stream_close(&channel->data.std.in, NULL); stream_close(&channel->data.std.out, NULL); - loop_push_event(&loop, - (Event) { .handler = on_stdio_close, .data = channel }, false); - break; + queue_put(loop.fast_events, exit_event, 1, channel); + return; default: abort(); } @@ -656,9 +661,9 @@ static void close_channel(Channel *channel) decref(channel); } -static void on_stdio_close(Event e) +static void exit_event(void **argv) { - decref(e.data); + decref(argv[0]); if (!exiting) { mch_exit(0); @@ -680,17 +685,19 @@ static void free_channel(Channel *channel) pmap_free(cstr_t)(channel->subscribed_events); kv_destroy(channel->call_stack); kv_destroy(channel->delayed_notifications); + queue_free(channel->events); xfree(channel); } static void close_cb(Stream *stream, void *data) { - xfree(data); + decref(data); } static Channel *register_channel(ChannelType type) { Channel *rv = xmalloc(sizeof(Channel)); + rv->events = queue_new_child(loop.events); rv->type = type; rv->refcount = 1; rv->closed = false; diff --git a/src/nvim/msgpack_rpc/defs.h b/src/nvim/msgpack_rpc/defs.h index 0492a65290..d97cf28ca1 100644 --- a/src/nvim/msgpack_rpc/defs.h +++ b/src/nvim/msgpack_rpc/defs.h @@ -11,9 +11,8 @@ typedef struct { uint64_t request_id, Array args, Error *error); - bool defer; // Should the call be deferred to the main loop? This should - // be true if the function mutates editor data structures such - // as buffers, windows, tabs, or if it executes vimscript code. + bool async; // function is always safe to run immediately instead of being + // put in a request queue for handling when nvim waits for input. } MsgpackRpcRequestHandler; /// Initializes the msgpack-rpc method table diff --git a/src/nvim/msgpack_rpc/remote_ui.c b/src/nvim/msgpack_rpc/remote_ui.c index 07d78dd9d7..3334b0e6af 100644 --- a/src/nvim/msgpack_rpc/remote_ui.c +++ b/src/nvim/msgpack_rpc/remote_ui.c @@ -28,7 +28,7 @@ void remote_ui_init(void) connected_uis = pmap_new(uint64_t)(); // Add handler for "attach_ui" String method = cstr_as_string("ui_attach"); - MsgpackRpcRequestHandler handler = {.fn = remote_ui_attach, .defer = false}; + MsgpackRpcRequestHandler handler = {.fn = remote_ui_attach, .async = true}; msgpack_rpc_add_method_handler(method, handler); method = cstr_as_string("ui_detach"); handler.fn = remote_ui_detach; @@ -86,8 +86,7 @@ static Object remote_ui_attach(uint64_t channel_id, uint64_t request_id, ui->busy_stop = remote_ui_busy_stop; ui->mouse_on = remote_ui_mouse_on; ui->mouse_off = remote_ui_mouse_off; - ui->insert_mode = remote_ui_insert_mode; - ui->normal_mode = remote_ui_normal_mode; + ui->mode_change = remote_ui_mode_change; ui->set_scroll_region = remote_ui_set_scroll_region; ui->scroll = remote_ui_scroll; ui->highlight_set = remote_ui_highlight_set; @@ -214,16 +213,18 @@ static void remote_ui_mouse_off(UI *ui) push_call(ui, "mouse_off", args); } -static void remote_ui_insert_mode(UI *ui) +static void remote_ui_mode_change(UI *ui, int mode) { Array args = ARRAY_DICT_INIT; - push_call(ui, "insert_mode", args); -} - -static void remote_ui_normal_mode(UI *ui) -{ - Array args = ARRAY_DICT_INIT; - push_call(ui, "normal_mode", args); + if (mode == INSERT) { + ADD(args, STRING_OBJ(cstr_to_string("insert"))); + } else if (mode == REPLACE) { + ADD(args, STRING_OBJ(cstr_to_string("replace"))); + } else { + assert(mode == NORMAL); + ADD(args, STRING_OBJ(cstr_to_string("normal"))); + } + push_call(ui, "mode_change", args); } static void remote_ui_set_scroll_region(UI *ui, int top, int bot, int left, diff --git a/src/nvim/normal.c b/src/nvim/normal.c index 95e1c3d113..5b35af9209 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -63,6 +63,7 @@ #include "nvim/window.h" #include "nvim/event/loop.h" #include "nvim/os/time.h" +#include "nvim/os/input.h" /* * The Visual area is remembered for reselection. @@ -487,12 +488,12 @@ normal_cmd ( /* * Get the command character from the user. */ - loop_enable_deferred_events(&loop); + input_enable_events(); c = safe_vgetc(); - loop_disable_deferred_events(&loop); + input_disable_events(); if (c == K_EVENT) { - loop_process_event(&loop); + queue_process_events(loop.events); return; } diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 8a7ced138b..766b5720d9 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -5278,11 +5278,11 @@ static yankreg_T *adjust_clipboard_name(int *name, bool quiet) } yankreg_T *target; if (cb_flags & CB_UNNAMEDPLUS) { - *name = '+'; - target = &y_regs[STAR_REGISTER]; + *name = cb_flags & CB_UNNAMED ? '"': '+'; + target = &y_regs[PLUS_REGISTER]; } else { *name = '*'; - target = &y_regs[PLUS_REGISTER]; + target = &y_regs[STAR_REGISTER]; } return target; // unnamed register } diff --git a/src/nvim/os/fs.c b/src/nvim/os/fs.c index 5eeb275701..785c79127f 100644 --- a/src/nvim/os/fs.c +++ b/src/nvim/os/fs.c @@ -270,13 +270,16 @@ bool os_file_exists(const char_u *name) return os_stat((char *)name, &statbuf); } -/// Check if a file is readonly. +/// Check if a file is readable. /// -/// @return `true` if `name` is readonly. -bool os_file_is_readonly(const char *name) - FUNC_ATTR_NONNULL_ALL +/// @return true if `name` is readable, otherwise false. +bool os_file_is_readable(const char *name) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { - return access(name, W_OK) != 0; + uv_fs_t req; + int r = uv_fs_access(&fs_loop, &req, name, R_OK, NULL); + uv_fs_req_cleanup(&req); + return (r == 0); } /// Check if a file is writable. @@ -285,13 +288,13 @@ bool os_file_is_readonly(const char *name) /// @return `1` if `name` is writable, /// @return `2` for a directory which we have rights to write into. int os_file_is_writable(const char *name) - FUNC_ATTR_NONNULL_ALL + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { - if (access(name, W_OK) == 0) { - if (os_isdir((char_u *)name)) { - return 2; - } - return 1; + uv_fs_t req; + int r = uv_fs_access(&fs_loop, &req, name, W_OK, NULL); + uv_fs_req_cleanup(&req); + if (r == 0) { + return os_isdir((char_u *)name) ? 2 : 1; } return 0; } diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index b0e0f57e60..09f162f79d 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -33,6 +33,7 @@ static Stream read_stream = {.closed = true}; static RBuffer *input_buffer = NULL; static bool input_eof = false; static int global_fd = 0; +static int events_enabled = 0; #ifdef INCLUDE_GENERATED_DECLARATIONS # include "os/input.c.generated.h" @@ -110,8 +111,8 @@ int os_inchar(uint8_t *buf, int maxlen, int ms, int tb_change_cnt) return (int)rbuffer_read(input_buffer, (char *)buf, (size_t)maxlen); } - // If there are deferred events, return the keys directly - if (loop_has_deferred_events(&loop)) { + // If there are events, return the keys directly + if (pending_events()) { return push_event_key(buf, maxlen); } @@ -131,11 +132,21 @@ bool os_char_avail(void) // Check for CTRL-C typed by reading all available characters. void os_breakcheck(void) { - if (!disable_breakcheck && !got_int) { + if (!got_int) { loop_poll_events(&loop, 0); } } +void input_enable_events(void) +{ + events_enabled++; +} + +void input_disable_events(void) +{ + events_enabled--; +} + /// Test whether a file descriptor refers to a terminal. /// /// @param fd File descriptor. @@ -281,7 +292,7 @@ static bool input_poll(int ms) prof_inchar_enter(); } - LOOP_POLL_EVENTS_UNTIL(&loop, ms, input_ready() || input_eof); + LOOP_PROCESS_EVENTS_UNTIL(&loop, NULL, ms, input_ready() || input_eof); if (do_profiling == PROF_YES && ms) { prof_inchar_exit(); @@ -305,7 +316,8 @@ static InbufPollResult inbuf_poll(int ms) return input_eof ? kInputEof : kInputNone; } -static void read_cb(Stream *stream, RBuffer *buf, void *data, bool at_eof) +static void read_cb(Stream *stream, RBuffer *buf, size_t c, void *data, + bool at_eof) { if (at_eof) { input_eof = true; @@ -358,7 +370,7 @@ static bool input_ready(void) { return typebuf_was_filled || // API call filled typeahead rbuffer_size(input_buffer) || // Input buffer filled - loop_has_deferred_events(&loop); // Events must be processed + pending_events(); // Events must be processed } // Exit because of an input read error. @@ -369,3 +381,8 @@ static void read_error_exit(void) STRCPY(IObuff, _("Vim: Error reading input, exiting...\n")); preserve_exit(); } + +static bool pending_events(void) +{ + return events_enabled && !queue_empty(loop.events); +} diff --git a/src/nvim/os/shell.c b/src/nvim/os/shell.c index e0d67d4951..2d97c4bf4f 100644 --- a/src/nvim/os/shell.c +++ b/src/nvim/os/shell.c @@ -205,13 +205,15 @@ static int do_os_system(char **argv, xstrlcpy(prog, argv[0], MAXPATHL); Stream in, out, err; - UvProcess uvproc = uv_process_init(&buf); + UvProcess uvproc = uv_process_init(&loop, &buf); Process *proc = &uvproc.process; + Queue *events = queue_new_child(loop.events); + proc->events = events; proc->argv = argv; proc->in = input != NULL ? &in : NULL; proc->out = &out; proc->err = &err; - if (!process_spawn(&loop, proc)) { + if (!process_spawn(proc)) { loop_poll_events(&loop, 0); // Failed, probably due to `sh` not being executable if (!silent) { @@ -219,14 +221,22 @@ static int do_os_system(char **argv, msg_outtrans((char_u *)prog); msg_putchar('\n'); } + queue_free(events); return -1; } + // We want to deal with stream events as fast a possible while queueing + // process events, so reset everything to NULL. It prevents closing the + // streams while there's still data in the OS buffer(due to the process + // exiting before all data is read). if (input != NULL) { + proc->in->events = NULL; wstream_init(proc->in, 0); } + proc->out->events = NULL; rstream_init(proc->out, 0); rstream_start(proc->out, data_cb); + proc->err->events = NULL; rstream_init(proc->err, 0); rstream_start(proc->err, data_cb); @@ -247,7 +257,7 @@ static int do_os_system(char **argv, // the UI ui_busy_start(); ui_flush(); - int status = process_wait(proc, -1); + int status = process_wait(proc, -1, NULL); ui_busy_stop(); // prepare the out parameters if requested @@ -267,6 +277,9 @@ static int do_os_system(char **argv, } } + assert(queue_empty(events)); + queue_free(events); + return status; } @@ -285,7 +298,8 @@ static void dynamic_buffer_ensure(DynamicBuffer *buf, size_t desired) buf->data = xrealloc(buf->data, buf->cap); } -static void system_data_cb(Stream *stream, RBuffer *buf, void *data, bool eof) +static void system_data_cb(Stream *stream, RBuffer *buf, size_t count, + void *data, bool eof) { DynamicBuffer *dbuf = data; @@ -295,16 +309,25 @@ static void system_data_cb(Stream *stream, RBuffer *buf, void *data, bool eof) dbuf->len += nread; } -static void out_data_cb(Stream *stream, RBuffer *buf, void *data, bool eof) +static void out_data_cb(Stream *stream, RBuffer *buf, size_t count, void *data, + bool eof) { - RBUFFER_UNTIL_EMPTY(buf, ptr, len) { - size_t written = write_output(ptr, len, false, - eof && len <= rbuffer_size(buf)); - if (written) { - rbuffer_consumed(buf, written); - } else { - break; - } + size_t cnt; + char *ptr = rbuffer_read_ptr(buf, &cnt); + + if (!cnt) { + return; + } + + size_t written = write_output(ptr, cnt, false, eof); + // No output written, force emptying the Rbuffer if it is full. + if (!written && rbuffer_size(buf) == rbuffer_capacity(buf)) { + screen_del_lines(0, 0, 1, (int)Rows, NULL); + screen_puts_len((char_u *)ptr, (int)cnt, (int)Rows - 1, 0, 0); + written = cnt; + } + if (written) { + rbuffer_consumed(buf, written); } } @@ -421,6 +444,7 @@ static size_t write_output(char *output, size_t remaining, bool to_buffer, if (!output) { return 0; } + char replacement_NUL = to_buffer ? NL : 1; char *start = output; size_t off = 0; @@ -428,9 +452,10 @@ static size_t write_output(char *output, size_t remaining, bool to_buffer, while (off < remaining) { if (output[off] == NL) { // Insert the line - output[off] = NUL; if (to_buffer) { - ml_append(curwin->w_cursor.lnum++, (char_u *)output, 0, false); + output[off] = NUL; + ml_append(curwin->w_cursor.lnum++, (char_u *)output, (int)off + 1, + false); } else { screen_del_lines(0, 0, 1, (int)Rows, NULL); screen_puts_len((char_u *)output, (int)off, lastrow, 0, 0); @@ -444,7 +469,7 @@ static size_t write_output(char *output, size_t remaining, bool to_buffer, if (output[off] == NUL) { // Translate NUL to NL - output[off] = NL; + output[off] = replacement_NUL; } off++; } diff --git a/src/nvim/os/signal.c b/src/nvim/os/signal.c index 6de3435c4c..7158721433 100644 --- a/src/nvim/os/signal.c +++ b/src/nvim/os/signal.c @@ -115,16 +115,6 @@ static void deadly_signal(int signum) static void on_signal(SignalWatcher *handle, int signum, void *data) { assert(signum >= 0); - loop_push_event(&loop, (Event) { - .handler = on_signal_event, - .data = (void *)(uintptr_t)signum - }, false); -} - -static void on_signal_event(Event event) -{ - int signum = (int)(uintptr_t)event.data; - switch (signum) { #ifdef SIGPWR case SIGPWR: @@ -148,4 +138,3 @@ static void on_signal_event(Event event) break; } } - diff --git a/src/nvim/os/time.c b/src/nvim/os/time.c index 6b5d4359db..ee17938afc 100644 --- a/src/nvim/os/time.c +++ b/src/nvim/os/time.c @@ -43,7 +43,7 @@ void os_delay(uint64_t milliseconds, bool ignoreinput) if (milliseconds > INT_MAX) { milliseconds = INT_MAX; } - LOOP_POLL_EVENTS_UNTIL(&loop, (int)milliseconds, got_int); + LOOP_PROCESS_EVENTS_UNTIL(&loop, NULL, (int)milliseconds, got_int); } else { os_microdelay(milliseconds * 1000); } diff --git a/src/nvim/os/users.c b/src/nvim/os/users.c index a57ba41af1..637a86c74f 100644 --- a/src/nvim/os/users.c +++ b/src/nvim/os/users.c @@ -41,7 +41,12 @@ int os_get_usernames(garray_T *users) // Return OK if a name found. int os_get_user_name(char *s, size_t len) { +#ifdef UNIX return os_get_uname(getuid(), s, len); +#else + // TODO(equalsraf): Windows GetUserName() + return os_get_uname(0, s, len); +#endif } // Insert user name for "uid" in s[len]. diff --git a/src/nvim/os/win_defs.h b/src/nvim/os/win_defs.h index 19d796bd08..a51898c9e7 100644 --- a/src/nvim/os/win_defs.h +++ b/src/nvim/os/win_defs.h @@ -21,4 +21,12 @@ // - SYS_VIMRC_FILE // - SPECIAL_WILDCHAR +// _access(): https://msdn.microsoft.com/en-us/library/1w06ktdy.aspx +#ifndef R_OK +# define R_OK 4 +#endif +#ifndef W_OK +# define W_OK 2 +#endif + #endif // NVIM_OS_WIN_DEFS_H diff --git a/src/nvim/path.c b/src/nvim/path.c index 152154e5f4..72980fcd0e 100644 --- a/src/nvim/path.c +++ b/src/nvim/path.c @@ -481,6 +481,21 @@ static size_t path_expand(garray_T *gap, const char_u *path, int flags) return do_path_expand(gap, path, 0, flags, false); } +static const char *scandir_next_with_dots(Directory *dir) +{ + static int count = 0; + if (dir == NULL) { // initialize + count = 0; + return NULL; + } + + count += 1; + if (count == 1 || count == 2) { + return (count == 1) ? "." : ".."; + } + return os_scandir_next(dir); +} + /// Implementation of path_expand(). /// /// Chars before `path + wildoff` do not get expanded. @@ -597,11 +612,12 @@ static size_t do_path_expand(garray_T *gap, const char_u *path, *s = NUL; Directory dir; - // open the directory for scanning - if (os_scandir(&dir, *buf == NUL ? "." : (char *)buf)) { + if (os_scandir(&dir, *buf == NUL ? "." : (char *)buf) + || os_isdir(*buf == NUL ? (char_u *)"." : (char_u *)buf)) { // Find all matching entries. char_u *name; - while((name = (char_u *) os_scandir_next(&dir))) { + scandir_next_with_dots(NULL /* initialize */); + while((name = (char_u *) scandir_next_with_dots(&dir)) && name != NULL) { if ((name[0] != '.' || starts_with_dot) && ((regmatch.regprog != NULL && vim_regexec(®match, name, 0)) || ((flags & EW_NOTWILD) diff --git a/src/nvim/regexp.c b/src/nvim/regexp.c index 4724a07895..e2c4b590d0 100644 --- a/src/nvim/regexp.c +++ b/src/nvim/regexp.c @@ -961,10 +961,10 @@ static void reg_equi_class(int c) REGMBC(0x107) REGMBC(0x109) REGMBC(0x10b) REGMBC(0x10d) return; - case 'd': CASEMBC(0x10f) CASEMBC(0x111) CASEMBC(0x1d0b) - CASEMBC(0x1e11) + case 'd': CASEMBC(0x10f) CASEMBC(0x111) CASEMBC(0x1e0b) + CASEMBC(0x1e0f) CASEMBC(0x1e11) regmbc('d'); REGMBC(0x10f) REGMBC(0x111) - REGMBC(0x1e0b) REGMBC(0x01e0f) REGMBC(0x1e11) + REGMBC(0x1e0b) REGMBC(0x1e0f) REGMBC(0x1e11) return; case 'e': case '\350': case '\351': case '\352': case '\353': CASEMBC(0x113) CASEMBC(0x115) CASEMBC(0x117) CASEMBC(0x119) diff --git a/src/nvim/regexp_nfa.c b/src/nvim/regexp_nfa.c index 42cebb0198..484dfe1e1f 100644 --- a/src/nvim/regexp_nfa.c +++ b/src/nvim/regexp_nfa.c @@ -270,8 +270,10 @@ typedef struct { /* When REG_MULTI is TRUE list.multi is used, otherwise list.line. */ union { struct multipos { - lpos_T start; - lpos_T end; + linenr_T start_lnum; + linenr_T end_lnum; + colnr_T start_col; + colnr_T end_col; } multi[NSUBEXP]; struct linepos { char_u *start; @@ -716,8 +718,8 @@ static int nfa_recognize_char_class(char_u *start, char_u *end, int extra_newl) */ static void nfa_emit_equi_class(int c) { -#define EMIT2(c) EMIT(c); EMIT(NFA_CONCAT); -# define EMITMBC(c) EMIT(c); EMIT(NFA_CONCAT); +#define EMIT2(c) EMIT(c); EMIT(NFA_CONCAT); +#define EMITMBC(c) EMIT(c); EMIT(NFA_CONCAT); if (enc_utf8 || STRCMP(p_enc, "latin1") == 0 || STRCMP(p_enc, "iso-8859-15") == 0) { @@ -906,10 +908,10 @@ static void nfa_emit_equi_class(int c) EMITMBC(0x10b) EMITMBC(0x10d) return; - case 'd': CASEMBC(0x10f) CASEMBC(0x111) CASEMBC(0x1d0b) - CASEMBC(0x1e11) + case 'd': CASEMBC(0x10f) CASEMBC(0x111) CASEMBC(0x1e0b) + CASEMBC(0x1e0f) CASEMBC(0x1e11) EMIT2('d'); EMITMBC(0x10f) EMITMBC(0x111) EMITMBC(0x1e0b) - EMITMBC(0x01e0f) EMITMBC(0x1e11) + EMITMBC(0x1e0f) EMITMBC(0x1e11) return; case 'e': case 0350: case 0351: case 0352: case 0353: @@ -1391,7 +1393,7 @@ static int nfa_regatom(void) * matched an unlimited number of times. NFA_NOPEN is * added only once at a position, while NFA_SPLIT is * added multiple times. This is more efficient than - * not allowsing NFA_SPLIT multiple times, it is used + * not allowing NFA_SPLIT multiple times, it is used * a lot. */ EMIT(NFA_NOPEN); break; @@ -3431,10 +3433,10 @@ static void log_subexpr(regsub_T *sub) if (REG_MULTI) fprintf(log_fd, "*** group %d, start: c=%d, l=%d, end: c=%d, l=%d\n", j, - sub->list.multi[j].start.col, - (int)sub->list.multi[j].start.lnum, - sub->list.multi[j].end.col, - (int)sub->list.multi[j].end.lnum); + sub->list.multi[j].start_col, + (int)sub->list.multi[j].start_lnum, + sub->list.multi[j].end_col, + (int)sub->list.multi[j].end_lnum); else { char *s = (char *)sub->list.line[j].start; char *e = (char *)sub->list.line[j].end; @@ -3535,8 +3537,10 @@ static void copy_ze_off(regsub_T *to, regsub_T *from) { if (nfa_has_zend) { if (REG_MULTI) { - if (from->list.multi[0].end.lnum >= 0) - to->list.multi[0].end = from->list.multi[0].end; + if (from->list.multi[0].end_lnum >= 0){ + to->list.multi[0].end_lnum = from->list.multi[0].end_lnum; + to->list.multi[0].end_col = from->list.multi[0].end_col; + } } else { if (from->list.line[0].end != NULL) to->list.line[0].end = from->list.line[0].end; @@ -3559,27 +3563,27 @@ static int sub_equal(regsub_T *sub1, regsub_T *sub2) if (REG_MULTI) { for (i = 0; i < todo; ++i) { if (i < sub1->in_use) - s1 = sub1->list.multi[i].start.lnum; + s1 = sub1->list.multi[i].start_lnum; else s1 = -1; if (i < sub2->in_use) - s2 = sub2->list.multi[i].start.lnum; + s2 = sub2->list.multi[i].start_lnum; else s2 = -1; if (s1 != s2) return FALSE; - if (s1 != -1 && sub1->list.multi[i].start.col - != sub2->list.multi[i].start.col) + if (s1 != -1 && sub1->list.multi[i].start_col + != sub2->list.multi[i].start_col) return FALSE; if (nfa_has_backref) { if (i < sub1->in_use) { - s1 = sub1->list.multi[i].end.lnum; + s1 = sub1->list.multi[i].end_lnum; } else { s1 = -1; } if (i < sub2->in_use) { - s2 = sub2->list.multi[i].end.lnum; + s2 = sub2->list.multi[i].end_lnum; } else { s2 = -1; } @@ -3587,7 +3591,7 @@ static int sub_equal(regsub_T *sub1, regsub_T *sub2) return FALSE; } if (s1 != -1 - && sub1->list.multi[i].end.col != sub2->list.multi[i].end.col) { + && sub1->list.multi[i].end_col != sub2->list.multi[i].end_col) { return FALSE; } } @@ -3637,7 +3641,7 @@ static void report_state(char *action, if (sub->in_use <= 0) col = -1; else if (REG_MULTI) - col = sub->list.multi[0].start.col; + col = sub->list.multi[0].start_col; else col = (int)(sub->list.line[0].start - regline); nfa_set_code(state->c); @@ -4023,22 +4027,24 @@ skip_add: * and restore it when it was in use. Otherwise fill any gap. */ if (REG_MULTI) { if (subidx < sub->in_use) { - save_lpos = sub->list.multi[subidx].start; + save_lpos.lnum = sub->list.multi[subidx].start_lnum; + save_lpos.col = sub->list.multi[subidx].start_col; save_in_use = -1; } else { save_in_use = sub->in_use; for (i = sub->in_use; i < subidx; ++i) { - sub->list.multi[i].start.lnum = -1; - sub->list.multi[i].end.lnum = -1; + sub->list.multi[i].start_lnum = -1; + sub->list.multi[i].end_lnum = -1; } sub->in_use = subidx + 1; } if (off == -1) { - sub->list.multi[subidx].start.lnum = reglnum + 1; - sub->list.multi[subidx].start.col = 0; + sub->list.multi[subidx].start_lnum = reglnum + 1; + sub->list.multi[subidx].start_col = 0; } else { - sub->list.multi[subidx].start.lnum = reglnum; - sub->list.multi[subidx].start.col = + + sub->list.multi[subidx].start_lnum = reglnum; + sub->list.multi[subidx].start_col = (colnr_T)(reginput - regline + off); } } else { @@ -4064,8 +4070,10 @@ skip_add: sub = &subs->norm; if (save_in_use == -1) { - if (REG_MULTI) - sub->list.multi[subidx].start = save_lpos; + if (REG_MULTI){ + sub->list.multi[subidx].start_lnum = save_lpos.lnum; + sub->list.multi[subidx].start_col = save_lpos.col; + } else sub->list.line[subidx].start = save_ptr; } else @@ -4074,7 +4082,7 @@ skip_add: case NFA_MCLOSE: if (nfa_has_zend && (REG_MULTI - ? subs->norm.list.multi[0].end.lnum >= 0 + ? subs->norm.list.multi[0].end_lnum >= 0 : subs->norm.list.line[0].end != NULL)) { /* Do not overwrite the position set by \ze. */ subs = addstate(l, state->out, subs, pim, off); @@ -4117,13 +4125,14 @@ skip_add: if (sub->in_use <= subidx) sub->in_use = subidx + 1; if (REG_MULTI) { - save_lpos = sub->list.multi[subidx].end; + save_lpos.lnum = sub->list.multi[subidx].end_lnum; + save_lpos.col = sub->list.multi[subidx].end_col; if (off == -1) { - sub->list.multi[subidx].end.lnum = reglnum + 1; - sub->list.multi[subidx].end.col = 0; + sub->list.multi[subidx].end_lnum = reglnum + 1; + sub->list.multi[subidx].end_col = 0; } else { - sub->list.multi[subidx].end.lnum = reglnum; - sub->list.multi[subidx].end.col = + sub->list.multi[subidx].end_lnum = reglnum; + sub->list.multi[subidx].end_col = (colnr_T)(reginput - regline + off); } /* avoid compiler warnings */ @@ -4143,8 +4152,10 @@ skip_add: else sub = &subs->norm; - if (REG_MULTI) - sub->list.multi[subidx].end = save_lpos; + if (REG_MULTI){ + sub->list.multi[subidx].end_lnum = save_lpos.lnum; + sub->list.multi[subidx].end_col = save_lpos.col; + } else sub->list.line[subidx].end = save_ptr; sub->in_use = save_in_use; @@ -4319,24 +4330,24 @@ retempty: } if (REG_MULTI) { - if (sub->list.multi[subidx].start.lnum < 0 - || sub->list.multi[subidx].end.lnum < 0) + if (sub->list.multi[subidx].start_lnum < 0 + || sub->list.multi[subidx].end_lnum < 0) goto retempty; - if (sub->list.multi[subidx].start.lnum == reglnum - && sub->list.multi[subidx].end.lnum == reglnum) { - len = sub->list.multi[subidx].end.col - - sub->list.multi[subidx].start.col; - if (cstrncmp(regline + sub->list.multi[subidx].start.col, + if (sub->list.multi[subidx].start_lnum == reglnum + && sub->list.multi[subidx].end_lnum == reglnum) { + len = sub->list.multi[subidx].end_col + - sub->list.multi[subidx].start_col; + if (cstrncmp(regline + sub->list.multi[subidx].start_col, reginput, &len) == 0) { *bytelen = len; return TRUE; } } else { if (match_with_backref( - sub->list.multi[subidx].start.lnum, - sub->list.multi[subidx].start.col, - sub->list.multi[subidx].end.lnum, - sub->list.multi[subidx].end.col, + sub->list.multi[subidx].start_lnum, + sub->list.multi[subidx].start_col, + sub->list.multi[subidx].end_lnum, + sub->list.multi[subidx].end_col, bytelen) == RA_MATCH) return TRUE; } @@ -4873,8 +4884,8 @@ static int nfa_regmatch(nfa_regprog_T *prog, nfa_state_T *start, regsubs_T *subm * it's the first MOPEN. */ if (toplevel) { if (REG_MULTI) { - m->norm.list.multi[0].start.lnum = reglnum; - m->norm.list.multi[0].start.col = (colnr_T)(reginput - regline); + m->norm.list.multi[0].start_lnum = reglnum; + m->norm.list.multi[0].start_col = (colnr_T)(reginput - regline); } else m->norm.list.line[0].start = reginput; m->norm.in_use = 1; @@ -4962,7 +4973,7 @@ static int nfa_regmatch(nfa_regprog_T *prog, nfa_state_T *start, regsubs_T *subm if (t->subs.norm.in_use <= 0) col = -1; else if (REG_MULTI) - col = t->subs.norm.list.multi[0].start.col; + col = t->subs.norm.list.multi[0].start_col; else col = (int)(t->subs.norm.list.line[0].start - regline); nfa_set_code(t->state->c); @@ -5214,7 +5225,7 @@ static int nfa_regmatch(nfa_regprog_T *prog, nfa_state_T *start, regsubs_T *subm * continue with what follows. */ if (REG_MULTI) /* TODO: multi-line match */ - bytelen = m->norm.list.multi[0].end.col + bytelen = m->norm.list.multi[0].end_col - (int)(reginput - regline); else bytelen = (int)(m->norm.list.line[0].end - reginput); @@ -6018,7 +6029,7 @@ static int nfa_regmatch(nfa_regprog_T *prog, nfa_state_T *start, regsubs_T *subm if (add) { if (REG_MULTI) - m->norm.list.multi[0].start.col = + m->norm.list.multi[0].start_col = (colnr_T)(reginput - regline) + clen; else m->norm.list.line[0].start = reginput + clen; @@ -6123,8 +6134,11 @@ static long nfa_regtry(nfa_regprog_T *prog, colnr_T col) cleanup_subexpr(); if (REG_MULTI) { for (i = 0; i < subs.norm.in_use; i++) { - reg_startpos[i] = subs.norm.list.multi[i].start; - reg_endpos[i] = subs.norm.list.multi[i].end; + reg_startpos[i].lnum = subs.norm.list.multi[i].start_lnum; + reg_startpos[i].col = subs.norm.list.multi[i].start_col; + + reg_endpos[i].lnum = subs.norm.list.multi[i].end_lnum; + reg_endpos[i].col = subs.norm.list.multi[i].end_col; } if (reg_startpos[0].lnum < 0) { @@ -6162,12 +6176,12 @@ static long nfa_regtry(nfa_regprog_T *prog, colnr_T col) struct multipos *mpos = &subs.synt.list.multi[i]; // Only accept single line matches that are valid. - if (mpos->start.lnum >= 0 - && mpos->start.lnum == mpos->end.lnum - && mpos->end.col >= mpos->start.col) { + if (mpos->start_lnum >= 0 + && mpos->start_lnum == mpos->end_lnum + && mpos->end_col >= mpos->start_col) { re_extmatch_out->matches[i] = - vim_strnsave(reg_getline(mpos->start.lnum) + mpos->start.col, - mpos->end.col - mpos->start.col); + vim_strnsave(reg_getline(mpos->start_lnum) + mpos->start_col, + mpos->end_col - mpos->start_col); } } else { struct linepos *lpos = &subs.synt.list.line[i]; diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 47fef692db..b9bc4c6d78 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -69,6 +69,7 @@ #include "nvim/fileio.h" #include "nvim/event/loop.h" #include "nvim/event/time.h" +#include "nvim/os/input.h" #include "nvim/api/private/helpers.h" #ifdef INCLUDE_GENERATED_DECLARATIONS @@ -324,7 +325,7 @@ void terminal_resize(Terminal *term, uint16_t width, uint16_t height) invalidate_terminal(term, -1, -1); } -void terminal_enter(bool process_deferred) +void terminal_enter(void) { Terminal *term = curbuf->terminal; assert(term && "should only be called when curbuf has a terminal"); @@ -353,15 +354,9 @@ void terminal_enter(bool process_deferred) bool got_bs = false; // True if the last input was <C-\> while (term->buf == curbuf) { - if (process_deferred) { - loop_enable_deferred_events(&loop); - } - + input_enable_events(); c = safe_vgetc(); - - if (process_deferred) { - loop_disable_deferred_events(&loop); - } + input_disable_events(); switch (c) { case K_LEFTMOUSE: @@ -381,7 +376,7 @@ void terminal_enter(bool process_deferred) break; case K_EVENT: - loop_process_event(&loop); + queue_process_events(loop.events); break; case Ctrl_N: @@ -427,7 +422,13 @@ void terminal_destroy(Terminal *term) term->buf->terminal = NULL; } term->buf = NULL; - pmap_del(ptr_t)(invalidated_terminals, term); + if (pmap_has(ptr_t)(invalidated_terminals, term)) { + // flush any pending changes to the buffer + block_autocmds(); + refresh_terminal(term); + unblock_autocmds(); + pmap_del(ptr_t)(invalidated_terminals, term); + } for (size_t i = 0 ; i < term->sb_current; i++) { xfree(term->sb_buffer[i]); } @@ -883,48 +884,47 @@ static void invalidate_terminal(Terminal *term, int start_row, int end_row) } } +static void refresh_terminal(Terminal *term) +{ + // TODO(SplinterOfChaos): Find the condition that makes term->buf invalid. + bool valid = true; + if (!term->buf || !(valid = buf_valid(term->buf))) { + // destroyed by `close_buffer`. Dont do anything else + if (!valid) { + term->buf = NULL; + } + return; + } + bool pending_resize = term->pending_resize; + WITH_BUFFER(term->buf, { + refresh_size(term); + refresh_scrollback(term); + refresh_screen(term); + redraw_buf_later(term->buf, NOT_VALID); + }); + adjust_topline(term, pending_resize); +} // libuv timer callback. This will enqueue on_refresh to be processed as an // event. static void refresh_timer_cb(TimeWatcher *watcher, void *data) { - loop_push_event(&loop, (Event) {.handler = on_refresh}, false); - refresh_pending = false; -} - -// Refresh all invalidated terminals -static void on_refresh(Event event) -{ if (exiting) { // bad things can happen if we redraw when exiting, and there's no need to // update the buffer. - return; + goto end; } Terminal *term; void *stub; (void)(stub); // don't process autocommands while updating terminal buffers block_autocmds(); map_foreach(invalidated_terminals, term, stub, { - // TODO(SplinterOfChaos): Find the condition that makes term->buf invalid. - bool valid = true; - if (!term->buf || !(valid = buf_valid(term->buf))) { - // destroyed by `close_buffer`. Dont do anything else - if (!valid) { - term->buf = NULL; - } - continue; - } - bool pending_resize = term->pending_resize; - WITH_BUFFER(term->buf, { - refresh_size(term); - refresh_scrollback(term); - refresh_screen(term); - redraw_buf_later(term->buf, NOT_VALID); - }); - adjust_topline(term, pending_resize); + refresh_terminal(term); }); pmap_clear(ptr_t)(invalidated_terminals); unblock_autocmds(); redraw(true); +end: + refresh_pending = false; } static void refresh_size(Terminal *term) diff --git a/src/nvim/tui/term_input.inl b/src/nvim/tui/term_input.inl index 0a84a3688b..c396557160 100644 --- a/src/nvim/tui/term_input.inl +++ b/src/nvim/tui/term_input.inl @@ -206,9 +206,10 @@ static bool handle_forced_escape(TermInput *input) return false; } -static void restart_reading(Event event); +static void restart_reading(void **argv); -static void read_cb(Stream *stream, RBuffer *buf, void *data, bool eof) +static void read_cb(Stream *stream, RBuffer *buf, size_t c, void *data, + bool eof) { TermInput *input = data; @@ -226,8 +227,7 @@ static void read_cb(Stream *stream, RBuffer *buf, void *data, bool eof) // ls *.md | xargs nvim input->in_fd = 2; stream_close(&input->read_stream, NULL); - loop_push_event(&loop, - (Event) { .data = input, .handler = restart_reading }, false); + queue_put(loop.fast_events, restart_reading, 1, input); } else { input_done(); } @@ -272,9 +272,9 @@ static void read_cb(Stream *stream, RBuffer *buf, void *data, bool eof) rbuffer_reset(input->read_stream.buffer); } -static void restart_reading(Event event) +static void restart_reading(void **argv) { - TermInput *input = event.data; + TermInput *input = argv[0]; rstream_init_fd(&loop, &input->read_stream, input->in_fd, 0xfff, input); rstream_start(&input->read_stream, read_cb); } diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index a1f56d2695..57a2b896f7 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -57,14 +57,17 @@ typedef struct { bool busy; HlAttrs attrs, print_attrs; Cell **screen; + int showing_mode; struct { int enable_mouse, disable_mouse; int enable_bracketed_paste, disable_bracketed_paste; - int enter_insert_mode, exit_insert_mode; + int enter_insert_mode, enter_replace_mode, exit_insert_mode; int set_rgb_foreground, set_rgb_background; } unibi_ext; } TUIData; +static bool volatile got_winch = false; + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "tui/tui.c.generated.h" #endif @@ -98,11 +101,13 @@ UI *tui_start(void) data->can_use_terminal_scroll = true; data->bufpos = 0; data->bufsize = sizeof(data->buf) - CNORM_COMMAND_MAX_SIZE; + data->showing_mode = 0; data->unibi_ext.enable_mouse = -1; data->unibi_ext.disable_mouse = -1; data->unibi_ext.enable_bracketed_paste = -1; data->unibi_ext.disable_bracketed_paste = -1; data->unibi_ext.enter_insert_mode = -1; + data->unibi_ext.enter_replace_mode = -1; data->unibi_ext.exit_insert_mode = -1; // write output to stderr if stdout is not a tty @@ -148,8 +153,7 @@ UI *tui_start(void) ui->busy_stop = tui_busy_stop; ui->mouse_on = tui_mouse_on; ui->mouse_off = tui_mouse_off; - ui->insert_mode = tui_insert_mode; - ui->normal_mode = tui_normal_mode; + ui->mode_change = tui_mode_change; ui->set_scroll_region = tui_set_scroll_region; ui->scroll = tui_scroll; ui->highlight_set = tui_highlight_set; @@ -178,7 +182,7 @@ static void tui_stop(UI *ui) // Destroy input stuff term_input_destroy(data->input); // Destroy output stuff - tui_normal_mode(ui); + tui_mode_change(ui, NORMAL); tui_mouse_off(ui); unibi_out(ui, unibi_exit_attribute_mode); // cursor should be set to normal before exiting alternate screen @@ -201,23 +205,14 @@ static void tui_stop(UI *ui) xfree(ui); } -static void try_resize(Event ev) +static void sigwinch_cb(SignalWatcher *watcher, int signum, void *data) { - UI *ui = ev.data; + got_winch = true; + UI *ui = data; update_size(ui); ui_refresh(); } -static void sigwinch_cb(SignalWatcher *watcher, int signum, void *data) -{ - // Queue the event because resizing can result in recursive event_poll calls - // FIXME(blueyed): TUI does not resize properly when not deferred. Why? #2322 - loop_push_event(&loop, (Event) { - .data = data, - .handler = try_resize - }, true); -} - static bool attrs_differ(HlAttrs a1, HlAttrs a2) { return a1.foreground != a2.foreground || a1.background != a2.background @@ -352,10 +347,13 @@ static void tui_resize(UI *ui, int width, int height) data->scroll_region.right = width - 1; data->row = data->col = 0; - // try to resize the terminal window - char r[16]; // enough for 9999x9999 - snprintf(r, sizeof(r), "\x1b[8;%d;%dt", height, width); - out(ui, r, strlen(r)); + if (!got_winch) { // Try to resize the terminal window. + char r[16]; // enough for 9999x9999 + snprintf(r, sizeof(r), "\x1b[8;%d;%dt", height, width); + out(ui, r, strlen(r)); + } else { // Already handled the SIGWINCH signal; avoid double-resize. + got_winch = false; + } } static void tui_clear(UI *ui) @@ -404,16 +402,25 @@ static void tui_mouse_off(UI *ui) data->mouse_enabled = false; } -static void tui_insert_mode(UI *ui) +static void tui_mode_change(UI *ui, int mode) { TUIData *data = ui->data; - unibi_out(ui, data->unibi_ext.enter_insert_mode); -} -static void tui_normal_mode(UI *ui) -{ - TUIData *data = ui->data; - unibi_out(ui, data->unibi_ext.exit_insert_mode); + if (mode == INSERT) { + if (data->showing_mode != INSERT) { + unibi_out(ui, data->unibi_ext.enter_insert_mode); + } + } else if (mode == REPLACE) { + if (data->showing_mode != REPLACE) { + unibi_out(ui, data->unibi_ext.enter_replace_mode); + } + } else { + assert(mode == NORMAL); + if (data->showing_mode != NORMAL) { + unibi_out(ui, data->unibi_ext.exit_insert_mode); + } + } + data->showing_mode = mode; } static void tui_set_scroll_region(UI *ui, int top, int bot, int left, @@ -798,18 +805,25 @@ static void fix_terminfo(TUIData *data) #define TMUX_WRAP(seq) (inside_tmux ? "\x1bPtmux;\x1b" seq "\x1b\\" : seq) // Support changing cursor shape on some popular terminals. const char *term_prog = os_getenv("TERM_PROGRAM"); + const char *vte_version = os_getenv("VTE_VERSION"); - if ((term_prog && !strcmp(term_prog, "iTerm.app")) - || os_getenv("ITERM_SESSION_ID") != NULL) { - // iterm + if ((term_prog && !strcmp(term_prog, "Konsole")) + || os_getenv("KONSOLE_DBUS_SESSION") != NULL) { + // Konsole uses a proprietary escape code to set the cursor shape + // and does not suppport DECSCUSR. data->unibi_ext.enter_insert_mode = (int)unibi_add_ext_str(ut, NULL, TMUX_WRAP("\x1b]50;CursorShape=1;BlinkingCursorEnabled=1\x07")); + data->unibi_ext.enter_replace_mode = (int)unibi_add_ext_str(ut, NULL, + TMUX_WRAP("\x1b]50;CursorShape=2;BlinkingCursorEnabled=1\x07")); data->unibi_ext.exit_insert_mode = (int)unibi_add_ext_str(ut, NULL, TMUX_WRAP("\x1b]50;CursorShape=0;BlinkingCursorEnabled=0\x07")); - } else { - // xterm-like sequences for blinking bar and solid block + } else if (!vte_version || atoi(vte_version) >= 3900) { + // Assume that the terminal supports DECSCUSR unless it is an + // old VTE based terminal data->unibi_ext.enter_insert_mode = (int)unibi_add_ext_str(ut, NULL, TMUX_WRAP("\x1b[5 q")); + data->unibi_ext.enter_replace_mode = (int)unibi_add_ext_str(ut, NULL, + TMUX_WRAP("\x1b[3 q")); data->unibi_ext.exit_insert_mode = (int)unibi_add_ext_str(ut, NULL, TMUX_WRAP("\x1b[2 q")); } diff --git a/src/nvim/ui.c b/src/nvim/ui.c index dc2bc0898c..ad875367c9 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -121,7 +121,7 @@ void ui_update_encoding(void) // May update the shape of the cursor. void ui_cursor_shape(void) { - ui_change_mode(); + ui_mode_change(); } void ui_refresh(void) @@ -214,9 +214,9 @@ void ui_detach(UI *ui) shift_index++; } - ui_count--; - // schedule a refresh - loop_push_event(&loop, (Event) { .handler = refresh }, false); + if (--ui_count) { + ui_refresh(); + } } void ui_clear(void) @@ -469,32 +469,19 @@ static void flush_cursor_update(void) // Notify that the current mode has changed. Can be used to change cursor // shape, for example. -static void ui_change_mode(void) +static void ui_mode_change(void) { - static int showing_insert_mode = MAYBE; - + int mode; if (!full_screen) { return; } - - if (State & INSERT) { - if (showing_insert_mode != TRUE) { - UI_CALL(insert_mode); - } - showing_insert_mode = TRUE; - } else { - if (showing_insert_mode != FALSE) { - UI_CALL(normal_mode); - } - showing_insert_mode = FALSE; - } + /* Get a simple UI mode out of State. */ + if ((State & REPLACE) == REPLACE) + mode = REPLACE; + else if (State & INSERT) + mode = INSERT; + else + mode = NORMAL; + UI_CALL(mode_change, mode); conceal_check_cursur_line(); } - -static void refresh(Event event) -{ - if (ui_count) { - ui_refresh(); - } -} - diff --git a/src/nvim/ui.h b/src/nvim/ui.h index 76ceec7775..9cfd99c096 100644 --- a/src/nvim/ui.h +++ b/src/nvim/ui.h @@ -24,8 +24,7 @@ struct ui_t { void (*busy_stop)(UI *ui); void (*mouse_on)(UI *ui); void (*mouse_off)(UI *ui); - void (*insert_mode)(UI *ui); - void (*normal_mode)(UI *ui); + void (*mode_change)(UI *ui, int mode); void (*set_scroll_region)(UI *ui, int top, int bot, int left, int right); void (*scroll)(UI *ui, int count); void (*highlight_set)(UI *ui, HlAttrs attrs); diff --git a/src/nvim/version.c b/src/nvim/version.c index a496af13b4..7ab7467661 100644 --- a/src/nvim/version.c +++ b/src/nvim/version.c @@ -80,15 +80,15 @@ static int included_patches[] = { 784, //783, //782, - //781, + 781, //780, //779, //778, //777, - //776, + 776, 775, 774, - //773, + 773, //772, //771, //770, @@ -261,7 +261,7 @@ static int included_patches[] = { //603, //602, 601, - //600, + 600, 599, //598, 597, @@ -1098,6 +1098,15 @@ void intro_message(int colon) N_("by Bram Moolenaar et al."), N_("Vim is open source and freely distributable"), "", + N_("First time using a vi-like editor?"), + N_("Type :Tutor<Enter> to get started!"), + "", + N_("Already know your way around Vim?"), + N_("See :help nvim-intro for an introduction to Neovim."), + "", + N_("Still have questions?"), + N_("Reach out to the Neovim community at neovim.io/community."), + "", N_("Help poor children in Uganda!"), N_("type :help iccf<Enter> for information "), "", diff --git a/src/nvim/vim.h b/src/nvim/vim.h index 17f9cbc310..9fbc6dd300 100644 --- a/src/nvim/vim.h +++ b/src/nvim/vim.h @@ -6,7 +6,7 @@ */ #ifndef NVIM_VIM_H -# define NVIM_VIM_H +#define NVIM_VIM_H #define MIN(X, Y) (X < Y ? X : Y) #define MAX(X, Y) (X > Y ? X : Y) diff --git a/test/functional/clipboard/clipboard_provider_spec.lua b/test/functional/clipboard/clipboard_provider_spec.lua index f6d6188d82..0550d22fa6 100644 --- a/test/functional/clipboard/clipboard_provider_spec.lua +++ b/test/functional/clipboard/clipboard_provider_spec.lua @@ -232,12 +232,15 @@ describe('clipboard usage', function() expect('words') eq({{'words'}, 'v'}, eval("g:test_clip['*']")) + -- "+ shouldn't have changed + eq({''}, eval("g:test_clip['+']")) + execute("let g:test_clip['*'] = ['linewise stuff','']") feed('p') expect([[ words linewise stuff]]) - end) + end) it('does not clobber "0 when pasting', function() insert('a line') @@ -284,6 +287,50 @@ describe('clipboard usage', function() end) + describe('with clipboard=unnamedplus', function() + before_each(function() + execute('set clipboard=unnamedplus') + end) + + it('links the "+ and unnamed registers', function() + insert("one two") + feed('^"+dwdw"+P') + expect('two') + eq({{'two'}, 'v'}, eval("g:test_clip['+']")) + + -- "* shouldn't have changed + eq({''}, eval("g:test_clip['*']")) + + execute("let g:test_clip['+'] = ['three']") + feed('p') + expect('twothree') + end) + + it('and unnamed, yanks to both', function() + execute('set clipboard=unnamedplus,unnamed') + insert([[ + really unnamed + text]]) + feed('ggdd"*p"+p') + expect([[ + text + really unnamed + really unnamed]]) + eq({{'really unnamed', ''}, 'V'}, eval("g:test_clip['+']")) + eq({{'really unnamed', ''}, 'V'}, eval("g:test_clip['*']")) + + -- unnamedplus takes predecence when pasting + execute("let g:test_clip['+'] = ['the plus','']") + execute("let g:test_clip['*'] = ['the star','']") + feed("p") + expect([[ + text + really unnamed + really unnamed + the plus]]) + end) + end) + it('supports :put', function() insert("a line") execute("let g:test_clip['*'] = ['some text']") @@ -322,6 +369,7 @@ describe('clipboard usage', function() [2] = {foreground = Screen.colors.Blue}, [3] = {bold = true, foreground = Screen.colors.SeaGreen}}, {{bold = true, foreground = Screen.colors.Blue}}) + feed('<cr>') -- clear out of Press ENTER screen end) it('can paste "* to the commandline', function() diff --git a/test/functional/eval/glob_spec.lua b/test/functional/eval/glob_spec.lua new file mode 100644 index 0000000000..136417249c --- /dev/null +++ b/test/functional/eval/glob_spec.lua @@ -0,0 +1,25 @@ +local helpers = require('test.functional.helpers') +local clear, execute, eval, eq = helpers.clear, helpers.execute, helpers.eval, helpers.eq + +before_each(function() + clear() + lfs.mkdir('test-glob') + execute('cd test-glob') +end) + +after_each(function() + lfs.rmdir('test-glob') +end) + +describe('glob()', function() + it("glob('.*') returns . and .. ", function() + eq({'.', '..'}, eval("glob('.*', 0, 1)")) + -- Do it again to verify scandir_next_with_dots() internal state. + eq({'.', '..'}, eval("glob('.*', 0, 1)")) + end) + it("glob('*') returns an empty list ", function() + eq({}, eval("glob('*', 0, 1)")) + -- Do it again to verify scandir_next_with_dots() internal state. + eq({}, eval("glob('*', 0, 1)")) + end) +end) diff --git a/test/functional/eval/msgpack_functions_spec.lua b/test/functional/eval/msgpack_functions_spec.lua new file mode 100644 index 0000000000..8ca2cca32c --- /dev/null +++ b/test/functional/eval/msgpack_functions_spec.lua @@ -0,0 +1,615 @@ +local helpers = require('test.functional.helpers') +local clear, feed, execute = helpers.clear, helpers.feed, helpers.execute +local eval, eq, neq = helpers.eval, helpers.eq, helpers.neq +local execute, source = helpers.execute, helpers.source +local nvim = helpers.nvim +describe('msgpack*() functions', function() + before_each(function() + clear() + end) + local obj_test = function(msg, obj) + it(msg, function() + nvim('set_var', 'obj', obj) + eq(obj, eval('msgpackparse(msgpackdump(g:obj))')) + end) + end + -- Regression test: msgpack_list_write was failing to write buffer with zero + -- length. + obj_test('are able to dump and restore {"file": ""}', {{file=''}}) + -- Regression test: msgpack_list_write was failing to write buffer with NL at + -- the end. + obj_test('are able to dump and restore {0, "echo mpack"}', {{0, 'echo mpack'}}) + obj_test('are able to dump and restore "Test\\n"', {'Test\n'}) + -- Regression test: msgpack_list_write was failing to write buffer with NL + -- inside. + obj_test('are able to dump and restore "Test\\nTest 2"', {'Test\nTest 2'}) + -- Test that big objects (requirement: dump to something that is bigger then + -- IOSIZE) are also fine. This particular object is obtained by concatenating + -- 5 identical shada files. + local big_obj = { + 1, 1436711454, 78, { + encoding="utf-8", + max_kbyte=10, + pid=19269, + version="NVIM 0.0.0-alpha+201507121634" + }, + 8, 1436711451, 40, { file="/home/zyx/.nvim/shada/main.shada" }, + 8, 1436711391, 8, { file="" }, + 4, 1436700940, 30, { 0, "call mkdir('/tmp/tty/tty')" }, + 4, 1436701355, 35, { 0, "call mkdir('/tmp/tty/tty', 'p')" }, + 4, 1436701368, 24, { 0, "call mkdir('/', 'p')" }, + 4, 1436701375, 26, { 0, "call mkdir('/tty/tty')" }, + 4, 1436701383, 30, { 0, "call mkdir('/tty/tty/tty')" }, + 4, 1436701407, 35, { 0, "call mkdir('/usr/tty/tty', 'p')" }, + 4, 1436701666, 35, { 0, "call mkdir('/tty/tty/tty', 'p')" }, + 4, 1436708101, 25, { 0, "echo msgpackdump([1])" }, + 4, 1436708966, 6, { 0, "cq" }, + 4, 1436709606, 25, { 0, "echo msgpackdump([5])" }, + 4, 1436709610, 26, { 0, "echo msgpackdump([10])" }, + 4, 1436709615, 31, { 0, "echo msgpackdump([5, 5, 5])" }, + 4, 1436709618, 35, { 0, "echo msgpackdump([5, 5, 5, 10])" }, + 4, 1436709634, 57, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1}]])" + }, + 4, 1436709651, 67, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1, \"def\": 0}]])" + }, + 4, 1436709660, 70, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1, \"def\": 0}], 0])" + }, + 4, 1436710095, 29, { 0, "echo msgpackparse([\"\\n\"])" }, + 4, 1436710100, 28, { 0, "echo msgpackparse([\"j\"])" }, + 4, 1436710109, 31, { 0, "echo msgpackparse([\"\", \"\"])" }, + 4, 1436710424, 33, { 0, "echo msgpackparse([\"\", \"\\n\"])" }, + 4, 1436710428, 32, { 0, "echo msgpackparse([\"\", \"j\"])" }, + 4, 1436711142, 14, { 0, "echo mpack" }, + 4, 1436711196, 45, { 0, "let lengths = map(mpack[:], 'len(v:val)')" }, + 4, 1436711206, 16, { 0, "echo lengths" }, + 4, 1436711244, 92, { + 0, + ("let sum = len(lengths) - 1 | call map(copy(lengths), " + .. "'extend(g:, {\"sum\": sum + v:val})')") + }, + 4, 1436711245, 12, { 0, "echo sum" }, + 4, 1436711398, 10, { 0, "echo s" }, + 4, 1436711404, 41, { 0, "let mpack = readfile('/tmp/foo', 'b')" }, + 4, 1436711408, 41, { 0, "let shada_objects=msgpackparse(mpack)" }, + 4, 1436711415, 22, { 0, "echo shada_objects" }, + 4, 1436711451, 30, { 0, "e ~/.nvim/shada/main.shada" }, + 4, 1436711454, 6, { 0, "qa" }, + 4, 1436711442, 9, { 1, "test", 47 }, + 4, 1436711443, 15, { 1, "aontsuesan", 47 }, + 2, 1436711443, 38, { hlsearch=1, pat="aontsuesan", smartcase=1 }, + 2, 0, 31, { islast=0, pat="", smartcase=1, sub=1 }, + 3, 0, 3, { "" }, + 10, 1436711451, 40, { file="/home/zyx/.nvim/shada/main.shada" }, + 1, 1436711454, 78, { + encoding="utf-8", + max_kbyte=10, + pid=19269, + version="NVIM 0.0.0-alpha+201507121634" + }, + 8, 1436711451, 40, { file="/home/zyx/.nvim/shada/main.shada" }, + 8, 1436711391, 8, { file="" }, + 4, 1436700940, 30, { 0, "call mkdir('/tmp/tty/tty')" }, + 4, 1436701355, 35, { 0, "call mkdir('/tmp/tty/tty', 'p')" }, + 4, 1436701368, 24, { 0, "call mkdir('/', 'p')" }, + 4, 1436701375, 26, { 0, "call mkdir('/tty/tty')" }, + 4, 1436701383, 30, { 0, "call mkdir('/tty/tty/tty')" }, + 4, 1436701407, 35, { 0, "call mkdir('/usr/tty/tty', 'p')" }, + 4, 1436701666, 35, { 0, "call mkdir('/tty/tty/tty', 'p')" }, + 4, 1436708101, 25, { 0, "echo msgpackdump([1])" }, + 4, 1436708966, 6, { 0, "cq" }, + 4, 1436709606, 25, { 0, "echo msgpackdump([5])" }, + 4, 1436709610, 26, { 0, "echo msgpackdump([10])" }, + 4, 1436709615, 31, { 0, "echo msgpackdump([5, 5, 5])" }, + 4, 1436709618, 35, { 0, "echo msgpackdump([5, 5, 5, 10])" }, + 4, 1436709634, 57, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1}]])" + }, + 4, 1436709651, 67, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1, \"def\": 0}]])" + }, + 4, 1436709660, 70, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1, \"def\": 0}], 0])" + }, + 4, 1436710095, 29, { 0, "echo msgpackparse([\"\\n\"])" }, + 4, 1436710100, 28, { 0, "echo msgpackparse([\"j\"])" }, + 4, 1436710109, 31, { 0, "echo msgpackparse([\"\", \"\"])" }, + 4, 1436710424, 33, { 0, "echo msgpackparse([\"\", \"\\n\"])" }, + 4, 1436710428, 32, { 0, "echo msgpackparse([\"\", \"j\"])" }, + 4, 1436711142, 14, { 0, "echo mpack" }, + 4, 1436711196, 45, { 0, "let lengths = map(mpack[:], 'len(v:val)')" }, + 4, 1436711206, 16, { 0, "echo lengths" }, + 4, 1436711244, 92, { + 0, + ("let sum = len(lengths) - 1 | call map(copy(lengths), " + .. "'extend(g:, {\"sum\": sum + v:val})')") + }, + 4, 1436711245, 12, { 0, "echo sum" }, + 4, 1436711398, 10, { 0, "echo s" }, + 4, 1436711404, 41, { 0, "let mpack = readfile('/tmp/foo', 'b')" }, + 4, 1436711408, 41, { 0, "let shada_objects=msgpackparse(mpack)" }, + 4, 1436711415, 22, { 0, "echo shada_objects" }, + 4, 1436711451, 30, { 0, "e ~/.nvim/shada/main.shada" }, + 4, 1436711454, 6, { 0, "qa" }, + 4, 1436711442, 9, { 1, "test", 47 }, + 4, 1436711443, 15, { 1, "aontsuesan", 47 }, + 2, 1436711443, 38, { hlsearch=1, pat="aontsuesan", smartcase=1 }, + 2, 0, 31, { islast=0, pat="", smartcase=1, sub=1 }, + 3, 0, 3, { "" }, + 10, 1436711451, 40, { file="/home/zyx/.nvim/shada/main.shada" }, + 1, 1436711454, 78, { + encoding="utf-8", + max_kbyte=10, + pid=19269, + version="NVIM 0.0.0-alpha+201507121634" + }, + 8, 1436711451, 40, { file="/home/zyx/.nvim/shada/main.shada" }, + 8, 1436711391, 8, { file="" }, + 4, 1436700940, 30, { 0, "call mkdir('/tmp/tty/tty')" }, + 4, 1436701355, 35, { 0, "call mkdir('/tmp/tty/tty', 'p')" }, + 4, 1436701368, 24, { 0, "call mkdir('/', 'p')" }, + 4, 1436701375, 26, { 0, "call mkdir('/tty/tty')" }, + 4, 1436701383, 30, { 0, "call mkdir('/tty/tty/tty')" }, + 4, 1436701407, 35, { 0, "call mkdir('/usr/tty/tty', 'p')" }, + 4, 1436701666, 35, { 0, "call mkdir('/tty/tty/tty', 'p')" }, + 4, 1436708101, 25, { 0, "echo msgpackdump([1])" }, + 4, 1436708966, 6, { 0, "cq" }, + 4, 1436709606, 25, { 0, "echo msgpackdump([5])" }, + 4, 1436709610, 26, { 0, "echo msgpackdump([10])" }, + 4, 1436709615, 31, { 0, "echo msgpackdump([5, 5, 5])" }, + 4, 1436709618, 35, { 0, "echo msgpackdump([5, 5, 5, 10])" }, + 4, 1436709634, 57, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1}]])" + }, + 4, 1436709651, 67, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1, \"def\": 0}]])" + }, + 4, 1436709660, 70, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1, \"def\": 0}], 0])" + }, + 4, 1436710095, 29, { 0, "echo msgpackparse([\"\\n\"])" }, + 4, 1436710100, 28, { 0, "echo msgpackparse([\"j\"])" }, + 4, 1436710109, 31, { 0, "echo msgpackparse([\"\", \"\"])" }, + 4, 1436710424, 33, { 0, "echo msgpackparse([\"\", \"\\n\"])" }, + 4, 1436710428, 32, { 0, "echo msgpackparse([\"\", \"j\"])" }, + 4, 1436711142, 14, { 0, "echo mpack" }, + 4, 1436711196, 45, { 0, "let lengths = map(mpack[:], 'len(v:val)')" }, + 4, 1436711206, 16, { 0, "echo lengths" }, + 4, 1436711244, 92, { + 0, + ("let sum = len(lengths) - 1 | call map(copy(lengths), " + .. "'extend(g:, {\"sum\": sum + v:val})')") + }, + 4, 1436711245, 12, { 0, "echo sum" }, + 4, 1436711398, 10, { 0, "echo s" }, + 4, 1436711404, 41, { 0, "let mpack = readfile('/tmp/foo', 'b')" }, + 4, 1436711408, 41, { 0, "let shada_objects=msgpackparse(mpack)" }, + 4, 1436711415, 22, { 0, "echo shada_objects" }, + 4, 1436711451, 30, { 0, "e ~/.nvim/shada/main.shada" }, + 4, 1436711454, 6, { 0, "qa" }, + 4, 1436711442, 9, { 1, "test", 47 }, + 4, 1436711443, 15, { 1, "aontsuesan", 47 }, + 2, 1436711443, 38, { hlsearch=1, pat="aontsuesan", smartcase=1 }, + 2, 0, 31, { islast=0, pat="", smartcase=1, sub=1 }, + 3, 0, 3, { "" }, + 10, 1436711451, 40, { file="/home/zyx/.nvim/shada/main.shada" }, + 1, 1436711454, 78, { + encoding="utf-8", + max_kbyte=10, + pid=19269, + version="NVIM 0.0.0-alpha+201507121634" + }, + 8, 1436711451, 40, { file="/home/zyx/.nvim/shada/main.shada" }, + 8, 1436711391, 8, { file="" }, + 4, 1436700940, 30, { 0, "call mkdir('/tmp/tty/tty')" }, + 4, 1436701355, 35, { 0, "call mkdir('/tmp/tty/tty', 'p')" }, + 4, 1436701368, 24, { 0, "call mkdir('/', 'p')" }, + 4, 1436701375, 26, { 0, "call mkdir('/tty/tty')" }, + 4, 1436701383, 30, { 0, "call mkdir('/tty/tty/tty')" }, + 4, 1436701407, 35, { 0, "call mkdir('/usr/tty/tty', 'p')" }, + 4, 1436701666, 35, { 0, "call mkdir('/tty/tty/tty', 'p')" }, + 4, 1436708101, 25, { 0, "echo msgpackdump([1])" }, + 4, 1436708966, 6, { 0, "cq" }, + 4, 1436709606, 25, { 0, "echo msgpackdump([5])" }, + 4, 1436709610, 26, { 0, "echo msgpackdump([10])" }, + 4, 1436709615, 31, { 0, "echo msgpackdump([5, 5, 5])" }, + 4, 1436709618, 35, { 0, "echo msgpackdump([5, 5, 5, 10])" }, + 4, 1436709634, 57, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1}]])" + }, + 4, 1436709651, 67, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1, \"def\": 0}]])" + }, + 4, 1436709660, 70, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1, \"def\": 0}], 0])" + }, + 4, 1436710095, 29, { 0, "echo msgpackparse([\"\\n\"])" }, + 4, 1436710100, 28, { 0, "echo msgpackparse([\"j\"])" }, + 4, 1436710109, 31, { 0, "echo msgpackparse([\"\", \"\"])" }, + 4, 1436710424, 33, { 0, "echo msgpackparse([\"\", \"\\n\"])" }, + 4, 1436710428, 32, { 0, "echo msgpackparse([\"\", \"j\"])" }, + 4, 1436711142, 14, { 0, "echo mpack" }, + 4, 1436711196, 45, { 0, "let lengths = map(mpack[:], 'len(v:val)')" }, + 4, 1436711206, 16, { 0, "echo lengths" }, + 4, 1436711244, 92, { + 0, + ("let sum = len(lengths) - 1 | call map(copy(lengths), " + .. "'extend(g:, {\"sum\": sum + v:val})')") + }, + 4, 1436711245, 12, { 0, "echo sum" }, + 4, 1436711398, 10, { 0, "echo s" }, + 4, 1436711404, 41, { 0, "let mpack = readfile('/tmp/foo', 'b')" }, + 4, 1436711408, 41, { 0, "let shada_objects=msgpackparse(mpack)" }, + 4, 1436711415, 22, { 0, "echo shada_objects" }, + 4, 1436711451, 30, { 0, "e ~/.nvim/shada/main.shada" }, + 4, 1436711454, 6, { 0, "qa" }, + 4, 1436711442, 9, { 1, "test", 47 }, + 4, 1436711443, 15, { 1, "aontsuesan", 47 }, + 2, 1436711443, 38, { hlsearch=1, pat="aontsuesan", smartcase=1 }, + 2, 0, 31, { islast=0, pat="", smartcase=1, sub=1 }, + 3, 0, 3, { "" }, + 10, 1436711451, 40, { file="/home/zyx/.nvim/shada/main.shada" }, + 1, 1436711454, 78, { + encoding="utf-8", + max_kbyte=10, + pid=19269, + version="NVIM 0.0.0-alpha+201507121634" + }, + 8, 1436711451, 40, { file="/home/zyx/.nvim/shada/main.shada" }, + 8, 1436711391, 8, { file="" }, + 4, 1436700940, 30, { 0, "call mkdir('/tmp/tty/tty')" }, + 4, 1436701355, 35, { 0, "call mkdir('/tmp/tty/tty', 'p')" }, + 4, 1436701368, 24, { 0, "call mkdir('/', 'p')" }, + 4, 1436701375, 26, { 0, "call mkdir('/tty/tty')" }, + 4, 1436701383, 30, { 0, "call mkdir('/tty/tty/tty')" }, + 4, 1436701407, 35, { 0, "call mkdir('/usr/tty/tty', 'p')" }, + 4, 1436701666, 35, { 0, "call mkdir('/tty/tty/tty', 'p')" }, + 4, 1436708101, 25, { 0, "echo msgpackdump([1])" }, + 4, 1436708966, 6, { 0, "cq" }, + 4, 1436709606, 25, { 0, "echo msgpackdump([5])" }, + 4, 1436709610, 26, { 0, "echo msgpackdump([10])" }, + 4, 1436709615, 31, { 0, "echo msgpackdump([5, 5, 5])" }, + 4, 1436709618, 35, { 0, "echo msgpackdump([5, 5, 5, 10])" }, + 4, 1436709634, 57, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1}]])" + }, + 4, 1436709651, 67, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1, \"def\": 0}]])" + }, + 4, 1436709660, 70, { + 0, + "echo msgpackdump([5, 5, 5, 10, [10, 20, {\"abc\": 1, \"def\": 0}], 0])" + }, + 4, 1436710095, 29, { 0, "echo msgpackparse([\"\\n\"])" }, + 4, 1436710100, 28, { 0, "echo msgpackparse([\"j\"])" }, + 4, 1436710109, 31, { 0, "echo msgpackparse([\"\", \"\"])" }, + 4, 1436710424, 33, { 0, "echo msgpackparse([\"\", \"\\n\"])" }, + 4, 1436710428, 32, { 0, "echo msgpackparse([\"\", \"j\"])" }, + 4, 1436711142, 14, { 0, "echo mpack" }, + 4, 1436711196, 45, { 0, "let lengths = map(mpack[:], 'len(v:val)')" }, + 4, 1436711206, 16, { 0, "echo lengths" }, + 4, 1436711244, 92, { + 0, + ("let sum = len(lengths) - 1 | call map(copy(lengths), " + .. "'extend(g:, {\"sum\": sum + v:val})')") + }, + 4, 1436711245, 12, { 0, "echo sum" }, + 4, 1436711398, 10, { 0, "echo s" }, + 4, 1436711404, 41, { 0, "let mpack = readfile('/tmp/foo', 'b')" }, + 4, 1436711408, 41, { 0, "let shada_objects=msgpackparse(mpack)" }, + 4, 1436711415, 22, { 0, "echo shada_objects" }, + 4, 1436711451, 30, { 0, "e ~/.nvim/shada/main.shada" }, + 4, 1436711454, 6, { 0, "qa" }, + 4, 1436711442, 9, { 1, "test", 47 }, + 4, 1436711443, 15, { 1, "aontsuesan", 47 }, + 2, 1436711443, 38, { hlsearch=1, pat="aontsuesan", smartcase=1 }, + 2, 0, 31, { islast=0, pat="", smartcase=1, sub=1 }, + 3, 0, 3, { "" }, + 10, 1436711451, 40, { file="/home/zyx/.nvim/shada/main.shada" } + } + obj_test('are able to dump and restore rather big object', big_obj) + + obj_test('are able to dump and restore floating-point value', {0.125}) + + it('restore nil as special dict', function() + execute('let dumped = ["\\xC0"]') + execute('let parsed = msgpackparse(dumped)') + eq({{_TYPE={}, _VAL=0}}, eval('parsed')) + eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.nil')) + end) + + it('restore boolean false as zero', function() + execute('let dumped = ["\\xC2"]') + execute('let parsed = msgpackparse(dumped)') + eq({{_TYPE={}, _VAL=0}}, eval('parsed')) + eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.boolean')) + end) + + it('restore boolean true as one', function() + execute('let dumped = ["\\xC3"]') + execute('let parsed = msgpackparse(dumped)') + eq({{_TYPE={}, _VAL=1}}, eval('parsed')) + eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.boolean')) + end) + + it('dump string as BIN 8', function() + nvim('set_var', 'obj', {'Test'}) + eq({"\196\004Test"}, eval('msgpackdump(obj)')) + end) + + it('restore FIXSTR as special dict', function() + execute('let dumped = ["\\xa2ab"]') + execute('let parsed = msgpackparse(dumped)') + eq({{_TYPE={}, _VAL={'ab'}}}, eval('parsed')) + eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.string')) + end) + + it('restore BIN 8 as string', function() + execute('let dumped = ["\\xC4\\x02ab"]') + eq({'ab'}, eval('msgpackparse(dumped)')) + end) + + it('restore FIXEXT1 as special dictionary', function() + execute('let dumped = ["\\xD4\\x10", ""]') + execute('let parsed = msgpackparse(dumped)') + eq({{_TYPE={}, _VAL={0x10, {"", ""}}}}, eval('parsed')) + eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.ext')) + end) + + it('restore MAP with BIN key as special dictionary', function() + execute('let dumped = ["\\x81\\xC4\\x01a\\xC4\\n"]') + execute('let parsed = msgpackparse(dumped)') + eq({{_TYPE={}, _VAL={{'a', ''}}}}, eval('parsed')) + eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.map')) + end) + + it('restore MAP with duplicate STR keys as special dictionary', function() + execute('let dumped = ["\\x82\\xA1a\\xC4\\n\\xA1a\\xC4\\n"]') + execute('let parsed = msgpackparse(dumped)') + eq({{_TYPE={}, _VAL={{{_TYPE={}, _VAL={'a'}}, ''}, + {{_TYPE={}, _VAL={'a'}}, ''}}}}, eval('parsed')) + eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.map')) + eq(1, eval('g:parsed[0]._VAL[0][0]._TYPE is v:msgpack_types.string')) + eq(1, eval('g:parsed[0]._VAL[1][0]._TYPE is v:msgpack_types.string')) + end) + + it('restore MAP with MAP key as special dictionary', function() + execute('let dumped = ["\\x81\\x80\\xC4\\n"]') + execute('let parsed = msgpackparse(dumped)') + eq({{_TYPE={}, _VAL={{{}, ''}}}}, eval('parsed')) + eq(1, eval('g:parsed[0]._TYPE is v:msgpack_types.map')) + end) + + it('can restore and dump UINT64_MAX', function() + execute('let dumped = ["\\xCF" . repeat("\\xFF", 8)]') + execute('let parsed = msgpackparse(dumped)') + execute('let dumped2 = msgpackdump(parsed)') + eq(1, eval('type(parsed[0]) == type(0) ' .. + '|| parsed[0]._TYPE is v:msgpack_types.integer')) + if eval('type(parsed[0]) == type(0)') == 1 then + eq(1, eval('0xFFFFFFFFFFFFFFFF == parsed[0]')) + else + eq({_TYPE={}, _VAL={1, 3, 0x7FFFFFFF, 0x7FFFFFFF}}, eval('parsed[0]')) + end + eq(1, eval('dumped ==# dumped2')) + end) + + it('can restore and dump INT64_MIN', function() + execute('let dumped = ["\\xD3\\x80" . repeat("\\n", 7)]') + execute('let parsed = msgpackparse(dumped)') + execute('let dumped2 = msgpackdump(parsed)') + eq(1, eval('type(parsed[0]) == type(0) ' .. + '|| parsed[0]._TYPE is v:msgpack_types.integer')) + if eval('type(parsed[0]) == type(0)') == 1 then + eq(1, eval('-0x8000000000000000 == parsed[0]')) + else + eq({_TYPE={}, _VAL={-1, 2, 0, 0}}, eval('parsed[0]')) + end + eq(1, eval('dumped ==# dumped2')) + end) + + it('can restore and dump BIN string with zero byte', function() + execute('let dumped = ["\\xC4\\x01\\n"]') + execute('let parsed = msgpackparse(dumped)') + execute('let dumped2 = msgpackdump(parsed)') + eq({{_TYPE={}, _VAL={'\n'}}}, eval('parsed')) + eq(1, eval('parsed[0]._TYPE is v:msgpack_types.binary')) + eq(1, eval('dumped ==# dumped2')) + end) + + it('can restore and dump STR string with zero byte', function() + execute('let dumped = ["\\xA1\\n"]') + execute('let parsed = msgpackparse(dumped)') + execute('let dumped2 = msgpackdump(parsed)') + eq({{_TYPE={}, _VAL={'\n'}}}, eval('parsed')) + eq(1, eval('parsed[0]._TYPE is v:msgpack_types.string')) + eq(1, eval('dumped ==# dumped2')) + end) + + it('can restore and dump BIN string with NL', function() + execute('let dumped = ["\\xC4\\x01", ""]') + execute('let parsed = msgpackparse(dumped)') + execute('let dumped2 = msgpackdump(parsed)') + eq({"\n"}, eval('parsed')) + eq(1, eval('dumped ==# dumped2')) + end) + + it('can dump generic mapping with generic mapping keys and values', function() + execute('let todump = {"_TYPE": v:msgpack_types.map, "_VAL": []}') + execute('let todumpv1 = {"_TYPE": v:msgpack_types.map, "_VAL": []}') + execute('let todumpv2 = {"_TYPE": v:msgpack_types.map, "_VAL": []}') + execute('call add(todump._VAL, [todumpv1, todumpv2])') + eq({'\129\128\128'}, eval('msgpackdump([todump])')) + end) + + it('can dump generic mapping with ext', function() + execute('let todump = {"_TYPE": v:msgpack_types.ext, "_VAL": [5, ["",""]]}') + eq({'\212\005', ''}, eval('msgpackdump([todump])')) + end) + + it('can dump generic mapping with array', function() + execute('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": [5, [""]]}') + eq({'\146\005\145\196\n'}, eval('msgpackdump([todump])')) + end) + + it('can dump generic mapping with UINT64_MAX', function() + execute('let todump = {"_TYPE": v:msgpack_types.integer}') + execute('let todump._VAL = [1, 3, 0x7FFFFFFF, 0x7FFFFFFF]') + eq({'\207\255\255\255\255\255\255\255\255'}, eval('msgpackdump([todump])')) + end) + + it('can dump generic mapping with INT64_MIN', function() + execute('let todump = {"_TYPE": v:msgpack_types.integer}') + execute('let todump._VAL = [-1, 2, 0, 0]') + eq({'\211\128\n\n\n\n\n\n\n'}, eval('msgpackdump([todump])')) + end) + + it('dump and restore generic mapping with floating-point value', function() + execute('let todump = {"_TYPE": v:msgpack_types.float, "_VAL": 0.125}') + eq({0.125}, eval('msgpackparse(msgpackdump([todump]))')) + end) + + it('fails to dump a function reference', function() + execute('let Todump = function("tr")') + execute([[ + try + let dumped = msgpackdump([Todump]) + let exception = 0 + catch + let exception = v:exception + endtry + ]]) + eq('Vim(let):E475: Invalid argument: attempt to dump function reference', + eval('exception')) + end) + + it('fails to dump a function reference in a list', function() + execute('let todump = [function("tr")]') + execute([[ + try + let dumped = msgpackdump([todump]) + let exception = 0 + catch + let exception = v:exception + endtry + ]]) + eq('Vim(let):E475: Invalid argument: attempt to dump function reference', + eval('exception')) + end) + + it('fails to dump a recursive list', function() + execute('let todump = [[[]]]') + execute('call add(todump[0][0], todump)') + execute([[ + try + let dumped = msgpackdump([todump]) + let exception = 0 + catch + let exception = v:exception + endtry + ]]) + eq('Vim(let):E475: Invalid argument: container references itself', + eval('exception')) + end) + + it('fails to dump a recursive dict', function() + execute('let todump = {"d": {"d": {}}}') + execute('call extend(todump.d.d, {"d": todump})') + execute([[ + try + let dumped = msgpackdump([todump]) + let exception = 0 + catch + let exception = v:exception + endtry + ]]) + eq('Vim(let):E475: Invalid argument: container references itself', + eval('exception')) + end) + + it('fails to dump a recursive list in a special dict', function() + execute('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": []}') + execute('call add(todump._VAL, todump)') + execute([[ + try + let dumped = msgpackdump([todump]) + let exception = 0 + catch + let exception = v:exception + endtry + ]]) + eq('Vim(let):E475: Invalid argument: container references itself', + eval('exception')) + end) + + it('fails to dump a recursive (key) map in a special dict', function() + execute('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": []}') + execute('call add(todump._VAL, [todump, 0])') + execute([[ + try + let dumped = msgpackdump([todump]) + let exception = 0 + catch + let exception = v:exception + endtry + ]]) + eq('Vim(let):E475: Invalid argument: container references itself', + eval('exception')) + end) + + it('fails to dump a recursive (val) map in a special dict', function() + execute('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": []}') + execute('call add(todump._VAL, [0, todump])') + execute([[ + try + let dumped = msgpackdump([todump]) + let exception = 0 + catch + let exception = v:exception + endtry + ]]) + eq('Vim(let):E475: Invalid argument: container references itself', + eval('exception')) + end) + + it('fails to dump a recursive (val) special list in a special dict', + function() + execute('let todump = {"_TYPE": v:msgpack_types.array, "_VAL": []}') + execute('call add(todump._VAL, [0, todump._VAL])') + execute([[ + try + let dumped = msgpackdump([todump]) + let exception = 0 + catch + let exception = v:exception + endtry + ]]) + eq('Vim(let):E475: Invalid argument: container references itself', + eval('exception')) + end) + + it('msgpackparse(systemlist(...)) does not segfault. #3135', function() + local cmd = "msgpackparse(systemlist('" + ..helpers.nvim_prog.." --api-info'))['_TYPE']['_VAL'][0][0]" + local api_info = eval(cmd) + api_info = eval(cmd) -- do it again (try to force segfault) + api_info = eval(cmd) -- do it again + eq('functions', api_info) + end) +end) diff --git a/test/functional/ex_cmds/grep_spec.lua b/test/functional/ex_cmds/grep_spec.lua new file mode 100644 index 0000000000..9c792099c1 --- /dev/null +++ b/test/functional/ex_cmds/grep_spec.lua @@ -0,0 +1,22 @@ +local helpers = require('test.functional.helpers') +local clear, execute, nvim, feed, eq, ok, eval = + helpers.clear, helpers.execute, helpers.nvim, helpers.feed, + helpers.eq, helpers.ok, helpers.eval + +describe(':grep', function() + before_each(clear) + + it('does not hang on large input #2983', function() + if eval("executable('grep')") == 0 then + pending('missing "grep" command') + return + end + + execute([[set grepprg=grep\ -r]]) + -- Change to test directory so that the test does not run too long. + execute('cd test') + execute('grep a **/*') + feed('<cr>') -- Press ENTER + ok(eval('len(getqflist())') > 9000) -- IT'S OVER 9000!!1 + end) +end) diff --git a/test/functional/fixtures/autoload/provider/clipboard.vim b/test/functional/fixtures/autoload/provider/clipboard.vim index a28484169a..0935ea45ff 100644 --- a/test/functional/fixtures/autoload/provider/clipboard.vim +++ b/test/functional/fixtures/autoload/provider/clipboard.vim @@ -9,16 +9,22 @@ function! s:methods.get(reg) if g:cliperror return 0 end + let reg = a:reg == '"' ? '+' : a:reg if g:cliplossy " behave like pure text clipboard - return g:test_clip[a:reg][0] + return g:test_clip[reg][0] else - "behave like VIMENC clipboard - return g:test_clip[a:reg] + " behave like VIMENC clipboard + return g:test_clip[reg] end endfunction function! s:methods.set(lines, regtype, reg) + if a:reg == '"' + call s:methods.set(a:lines,a:regtype,'+') + call s:methods.set(a:lines,a:regtype,'*') + return 0 + end let g:test_clip[a:reg] = [a:lines, a:regtype] endfunction diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index d5c22c6f1c..1159707282 100644 --- a/test/functional/helpers.lua +++ b/test/functional/helpers.lua @@ -6,7 +6,6 @@ local AsyncSession = require('nvim.async_session') local Session = require('nvim.session') local nvim_prog = os.getenv('NVIM_PROG') or 'build/bin/nvim' ---- FIXME: 'autoindent' messes up the insert() function local nvim_argv = {nvim_prog, '-u', 'NONE', '-i', 'NONE', '-N', '--cmd', 'set shortmess+=I background=light noswapfile noautoindent', '--embed'} diff --git a/test/functional/legacy/077_mf_hash_grow_spec.lua b/test/functional/legacy/077_mf_hash_grow_spec.lua index 01d916ef04..825f08e968 100644 --- a/test/functional/legacy/077_mf_hash_grow_spec.lua +++ b/test/functional/legacy/077_mf_hash_grow_spec.lua @@ -15,7 +15,7 @@ describe('mf_hash_grow()', function() -- Check to see if cksum exists, otherwise skip the test if os.execute('which cksum 2>&1 > /dev/null') ~= 0 then - pending("was not tested because cksum was not found") + pending('was not tested because cksum was not found', function() end) else it('is working', function() execute('set fileformat=unix undolevels=-1') diff --git a/test/functional/legacy/mapping_spec.lua b/test/functional/legacy/mapping_spec.lua index c387d7484c..0843506827 100644 --- a/test/functional/legacy/mapping_spec.lua +++ b/test/functional/legacy/mapping_spec.lua @@ -23,16 +23,27 @@ describe('mapping', function() execute('set langmap=+{ langnoremap') feed('o+<esc>') - -- expr mapping with langmap. + -- Insert mode expr mapping with langmap. execute('inoremap <expr> { "FAIL_iexplangmap"') feed('o+<esc>') + -- langmap should not get remapped in cmdline mode. + execute('cnoremap { FAIL_clangmap') + feed('o+<esc>') + execute('cunmap {') + + -- cmdline mode expr mapping with langmap. + execute('cnoremap <expr> { "FAIL_cexplangmap"') + feed('o+<esc>') + execute('cunmap {') -- Assert buffer contents. expect([[ test starts here: vim + + + + + +]]) end) end) diff --git a/test/functional/runtime/autoload/remote/define_spec.lua b/test/functional/provider/define_spec.lua index 9b97ed84d9..9b97ed84d9 100644 --- a/test/functional/runtime/autoload/remote/define_spec.lua +++ b/test/functional/provider/define_spec.lua diff --git a/test/functional/runtime/autoload/provider/python3_spec.lua b/test/functional/provider/python3_spec.lua index 43889b7b2a..5be5390370 100644 --- a/test/functional/runtime/autoload/provider/python3_spec.lua +++ b/test/functional/provider/python3_spec.lua @@ -1,19 +1,19 @@ do - local proc = - io.popen([[python3 -c 'import neovim, sys; sys.stdout.write("ok")' 2> /dev/null]]) + local proc = io.popen( + [[python3 -c 'import neovim, sys; sys.stdout.write("ok")' 2> /dev/null]]) if proc:read() ~= 'ok' then - -- Don't run these tests if python3 is not available + pending( + 'python3 (or the python3 neovim module) is broken or missing', + function() end) return end end - local helpers = require('test.functional.helpers') local eval, command, feed = helpers.eval, helpers.command, helpers.feed local eq, clear, insert = helpers.eq, helpers.clear, helpers.insert local expect, write_file = helpers.expect, helpers.write_file - describe('python3 commands and functions', function() before_each(function() clear() diff --git a/test/functional/runtime/autoload/provider/python_spec.lua b/test/functional/provider/python_spec.lua index e71a7a4309..ec1a853546 100644 --- a/test/functional/runtime/autoload/provider/python_spec.lua +++ b/test/functional/provider/python_spec.lua @@ -1,19 +1,19 @@ do - local proc = - io.popen([[python -c 'import neovim, sys; sys.stdout.write("ok")' 2> /dev/null]]) + local proc = io.popen( + [[python -c 'import neovim, sys; sys.stdout.write("ok")' 2> /dev/null]]) if proc:read() ~= 'ok' then - -- Don't run these tests if python is not available + pending( + 'python (or the python neovim module) is broken or missing', + function() end) return end end - local helpers = require('test.functional.helpers') local eval, command, feed = helpers.eval, helpers.command, helpers.feed local eq, clear, insert = helpers.eq, helpers.clear, helpers.insert local expect, write_file = helpers.expect, helpers.write_file - describe('python commands and functions', function() before_each(function() clear() diff --git a/test/functional/shell/viml_system_spec.lua b/test/functional/shell/viml_system_spec.lua index 6e10715612..4985c24aec 100644 --- a/test/functional/shell/viml_system_spec.lua +++ b/test/functional/shell/viml_system_spec.lua @@ -186,7 +186,7 @@ describe('system()', function() describe("with a program that doesn't close stdout", function() if not xclip then - pending('skipped (missing xclip)') + pending('skipped (missing xclip)', function() end) else it('will exit properly after passing input', function() eq('', eval([[system('xclip -i -selection clipboard', 'clip-data')]])) @@ -365,7 +365,7 @@ describe('systemlist()', function() describe("with a program that doesn't close stdout", function() if not xclip then - pending('skipped (missing xclip)') + pending('skipped (missing xclip)', function() end) else it('will exit properly after passing input', function() eq({}, eval( diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua index 60198bb008..1d616ed853 100644 --- a/test/functional/ui/screen.lua +++ b/test/functional/ui/screen.lua @@ -340,12 +340,9 @@ function Screen:_handle_mouse_off() self._mouse_enabled = false end -function Screen:_handle_insert_mode() - self._mode = 'insert' -end - -function Screen:_handle_normal_mode() - self._mode = 'normal' +function Screen:_handle_mode_change(mode) + assert(mode == 'insert' or mode == 'replace' or mode == 'normal') + self._mode = mode end function Screen:_handle_set_scroll_region(top, bot, left, right) diff --git a/test/unit/fixtures/queue.c b/test/unit/fixtures/queue.c new file mode 100644 index 0000000000..bbb6274b21 --- /dev/null +++ b/test/unit/fixtures/queue.c @@ -0,0 +1,16 @@ +#include <string.h> +#include <stdlib.h> +#include "nvim/event/queue.h" +#include "queue.h" + + +void ut_queue_put(Queue *queue, const char *str) +{ + queue_put(queue, NULL, 1, str); +} + +const char *ut_queue_get(Queue *queue) +{ + Event event = queue_get(queue); + return event.argv[0]; +} diff --git a/test/unit/fixtures/queue.h b/test/unit/fixtures/queue.h new file mode 100644 index 0000000000..ae949c9f29 --- /dev/null +++ b/test/unit/fixtures/queue.h @@ -0,0 +1,4 @@ +#include "nvim/event/queue.h" + +void ut_queue_put(Queue *queue, const char *str); +const char *ut_queue_get(Queue *queue); diff --git a/test/unit/os/fs_spec.lua b/test/unit/os/fs_spec.lua index 20aca9109e..28e831229f 100644 --- a/test/unit/os/fs_spec.lua +++ b/test/unit/os/fs_spec.lua @@ -32,6 +32,14 @@ local directory = nil local absolute_executable = nil local executable_name = nil +local function set_bit(number, to_set) + return bit.bor(number, to_set) +end + +local function unset_bit(number, to_unset) + return bit.band(number, (bit.bnot(to_unset))) +end + local function assert_file_exists(filepath) neq(nil, lfs.attributes(filepath)) end @@ -40,11 +48,24 @@ local function assert_file_does_not_exist(filepath) eq(nil, lfs.attributes(filepath)) end +local function os_setperm(filename, perm) + return fs.os_setperm((to_cstr(filename)), perm) +end + +local function os_getperm(filename) + local perm = fs.os_getperm((to_cstr(filename))) + return tonumber(perm) +end + describe('fs function', function() + local orig_test_file_perm setup(function() lfs.mkdir('unit-test-directory'); + io.open('unit-test-directory/test.file', 'w').close() + orig_test_file_perm = os_getperm('unit-test-directory/test.file') + io.open('unit-test-directory/test_2.file', 'w').close() lfs.link('test.file', 'unit-test-directory/test_link.file', true) -- Since the tests are executed, they are called by an executable. We use @@ -188,6 +209,10 @@ describe('fs function', function() end) describe('file permissions', function() + before_each(function() + os_setperm('unit-test-directory/test.file', orig_test_file_perm) + end) + local function os_getperm(filename) local perm = fs.os_getperm((to_cstr(filename))) return tonumber(perm) @@ -204,8 +229,8 @@ describe('fs function', function() return res end - local function os_file_is_readonly(filename) - return fs.os_file_is_readonly((to_cstr(filename))) + local function os_file_is_readable(filename) + return fs.os_file_is_readable((to_cstr(filename))) end local function os_file_is_writable(filename) @@ -216,14 +241,6 @@ describe('fs function', function() return 0 ~= (bit.band(number, check_bit)) end - local function set_bit(number, to_set) - return bit.bor(number, to_set) - end - - local function unset_bit(number, to_unset) - return bit.band(number, (bit.bnot(to_unset))) - end - describe('os_getperm', function() it('returns -1 when the given file does not exist', function() eq(-1, (os_getperm('non-existing-file'))) @@ -270,7 +287,7 @@ describe('fs function', function() -- Some systems may not have `id` utility. if (os.execute('id -G > /dev/null 2>&1') ~= 0) then - pending('skipped (missing `id` utility)') + pending('skipped (missing `id` utility)', function() end) else it('owner of a file may change the group of the file to any group of which that owner is a member', function() local file_gid = lfs.attributes(filename, 'gid') @@ -293,10 +310,8 @@ describe('fs function', function() end) end - -- On Windows `os_fchown` always returns 0 - -- because `uv_fs_chown` is no-op on this platform. if (ffi.os == 'Windows' or ffi.C.geteuid() == 0) then - pending('skipped (os_fchown is no-op on Windows)') + pending('skipped (uv_fs_chown is no-op on Windows)', function() end) else it('returns nonzero if process has not enough permissions', function() -- chown to root @@ -305,33 +320,36 @@ describe('fs function', function() end end) - describe('os_file_is_readonly', function() - it('returns true if the file is readonly', function() + + describe('os_file_is_readable', function() + it('returns false if the file is not readable', function() local perm = os_getperm('unit-test-directory/test.file') - local perm_orig = perm - perm = unset_bit(perm, ffi.C.kS_IWUSR) - perm = unset_bit(perm, ffi.C.kS_IWGRP) - perm = unset_bit(perm, ffi.C.kS_IWOTH) + perm = unset_bit(perm, ffi.C.kS_IRUSR) + perm = unset_bit(perm, ffi.C.kS_IRGRP) + perm = unset_bit(perm, ffi.C.kS_IROTH) eq(OK, (os_setperm('unit-test-directory/test.file', perm))) - eq(true, os_file_is_readonly('unit-test-directory/test.file')) - eq(OK, (os_setperm('unit-test-directory/test.file', perm_orig))) + eq(false, os_file_is_readable('unit-test-directory/test.file')) + end) + + it('returns false if the file does not exist', function() + eq(false, os_file_is_readable( + 'unit-test-directory/what_are_you_smoking.gif')) end) - it('returns false if the file is writable', function() - eq(false, os_file_is_readonly('unit-test-directory/test.file')) + it('returns true if the file is readable', function() + eq(true, os_file_is_readable( + 'unit-test-directory/test.file')) end) end) describe('os_file_is_writable', function() it('returns 0 if the file is readonly', function() local perm = os_getperm('unit-test-directory/test.file') - local perm_orig = perm perm = unset_bit(perm, ffi.C.kS_IWUSR) perm = unset_bit(perm, ffi.C.kS_IWGRP) perm = unset_bit(perm, ffi.C.kS_IWOTH) eq(OK, (os_setperm('unit-test-directory/test.file', perm))) eq(0, os_file_is_writable('unit-test-directory/test.file')) - eq(OK, (os_setperm('unit-test-directory/test.file', perm_orig))) end) it('returns 1 if the file is writable', function() diff --git a/test/unit/queue_spec.lua b/test/unit/queue_spec.lua new file mode 100644 index 0000000000..9326c1cad6 --- /dev/null +++ b/test/unit/queue_spec.lua @@ -0,0 +1,123 @@ +local helpers = require("test.unit.helpers") + +local ffi = helpers.ffi +local eq = helpers.eq + +local queue = helpers.cimport("./test/unit/fixtures/queue.h") + +describe('queue', function() + local parent, child1, child2, child3 + + local function put(q, str) + queue.ut_queue_put(q, str) + end + + local function get(q) + return ffi.string(queue.ut_queue_get(q)) + end + + local function free(q) + queue.queue_free(q) + end + + before_each(function() + parent = queue.queue_new_parent(ffi.NULL, ffi.NULL) + child1 = queue.queue_new_child(parent) + child2 = queue.queue_new_child(parent) + child3 = queue.queue_new_child(parent) + put(child1, 'c1i1') + put(child1, 'c1i2') + put(child2, 'c2i1') + put(child1, 'c1i3') + put(child2, 'c2i2') + put(child2, 'c2i3') + put(child2, 'c2i4') + put(child3, 'c3i1') + put(child3, 'c3i2') + end) + + it('removing from parent removes from child', function() + eq('c1i1', get(parent)) + eq('c1i2', get(parent)) + eq('c2i1', get(parent)) + eq('c1i3', get(parent)) + eq('c2i2', get(parent)) + eq('c2i3', get(parent)) + eq('c2i4', get(parent)) + end) + + it('removing from child removes from parent', function() + eq('c2i1', get(child2)) + eq('c2i2', get(child2)) + eq('c1i1', get(child1)) + eq('c1i2', get(parent)) + eq('c1i3', get(parent)) + eq('c2i3', get(parent)) + eq('c2i4', get(parent)) + end) + + it('removing from child at the beginning of parent', function() + eq('c1i1', get(child1)) + eq('c1i2', get(child1)) + eq('c2i1', get(parent)) + end) + + it('removing from parent after get from parent and put to child', function() + eq('c1i1', get(parent)) + eq('c1i2', get(parent)) + eq('c2i1', get(parent)) + eq('c1i3', get(parent)) + eq('c2i2', get(parent)) + eq('c2i3', get(parent)) + eq('c2i4', get(parent)) + eq('c3i1', get(parent)) + put(child1, 'c1i11') + put(child1, 'c1i22') + eq('c3i2', get(parent)) + eq('c1i11', get(parent)) + eq('c1i22', get(parent)) + end) + + it('removing from parent after get and put to child', function() + eq('c1i1', get(child1)) + eq('c1i2', get(child1)) + eq('c2i1', get(child2)) + eq('c1i3', get(child1)) + eq('c2i2', get(child2)) + eq('c2i3', get(child2)) + eq('c2i4', get(child2)) + eq('c3i1', get(child3)) + eq('c3i2', get(parent)) + put(child1, 'c1i11') + put(child2, 'c2i11') + put(child1, 'c1i12') + eq('c2i11', get(child2)) + eq('c1i11', get(parent)) + eq('c1i12', get(parent)) + end) + + it('put after removing from child at the end of parent', function() + eq('c3i1', get(child3)) + eq('c3i2', get(child3)) + put(child1, 'c1i11') + put(child2, 'c2i11') + eq('c1i1', get(parent)) + eq('c1i2', get(parent)) + eq('c2i1', get(parent)) + eq('c1i3', get(parent)) + eq('c2i2', get(parent)) + eq('c2i3', get(parent)) + eq('c2i4', get(parent)) + eq('c1i11', get(parent)) + eq('c2i11', get(parent)) + end) + + it('removes from parent queue when child is freed', function() + free(child2) + eq('c1i1', get(parent)) + eq('c1i2', get(parent)) + eq('c1i3', get(parent)) + eq('c3i1', get(child3)) + eq('c3i2', get(child3)) + end) +end) |