" Script Name: mark.vim
" Description: Highlight several words in different colors simultaneously.
"
" Copyright:   (C) 2008-2015 Ingo Karkat
"              (C) 2005-2008 Yuheng Xie
"   The VIM LICENSE applies to this script; see ':help copyright'.
"
" Maintainer:  Ingo Karkat <ingo@karkat.de>
"
" Dependencies:
"	- ingo/cmdargs/pattern.vim autoload script
"	- ingo/err.vim autoload script
"	- ingo/msg.vim autoload script
"	- SearchSpecial.vim autoload script (optional, for improved search messages).
"
" Version:     3.0.0
" Changes:
" 26-May-2015 Ingo Karkat
" - CHG: Parse :Mark arguments as either /{pattern}/ or whole {word}.
" - Adapt mark#YankDefinitions(), too.
" - ENH: Add mark#SetName(). Adapt mark#List() to show mark names (if set).
"   Adapt mark persistence functions to save / restore mark name within the List
"   of marks (as an object {'pattern': ..., 'name': ...}) if defined; mostly via
"   s:SerializeMark() and s:DeserializeMark(). Keep old List of Strings format
"   for marks without names, as this is shorter and will likely remain the most
"   common use. Parse optional " as [name]" in mark#SetMark(). Add optional a:2
"   / a:name argument to mark#DoMark(); because it prints a message of the added
"   / cleared mark (which should include the set / updated {name}), this cannot
"   be done in the mark#SetMark() wrapper afterwards.
" - Indicate current mark name for current-mark searches. Rename "mark-*" search
"   type and append the actual current mark (plus optional name) to it:
"   "mark*3:foo".
" - Rename mark#ToPatternList() to mark#ToList().
" - CHG: Duplicate mark#GetNum() and mark#GetGroupNum(). Rename the former into
"   mark#GetCount() and have it return the number of actually defined (i.e.
"   non-empty) marks.
" - FIX: mark#DoMarkAndSetCurrent() error case of invalid regexp needs to return
"   List [0, 0] (just like subordinate mark#DoMark()) instead of Number 0.
"
" 19-May-2015, Ingo Karkat
" - Properly abort on error by using :echoerr. Use ingo/err.vim for the
"   implementation. Retire mark#ErrorMsg() and mark#WarningMsg().
"
" 16-May-2015, Ingo Karkat
" - Move implementation of cascading search to separate autoload script.
" - Expose various utility functions for re-use in mark/cascade.vim.
"
" 15-May-2015, Ingo Karkat
" - ENH: Add mark#SearchNextGroup().
"
" 16-Apr-2015, Ingo Karkat
" - ENH: Add mark#YankDefinitions().
"
" 30-Jan-2015, Ingo Karkat
" - ENH: Keep previous (last accessed) window on :windo.
" - Consistently use :noautocmd during window iteration.
"
" 16-Jun-2014, Ingo Karkat
" - To avoid accepting an invalid regular expression (e.g. "\(blah") and then
"   causing ugly errors on every mark update, check the patterns passed by the
"   user for validity.
" - Introduce mark#SetMark() for the :Mark command, so that it doesn't query a
"   mark when the passed mark group doesn't exist (interactivity in Ex commands
"   is unexpected). Instead, return an error.
"
" 23-May-2014, Ingo Karkat
" - The additional mapping described under :help mark-whitespace-indifferent got
"   broken again by the refactoring of mark#DoMark() on 31-Jan-2013. Finally
"   include this in the script as <Plug>MarkIWhiteSet and
"   mark#GetVisualSelectionAsLiteralWhitespaceIndifferentPattern().
"
" 20-Jun-2013, Ingo Karkat
" - ENH: Implement command completion for :[N]Mark that offers existing mark
"   patterns (from group [N] / all groups), both as one regular expression and
"	individual alternatives. The leading \< can be omitted.
"
" 29-May-2013, Ingo Karkat
" - Factor out s:HasVariablePersistence() and include the note in :MarkLoad,
"   too.
" - Use s:ErrorMsg() everywhere, and allow to suppress the output via optional
"   flag.
" - Define s:WarningMsg(), too; we now issue them in two locations.
" - ENH: mark#LoadCommand() and mark#SaveCommand() now take an optional marks
"   variable name to store multiple named marks (and persist them if the name is
"   all uppercase). Allow completion via mark#MarksVariablesComplete().
"
" 31-Jan-2013, Ingo Karkat
" - mark#MarkRegex() takes an additional a:groupNum argument to also allow a
"   [count] for <Leader>r.
" - Add mark#DoMarkAndSetCurrent() variant of mark#DoMark() that also sets the
"   current mark to the used mark group when a mark was set. Use that for
"   <Leader>r and :Mark so that it is easier to determine whether the entered
"   pattern actually matches anywhere. Thanks to Xiaopan Zhang for notifying me
"   about this problem. mark#DoMark() now returns a List of [success,
"   markGroupNum] to enable the wrapper.
" - Let the various search functions return whether the search succeeded. Though
"   I don't use this (the mark search shouldn't beep like built-in n / N), it
"   may come handy one day.
" - Add mark#SearchGroupMark() to be able to search for a particular mark group
"   (or the current if none specified), with a specified count.
"
" 15-Oct-2012, Ingo Karkat
" - Issue an error message "No marks defined" instead of moving the cursor by
"   one character when there are no marks (e.g. initially or after :MarkClear).
" - Enable custom integrations via new mark#GetNum() and mark#GetPattern()
"   functions.
"
" 13-Sep-2012, Ingo Karkat
" - Enable alternative * / # mappings that do not remember the last search type
"   by adding optional search function argument to mark#SearchNext().
"
" 04-Jul-2012, Ingo Karkat
" - ENH: Handle on-the-fly change of mark highlighting via mark#ReInit(), which
"   truncates / expands s:pattern and corrects the indices. Also, w:mwMatch List
"   size mismatches must be handled in s:MarkMatch().
"
" 23-Apr-2012, Ingo Karkat + fanhe
" - Force case via \c / \C instead of temporarily unsetting 'smartcase'.
" - Allow to override 'ignorecase' setting via g:mwIgnoreCase. Thanks to fanhe
"   for the idea and sending a patch.
"
" 26-Mar-2012, Ingo Karkat
" - ENH: When a [count] exceeding the number of available mark groups is given,
"   a summary of marks is given and the user is asked to select a mark group.
"   This allows to interactively choose a color via 99<Leader>m.
" - ENH: Include count of alternative patterns in :Marks list.
" - CHG: Use ">" for next mark and "/" for last search in :Marks.
"
" 23-Mar-2012, Ingo Karkat
" - ENH: Add :Marks command that prints all mark highlight groups and their
"   search patterns, plus information about the current search mark, next mark
"   group, and whether marks are disabled.
" - ENH: Show which mark group a pattern was set / added / removed / cleared.
" - Refactoring: Store index into s:pattern instead of pattern itself in
"   s:lastSearch. For that, mark#CurrentMark() now additionally returns the
"   index.
" - CHG: Show mark group number in same-mark search and rename search types from
"   "any-mark", "same-mark", and "new-mark" to the shorter "mark-*", "mark-N",
"   and "mark-N!", respectively.
"
" 22-Mar-2012, Ingo Karkat
" - ENH: Allow [count] for <Leader>m and :Mark to add / subtract match to / from
"   highlight group [count], and use [count]<Leader>n to clear only highlight
"   group [count]. This was also requested by Philipp Marek.
" - FIX: :Mark and <Leader>n actually toggled marks back on when they were
"   already off. Now, they stay off on multiple invocations. Use :call
"   mark#Toggle() / <Plug>MarkToggle if you want toggling.
"
" 09-Nov-2011, Ingo Karkat
" - BUG: With a single match and 'wrapscan' set, a search error was issued
"   instead of the wrap message. Add check for l:isStuckAtCurrentMark &&
"   l:isWrapped in the no-match part of s:Search().
" - FIX: In backwards search with single match, the :break short-circuits the
"   l:isWrapped logic, resets l:line and therefore also confuses the logic and
"   leads to wrong error message instead of wrap message. Don't reset l:line,
"   set l:isWrapped instead.
" - FIX: Wrong logic for determining l:isWrapped lets wrap-around go undetected
"   when v:count >= number of total matches. [l:startLine, l:startCol] must
"   be updated on every iteration, and should therefore be named [l:prevLine,
"   l:prevCol].
"
" 17-May-2011, Ingo Karkat
" - Make s:GetVisualSelection() public to allow use in suggested
"   <Plug>MarkSpaceIndifferent vmap.
" - FIX: == comparison in s:DoMark() leads to wrong regexp (\A vs. \a) being
"   cleared when 'ignorecase' is set. Use case-sensitive comparison ==# instead.
"
" 10-May-2011, Ingo Karkat
" - Refine :MarkLoad messages: Differentiate between nonexistent and empty
"   g:MARK_MARKS; add note when marks are disabled.
"
" 06-May-2011, Ingo Karkat
" - Also print status message on :MarkClear to be consistent with :MarkToggle.
"
" 21-Apr-2011, Ingo Karkat
" - Implement toggling of mark display (keeping the mark patterns, unlike the
"   clearing of marks), determined by s:enable. s:DoMark() now toggles on empty
"   regexp, affecting the \n mapping and :Mark. Introduced
"   s:EnableAndMarkScope() wrapper to correctly handle the highlighting updates
"   depending on whether marks were previously disabled.
" - Implement persistence of s:enable via g:MARK_ENABLED.
" - Generalize s:Enable() and combine with intermediate s:Disable() into
"   s:MarkEnable(), which also performs the persistence of s:enabled.
" - Implement lazy-loading of disabled persistent marks via g:mwDoDeferredLoad
"   flag passed from plugin/mark.vim.
"
" 20-Apr-2011, Ingo Karkat
" - Extract setting of s:pattern into s:SetPattern() and implement the automatic
"   persistence there.
"
" 19-Apr-2011, Ingo Karkat
" - ENH: Add enabling functions for mark persistence: mark#Load() and
"   mark#ToPatternList().
" - Implement :MarkLoad and :MarkSave commands in mark#LoadCommand() and
"   mark#SaveCommand().
" - Remove superfluous update autocmd on VimEnter: Persistent marks trigger the
"   update themselves, same for :Mark commands which could potentially be issued
"   e.g. in .vimrc. Otherwise, when no marks are defined after startup, the
"   autosource script isn't even loaded yet, so the autocmd on the VimEnter
"   event isn't yet defined.
"
" 18-Apr-2011, Ingo Karkat
" - BUG: Include trailing newline character in check for current mark, so that a
"   mark that matches the entire line (e.g. created by V<Leader>m) can be
"   cleared via <Leader>n. Thanks to ping for reporting this.
" - Minor restructuring of mark#MarkCurrentWord().
" - FIX: On overlapping marks, mark#CurrentMark() returned the lowest, not the
"   highest visible mark. So on overlapping marks, the one that was not visible
"   at the cursor position was removed; very confusing! Use reverse iteration
"   order.
" - FIX: To avoid an arbitrary ordering of highlightings when the highlighting
"   group names roll over, and to avoid order inconsistencies across different
"   windows and tabs, we assign a different priority based on the highlighting
"   group.
" - Rename s:cycleMax to s:markNum; the previous name was too
"   implementation-focused and off-by-one with regards to the actual value.
"
" 16-Apr-2011, Ingo Karkat
" - Move configuration variable g:mwHistAdd to plugin/mark.vim (as is customary)
"   and make the remaining g:mw... variables script-local, as these contain
"   internal housekeeping information that does not need to be accessible by the
"   user.
" - Add :MarkSave warning if 'viminfo' doesn't enable global variable
"   persistence.
"
" 15-Apr-2011, Ingo Karkat
" - Robustness: Move initialization of w:mwMatch from mark#UpdateMark() to
"   s:MarkMatch(), where the variable is actually used. I had encountered cases
"   where it w:mwMatch was undefined when invoked through mark#DoMark() ->
"   s:MarkScope() -> s:MarkMatch(). This can be forced by :unlet w:mwMatch
"   followed by :Mark foo.
" - Robustness: Checking for s:markNum == 0 in mark#DoMark(), trying to
"   re-detect the mark highlightings and finally printing an error instead of
"   choking. This can happen when somehow no mark highlightings are defined.
"
" 14-Jan-2011, Ingo Karkat
" - FIX: Capturing the visual selection could still clobber the blockwise yank
"   mode of the unnamed register.
"
" 13-Jan-2011, Ingo Karkat
" - FIX: Using a named register for capturing the visual selection on
"   {Visual}<Leader>m and {Visual}<Leader>r clobbered the unnamed register. Now
"   using the unnamed register.
"
" 13-Jul-2010, Ingo Karkat
" - ENH: The MarkSearch mappings (<Leader>[*#/?]) add the original cursor
"   position to the jump list, like the built-in [/?*#nN] commands. This allows
"   to use the regular jump commands for mark matches, like with regular search
"   matches.
"
" 19-Feb-2010, Andy Wokula
" - BUG: Clearing of an accidental zero-width match (e.g. via :Mark \zs) results
"   in endless loop. Thanks to Andy Wokula for the patch.
"
" 17-Nov-2009, Ingo Karkat + Andy Wokula
" - BUG: Creation of literal pattern via '\V' in {Visual}<Leader>m mapping
"   collided with individual escaping done in <Leader>m mapping so that an
"   escaped '\*' would be interpreted as a multi item when both modes are used
"   for marking. Replaced \V with s:EscapeText() to be consistent. Replaced the
"   (overly) generic mark#GetVisualSelectionEscaped() with
"   mark#GetVisualSelectionAsRegexp() and
"   mark#GetVisualSelectionAsLiteralPattern(). Thanks to Andy Wokula for the
"   patch.
"
" 06-Jul-2009, Ingo Karkat
" - Re-wrote s:AnyMark() in functional programming style.
" - Now resetting 'smartcase' before the search, this setting should not be
"   considered for *-command-alike searches and cannot be supported because all
"   mark patterns are concatenated into one large regexp, anyway.
"
" 04-Jul-2009, Ingo Karkat
" - Re-wrote s:Search() to handle v:count:
"   - Obsoleted s:current_mark_position; mark#CurrentMark() now returns both the
"     mark text and start position.
"   - s:Search() now checks for a jump to the current mark during a backward
"     search; this eliminates a lot of logic at its calling sites.
"   - Reverted negative logic at calling sites; using empty() instead of != "".
"   - Now passing a:isBackward instead of optional flags into s:Search() and
"     around its callers.
"   - ':normal! zv' moved from callers into s:Search().
" - Removed delegation to SearchSpecial#ErrorMessage(), because the fallback
"   implementation is perfectly fine and the SearchSpecial routine changed its
"   output format into something unsuitable anyway.
" - Using descriptive text instead of "@" (and appropriate highlighting) when
"   querying for the pattern to mark.
"
" 02-Jul-2009, Ingo Karkat
" - Split off functions into autoload script.

"- functions ------------------------------------------------------------------

silent! call SearchSpecial#DoesNotExist()	" Execute a function to force autoload.
if exists('*SearchSpecial#WrapMessage')
	function! s:WrapMessage( searchType, searchPattern, isBackward )
		redraw
		call SearchSpecial#WrapMessage(a:searchType, a:searchPattern, a:isBackward)
	endfunction
	function! s:EchoSearchPattern( searchType, searchPattern, isBackward )
		call SearchSpecial#EchoSearchPattern(a:searchType, a:searchPattern, a:isBackward)
	endfunction
else
	function! s:Trim( message )
		" Limit length to avoid "Hit ENTER" prompt.
		return strpart(a:message, 0, (&columns / 2)) . (len(a:message) > (&columns / 2) ? "..." : "")
	endfunction
	function! s:WrapMessage( searchType, searchPattern, isBackward )
		redraw
		let v:warningmsg = printf('%s search hit %s, continuing at %s', a:searchType, (a:isBackward ? 'TOP' : 'BOTTOM'), (a:isBackward ? 'BOTTOM' : 'TOP'))
		echohl WarningMsg
		echo s:Trim(v:warningmsg)
		echohl None
	endfunction
	function! s:EchoSearchPattern( searchType, searchPattern, isBackward )
		let l:message = (a:isBackward ? '?' : '/') .  a:searchPattern
		echohl SearchSpecialSearchType
		echo a:searchType
		echohl None
		echon s:Trim(l:message)
	endfunction
endif

function! s:EscapeText( text )
	return substitute( escape(a:text, '\' . '^$.*[~'), "\n", '\\n', 'ge' )
endfunction
function! s:IsIgnoreCase( expr )
	return ((exists('g:mwIgnoreCase') ? g:mwIgnoreCase : &ignorecase) && a:expr !~# '\\\@<!\\C')
endfunction
" Mark the current word, like the built-in star command.
" If the cursor is on an existing mark, remove it.
function! mark#MarkCurrentWord( groupNum )
	let l:regexp = (a:groupNum == 0 ? mark#CurrentMark()[0] : '')
	if empty(l:regexp)
		let l:cword = expand('<cword>')
		if ! empty(l:cword)
			let l:regexp = s:EscapeText(l:cword)
			" The star command only creates a \<whole word\> search pattern if the
			" <cword> actually only consists of keyword characters.
			if l:cword =~# '^\k\+$'
				let l:regexp = '\<' . l:regexp . '\>'
			endif
		endif
	endif
	return (empty(l:regexp) ? 0 : mark#DoMark(a:groupNum, l:regexp)[0])
endfunction

function! mark#GetVisualSelection()
	let save_clipboard = &clipboard
	set clipboard= " Avoid clobbering the selection and clipboard registers.
	let save_reg = getreg('"')
	let save_regmode = getregtype('"')
	silent normal! gvy
	let res = getreg('"')
	call setreg('"', save_reg, save_regmode)
	let &clipboard = save_clipboard
	return res
endfunction
function! mark#GetVisualSelectionAsLiteralPattern()
	return s:EscapeText(mark#GetVisualSelection())
endfunction
function! mark#GetVisualSelectionAsRegexp()
	return substitute(mark#GetVisualSelection(), '\n', '', 'g')
endfunction
function! mark#GetVisualSelectionAsLiteralWhitespaceIndifferentPattern()
	return substitute(escape(mark#GetVisualSelection(), '\' . '^$.*[~'), '\_s\+', '\\_s\\+', 'g')
endfunction

" Manually input a regular expression.
function! mark#MarkRegex( groupNum, regexpPreset )
	call inputsave()
		echohl Question
			let l:regexp = input('Input pattern to mark: ', a:regexpPreset)
		echohl None
	call inputrestore()
	if empty(l:regexp)
		call ingo#err#Clear()
		return 0
	endif

	redraw " This is necessary when the user is queried for the mark group.
	return mark#DoMarkAndSetCurrent(a:groupNum, l:regexp)[0]
endfunction

function! s:Cycle( ... )
	let l:currentCycle = s:cycle
	let l:newCycle = (a:0 ? a:1 : s:cycle) + 1
	let s:cycle = (l:newCycle < s:markNum ? l:newCycle : 0)
	return l:currentCycle
endfunction
function! s:FreeGroupIndex()
	let i = 0
	while i < s:markNum
		if empty(s:pattern[i])
			return i
		endif
		let i += 1
	endwhile
	return -1
endfunction
function! mark#NextUsedGroupIndex( isBackward, isWrapAround, startIndex, count )
	if a:isBackward
		let l:indices = range(a:startIndex - 1, 0, -1)
		if a:isWrapAround
			let l:indices += range(s:markNum - 1, a:startIndex + 1, -1) :
		endif
	else
		let l:indices = range(a:startIndex + 1, s:markNum - 1)
		if a:isWrapAround
			let l:indices += range(0, max([-1, a:startIndex - 1]))
		endif
	endif

	let l:count = a:count
	for l:i in l:indices
		if ! empty(s:pattern[l:i])
			let l:count -= 1
			if l:count == 0
				return l:i
			endif
		endif
	endfor
	return -1
endfunction

" Set match / clear matches in the current window.
function! s:MarkMatch( indices, expr )
	if ! exists('w:mwMatch')
		let w:mwMatch = repeat([0], s:markNum)
	elseif len(w:mwMatch) != s:markNum
		" The number of marks has changed.
		if len(w:mwMatch) > s:markNum
			" Truncate the matches.
			for l:match in filter(w:mwMatch[s:markNum : ], 'v:val > 0')
				silent! call matchdelete(l:match)
			endfor
			let w:mwMatch = w:mwMatch[0 : (s:markNum - 1)]
		else
			" Expand the matches.
			let w:mwMatch += repeat([0], (s:markNum - len(w:mwMatch)))
		endif
	endif

	for l:index in a:indices
		if w:mwMatch[l:index] > 0
			silent! call matchdelete(w:mwMatch[l:index])
			let w:mwMatch[l:index] = 0
		endif
	endfor

	if ! empty(a:expr)
		let l:index = a:indices[0]	" Can only set one index for now.

		" Info: matchadd() does not consider the 'magic' (it's always on),
		" 'ignorecase' and 'smartcase' settings.
		" Make the match according to the 'ignorecase' setting, like the star command.
		" (But honor an explicit case-sensitive regexp via the /\C/ atom.)
		let l:expr = (s:IsIgnoreCase(a:expr) ? '\c' : '') . a:expr

		" To avoid an arbitrary ordering of highlightings, we assign a different
		" priority based on the highlight group, and ensure that the highest
		" priority is -10, so that we do not override the 'hlsearch' of 0, and still
		" allow other custom highlightings to sneak in between.
		let l:priority = -10 - s:markNum + 1 + l:index

		let w:mwMatch[l:index] = matchadd('MarkWord' . (l:index + 1), l:expr, l:priority)
	endif
endfunction
" Initialize mark colors in a (new) window.
function! mark#UpdateMark()
	let i = 0
	while i < s:markNum
		if ! s:enabled || empty(s:pattern[i])
			call s:MarkMatch([i], '')
		else
			call s:MarkMatch([i], s:pattern[i])
		endif
		let i += 1
	endwhile
endfunction
" Set / clear matches in all windows.
function! s:MarkScope( indices, expr )
	" By entering a window, its height is potentially increased from 0 to 1 (the
	" minimum for the current window). To avoid any modification, save the window
	" sizes and restore them after visiting all windows.
	let l:originalWindowLayout = winrestcmd()
		let l:originalWinNr = winnr()
		let l:previousWinNr = winnr('#') ? winnr('#') : 1
			noautocmd windo call s:MarkMatch(a:indices, a:expr)
		noautocmd execute l:previousWinNr . 'wincmd w'
		noautocmd execute l:originalWinNr . 'wincmd w'
	silent! execute l:originalWindowLayout
endfunction
" Update matches in all windows.
function! mark#UpdateScope()
	" By entering a window, its height is potentially increased from 0 to 1 (the
	" minimum for the current window). To avoid any modification, save the window
	" sizes and restore them after visiting all windows.
	let l:originalWindowLayout = winrestcmd()
		let l:originalWinNr = winnr()
		let l:previousWinNr = winnr('#') ? winnr('#') : 1
			noautocmd windo call mark#UpdateMark()
		noautocmd execute l:previousWinNr . 'wincmd w'
		noautocmd execute l:originalWinNr . 'wincmd w'
	silent! execute l:originalWindowLayout
endfunction

function! s:MarkEnable( enable, ...)
	if s:enabled != a:enable
		" En-/disable marks and perform a full refresh in all windows, unless
		" explicitly suppressed by passing in 0.
		let s:enabled = a:enable
		if g:mwAutoSaveMarks
			let g:MARK_ENABLED = s:enabled
		endif

		if ! a:0 || ! a:1
			call mark#UpdateScope()
		endif
	endif
endfunction
function! s:EnableAndMarkScope( indices, expr )
	if s:enabled
		" Marks are already enabled, we just need to push the changes to all
		" windows.
		call s:MarkScope(a:indices, a:expr)
	else
		call s:MarkEnable(1)
	endif
endfunction

" Toggle visibility of marks, like :nohlsearch does for the regular search
" highlighting.
function! mark#Toggle()
	if s:enabled
		call s:MarkEnable(0)
		echo 'Disabled marks'
	else
		call s:MarkEnable(1)

		let l:markCnt = mark#GetCount()
		echo 'Enabled' (l:markCnt > 0 ? l:markCnt . ' ' : '') . 'marks'
	endif
endfunction


" Mark or unmark a regular expression.
function! s:SetPattern( index, pattern )
	let s:pattern[a:index] = a:pattern

	if g:mwAutoSaveMarks
		call s:SavePattern()
	endif
endfunction
function! mark#ClearAll()
	let i = 0
	let indices = []
	while i < s:markNum
		if ! empty(s:pattern[i])
			call s:SetPattern(i, '')
			call add(indices, i)
		endif
		let i += 1
	endwhile
	let s:lastSearch = -1

	" Re-enable marks; not strictly necessary, since all marks have just been
	" cleared, and marks will be re-enabled, anyway, when the first mark is
	" added. It's just more consistent for mark persistence. But save the full
	" refresh, as we do the update ourselves.
	call s:MarkEnable(0, 0)

	call s:MarkScope(l:indices, '')

	if len(indices) > 0
		echo 'Cleared all' len(indices) 'marks'
	else
		echo 'All marks cleared'
	endif
endfunction
function! s:SetMark( index, regexp, ... )
	if a:0
		if s:lastSearch == a:index
			let s:lastSearch = a:1
		endif
	endif
	call s:SetPattern(a:index, a:regexp)
	call s:EnableAndMarkScope([a:index], a:regexp)
endfunction
function! s:ClearMark( index )
	" A last search there is reset.
	call s:SetMark(a:index, '', -1)
endfunction
function! s:RenderName( groupNum )
	return (empty(s:names[a:groupNum - 1]) ? '' : ':' . s:names[a:groupNum - 1])
endfunction
function! s:EnrichSearchType( searchType )
	if a:searchType !=# 'mark*'
		return a:searchType
	endif

	let [l:markText, l:markPosition, l:markIndex] = mark#CurrentMark()
	return (l:markIndex >= 0 ? a:searchType . (l:markIndex + 1) .  s:RenderName(l:markIndex + 1) : a:searchType)
endfunction
function! s:RenderMark( groupNum )
	return 'mark-' . a:groupNum . s:RenderName(a:groupNum)
endfunction
function! s:EchoMark( groupNum, regexp )
	call s:EchoSearchPattern(s:RenderMark(a:groupNum), a:regexp, 0)
endfunction
function! s:EchoMarkCleared( groupNum )
	echohl SearchSpecialSearchType
	echo s:RenderMark(a:groupNum)
	echohl None
	echon ' cleared'
endfunction
function! s:EchoMarksDisabled()
	echo 'All marks disabled'
endfunction

function! s:SplitIntoAlternatives( pattern )
	return split(a:pattern, '\%(\%(^\|[^\\]\)\%(\\\\\)*\\\)\@<!\\|')
endfunction

" Return [success, markGroupNum]. success is true when the mark has been set or
" cleared. markGroupNum is the mark group number where the mark was set. It is 0
" if the group was cleared.
function! mark#DoMark( groupNum, ... )
	call ingo#err#Clear()
	if s:markNum <= 0
		" Uh, somehow no mark highlightings were defined. Try to detect them again.
		call mark#Init()
		if s:markNum <= 0
			" Still no mark highlightings; complain.
			call ingo#err#Set('No mark highlightings defined')
			return [0, 0]
		endif
	endif

	let l:groupNum = a:groupNum
	if l:groupNum > s:markNum
		" This highlight group does not exist.
		let l:groupNum = mark#QueryMarkGroupNum()
		if l:groupNum < 1 || l:groupNum > s:markNum
			return [0, 0]
		endif
	endif

	let regexp = (a:0 ? a:1 : '')
	if empty(regexp)
		if l:groupNum == 0
			if a:0
				" :Mark // looks more like a typo than a command to disable all
				" marks; prevent that, and only accept :Mark for it.
				call ingo#err#Set('Do not pass empty pattern to disable all marks')
				return [0, 0]
			endif

			" Disable all marks.
			call s:MarkEnable(0)
			call s:EchoMarksDisabled()
		else
			" Clear the mark represented by the passed highlight group number.
			call s:ClearMark(l:groupNum - 1)
			if a:0 >= 2 | let s:names[l:groupNum - 1] = a:2 | endif
			call s:EchoMarkCleared(l:groupNum)
		endif

		return [1, 0]
	endif

	if l:groupNum == 0
		" Clear the mark if it has been marked.
		let i = 0
		while i < s:markNum
			if regexp ==# s:pattern[i]
				call s:ClearMark(i)
				if a:0 >= 2 | let s:names[i] = a:2 | endif
				call s:EchoMarkCleared(i + 1)
				return [1, 0]
			endif
			let i += 1
		endwhile
	else
		" Add / subtract the pattern as an alternative to the mark represented
		" by the passed highlight group number.
		let existingPattern = s:pattern[l:groupNum - 1]
		if ! empty(existingPattern)
			" Split only on \|, but not on \\|.
			let alternatives = s:SplitIntoAlternatives(existingPattern)
			if index(alternatives, regexp) == -1
				let regexp = existingPattern . '\|' . regexp
			else
				let regexp = join(filter(alternatives, 'v:val !=# regexp'), '\|')
				if empty(regexp)
					call s:ClearMark(l:groupNum - 1)
					if a:0 >= 2 | let s:names[l:groupNum - 1] = a:2 | endif
					call s:EchoMarkCleared(l:groupNum)
					return [1, 0]
				endif
			endif
		endif
	endif

	" add to history
	if stridx(g:mwHistAdd, '/') >= 0
		call histadd('/', regexp)
	endif
	if stridx(g:mwHistAdd, '@') >= 0
		call histadd('@', regexp)
	endif

	if l:groupNum == 0
		let i = s:FreeGroupIndex()
		if i != -1
			" Choose an unused highlight group. The last search is kept untouched.
			call s:Cycle(i)
			call s:SetMark(i, regexp)
		else
			" Choose a highlight group by cycle. A last search there is reset.
			let i = s:Cycle()
			call s:SetMark(i, regexp, -1)
		endif
	else
		let i = l:groupNum - 1
		" Use and extend the passed highlight group. A last search is updated
		" and thereby kept active.
		call s:SetMark(i, regexp, i)
	endif

	if a:0 >= 2 | let s:names[i] = a:2 | endif
	call s:EchoMark(i + 1, regexp)
	return [1, i + 1]
endfunction
" To avoid accepting an invalid regular expression (e.g. "\(blah") and then
" causing ugly errors on every mark update, check the patterns passed by the
" user for validity. (We assume that the expressions generated by the plugin
" itself from literal text are all valid.)
function! s:IsRegexpValid( expr )
	try
		call match('', a:expr)
		return 1
	catch /^Vim\%((\a\+)\)\=:/
		call ingo#err#SetVimException()
		return 0
	endtry
endfunction
function! mark#DoMarkAndSetCurrent( groupNum, ... )
	if a:0 && ! s:IsRegexpValid(a:1)
		return [0, 0]
	endif

	let l:result = call('mark#DoMark', [a:groupNum] + a:000)
	let l:markGroupNum = l:result[1]
	if l:markGroupNum > 0
		let s:lastSearch = l:markGroupNum - 1
	endif

	return l:result
endfunction
function! mark#SetMark( groupNum, ... )
	" For the :Mark command, don't query when the passed mark group doesn't
	" exist (interactivity in Ex commands is unexpected). Instead, return an
	" error.
	if s:markNum > 0 && a:groupNum > s:markNum
		call ingo#err#Set(printf('Only %d mark highlight groups', s:markNum))
		return 0
	endif
	if a:0
		let [l:pattern, l:nameArgument] = ingo#cmdargs#pattern#ParseUnescapedWithLiteralWholeWord(a:1, '\(\s\+as\%(\s\+\(.\{-}\)\)\?\)\?\s*')
		if ! empty(l:nameArgument)
			let l:name = substitute(l:nameArgument, '^\s\+as\s*', '', '')
			return mark#DoMarkAndSetCurrent(a:groupNum, l:pattern, l:name)
		else
			return mark#DoMarkAndSetCurrent(a:groupNum, l:pattern)
		endif
	else
		return mark#DoMarkAndSetCurrent(a:groupNum)
	endif
endfunction

" Return [mark text, mark start position, mark index] of the mark under the
" cursor (or ['', [], -1] if there is no mark).
" The mark can include the trailing newline character that concludes the line,
" but marks that span multiple lines are not supported.
function! mark#CurrentMark()
	let line = getline('.') . "\n"

	" Highlighting groups with higher numbers take precedence over lower numbers,
	" and therefore its marks appear "above" other marks. To retrieve the visible
	" mark in case of overlapping marks, we need to check from highest to lowest
	" highlight group.
	let i = s:markNum - 1
	while i >= 0
		if ! empty(s:pattern[i])
			let matchPattern = (s:IsIgnoreCase(s:pattern[i]) ? '\c' : '\C') . s:pattern[i]
			" Note: col() is 1-based, all other indexes zero-based!
			let start = 0
			while start >= 0 && start < strlen(line) && start < col('.')
				let b = match(line, matchPattern, start)
				let e = matchend(line, matchPattern, start)
				if b < col('.') && col('.') <= e
					return [s:pattern[i], [line('.'), (b + 1)], i]
				endif
				if b == e
					break
				endif
				let start = e
			endwhile
		endif
		let i -= 1
	endwhile
	return ['', [], -1]
endfunction

" Search current mark.
function! mark#SearchCurrentMark( isBackward )
	let l:result = 0

	let [l:markText, l:markPosition, l:markIndex] = mark#CurrentMark()
	if empty(l:markText)
		if s:lastSearch == -1
			let l:result = mark#SearchAnyMark(a:isBackward)
			let s:lastSearch = mark#CurrentMark()[2]
		else
			let l:result = s:Search(s:pattern[s:lastSearch], v:count1, a:isBackward, [], s:RenderMark(s:lastSearch + 1))
		endif
	else
		let l:result = s:Search(l:markText, v:count1, a:isBackward, l:markPosition, s:RenderMark(l:markIndex + 1) . (l:markIndex ==# s:lastSearch ? '' : '!'))
		let s:lastSearch = l:markIndex
	endif

	return l:result
endfunction

function! mark#SearchGroupMark( groupNum, count, isBackward, isSetLastSearch )
	call ingo#err#Clear()
	if a:groupNum == 0
		" No mark group number specified; use last search, and fall back to
		" current mark if possible.
		if s:lastSearch == -1
			let [l:markText, l:markPosition, l:markIndex] = mark#CurrentMark()
			if empty(l:markText)
				return 0
			endif
		else
			let l:markIndex = s:lastSearch
			let l:markText = s:pattern[l:markIndex]
			let l:markPosition = []
		endif
	else
		let l:groupNum = a:groupNum
		if l:groupNum > s:markNum
			" This highlight group does not exist.
			let l:groupNum = mark#QueryMarkGroupNum()
			if l:groupNum < 1 || l:groupNum > s:markNum
				return 0
			endif
		endif

		let l:markIndex = l:groupNum - 1
		let l:markText = s:pattern[l:markIndex]
		let l:markPosition = []
	endif

	let l:result =  s:Search(l:markText, a:count, a:isBackward, l:markPosition, s:RenderMark(l:markIndex + 1) . (l:markIndex ==# s:lastSearch ? '' : '!'))
	if a:isSetLastSearch
		let s:lastSearch = l:markIndex
	endif
	return l:result
endfunction

function! mark#SearchNextGroup( count, isBackward )
	if s:lastSearch == -1
		" Fall back to current mark in case of no last search.
		let [l:markText, l:markPosition, l:markIndex] = mark#CurrentMark()
		if empty(l:markText)
			" Fall back to next group that would be taken.
			let l:groupIndex = s:GetNextGroupIndex()
		else
			let l:groupIndex = l:markIndex
		endif
	else
		let l:groupIndex = s:lastSearch
	endif

	let l:groupIndex = mark#NextUsedGroupIndex(a:isBackward, 1, l:groupIndex, a:count)
	if l:groupIndex == -1
		call ingo#err#Set(printf('No %s mark group%s used', (a:count == 1 ? '' : a:count . ' ') . (a:isBackward ? 'previous' : 'next'), (a:count == 1 ? '' : 's')))
		return 0
	endif
	return mark#SearchGroupMark(l:groupIndex + 1, 1, a:isBackward, 1)
endfunction


function! mark#NoMarkErrorMessage()
	call ingo#err#Set('No marks defined')
endfunction
function! s:ErrorMessage( searchType, searchPattern, isBackward )
	if &wrapscan
		let l:errmsg = a:searchType . ' not found: ' . a:searchPattern
	else
		let l:errmsg = printf('%s search hit %s without match for: %s', a:searchType, (a:isBackward ? 'TOP' : 'BOTTOM'), a:searchPattern)
	endif
	call ingo#err#Set(l:errmsg)
endfunction

" Wrapper around search() with additonal search and error messages and "wrapscan" warning.
function! s:Search( pattern, count, isBackward, currentMarkPosition, searchType )
	if empty(a:pattern)
		call mark#NoMarkErrorMessage()
		return 0
	endif

	let l:save_view = winsaveview()

	" searchpos() obeys the 'smartcase' setting; however, this setting doesn't
	" make sense for the mark search, because all patterns for the marks are
	" concatenated as branches in one large regexp, and because patterns that
	" result from the *-command-alike mappings should not obey 'smartcase' (like
	" the * command itself), anyway. If the :Mark command wants to support
	" 'smartcase', it'd have to emulate that into the regular expression.
	" Instead of temporarily unsetting 'smartcase', we force the correct
	" case-matching behavior through \c / \C.
	let l:searchPattern = (s:IsIgnoreCase(a:pattern) ? '\c' : '\C') . a:pattern

	let l:count = a:count
	let l:isWrapped = 0
	let l:isMatch = 0
	let l:line = 0
	while l:count > 0
		let [l:prevLine, l:prevCol] = [line('.'), col('.')]

		" Search for next match, 'wrapscan' applies.
		let [l:line, l:col] = searchpos( l:searchPattern, (a:isBackward ? 'b' : '') )

"****D echomsg '****' a:isBackward string([l:line, l:col]) string(a:currentMarkPosition) l:count
		if a:isBackward && l:line > 0 && [l:line, l:col] == a:currentMarkPosition && l:count == a:count
			" On a search in backward direction, the first match is the start of the
			" current mark (if the cursor was positioned on the current mark text, and
			" not at the start of the mark text).
			" In contrast to the normal search, this is not considered the first
			" match. The mark text is one entity; if the cursor is positioned anywhere
			" inside the mark text, the mark text is considered the current mark. The
			" built-in '*' and '#' commands behave in the same way; the entire <cword>
			" text is considered the current match, and jumps move outside that text.
			" In normal search, the cursor can be positioned anywhere (via offsets)
			" around the search, and only that single cursor position is considered
			" the current match.
			" Thus, the search is retried without a decrease of l:count, but only if
			" this was the first match; repeat visits during wrapping around count as
			" a regular match. The search also must not be retried when this is the
			" first match, but we've been here before (i.e. l:isMatch is set): This
			" means that there is only the current mark in the buffer, and we must
			" break out of the loop and indicate that search wrapped around and no
			" other mark was found.
			if l:isMatch
				let l:isWrapped = 1
				break
			endif

			" The l:isMatch flag is set so if the final mark cannot be reached, the
			" original cursor position is restored. This flag also allows us to detect
			" whether we've been here before, which is checked above.
			let l:isMatch = 1
		elseif l:line > 0
			let l:isMatch = 1
			let l:count -= 1

			" Note: No need to check 'wrapscan'; the wrapping can only occur if
			" 'wrapscan' is actually on.
			if ! a:isBackward && (l:prevLine > l:line || l:prevLine == l:line && l:prevCol >= l:col)
				let l:isWrapped = 1
			elseif a:isBackward && (l:prevLine < l:line || l:prevLine == l:line && l:prevCol <= l:col)
				let l:isWrapped = 1
			endif
		else
			break
		endif
	endwhile

	" We're not stuck when the search wrapped around and landed on the current
	" mark; that's why we exclude a possible wrap-around via a:count == 1.
	let l:isStuckAtCurrentMark = ([l:line, l:col] == a:currentMarkPosition && a:count == 1)
"****D echomsg '****' l:line l:isStuckAtCurrentMark l:isWrapped l:isMatch string([l:line, l:col]) string(a:currentMarkPosition)
	if l:line > 0 && ! l:isStuckAtCurrentMark
		let l:matchPosition = getpos('.')

		" Open fold at the search result, like the built-in commands.
		normal! zv

		" Add the original cursor position to the jump list, like the
		" [/?*#nN] commands.
		" Implementation: Memorize the match position, restore the view to the state
		" before the search, then jump straight back to the match position. This
		" also allows us to set a jump only if a match was found. (:call
		" setpos("''", ...) doesn't work in Vim 7.2)
		call winrestview(l:save_view)
		normal! m'
		call setpos('.', l:matchPosition)

		" Enable marks (in case they were disabled) after arriving at the mark (to
		" avoid unnecessary screen updates) but before the error message (to avoid
		" it getting lost due to the screen updates).
		call s:MarkEnable(1)

		if l:isWrapped
			call s:WrapMessage(s:EnrichSearchType(a:searchType), a:pattern, a:isBackward)
		else
			call s:EchoSearchPattern(s:EnrichSearchType(a:searchType), a:pattern, a:isBackward)
		endif
		return 1
	else
		if l:isMatch
			" The view has been changed by moving through matches until the end /
			" start of file, when 'nowrapscan' forced a stop of searching before the
			" l:count'th match was found.
			" Restore the view to the state before the search.
			call winrestview(l:save_view)
		endif

		" Enable marks (in case they were disabled) after arriving at the mark (to
		" avoid unnecessary screen updates) but before the error message (to avoid
		" it getting lost due to the screen updates).
		call s:MarkEnable(1)

		if l:line > 0 && l:isStuckAtCurrentMark && l:isWrapped
			call s:WrapMessage(s:EnrichSearchType(a:searchType), a:pattern, a:isBackward)
			return 1
		else
			call s:ErrorMessage(a:searchType, a:pattern, a:isBackward)
			return 0
		endif
	endif
endfunction

" Combine all marks into one regexp.
function! s:AnyMark()
	return join(filter(copy(s:pattern), '! empty(v:val)'), '\|')
endfunction

" Search any mark.
function! mark#SearchAnyMark( isBackward )
	let l:markPosition = mark#CurrentMark()[1]
	let l:markText = s:AnyMark()
	let s:lastSearch = -1
	return s:Search(l:markText, v:count1, a:isBackward, l:markPosition, 'mark*')
endfunction

" Search last searched mark.
function! mark#SearchNext( isBackward, ... )
	let l:markText = mark#CurrentMark()[0]
	if empty(l:markText)
		return 0    " Fall back to the built-in * / # command (done by the mapping).
	endif

	" Use the provided search type or choose depending on last use of
	" <Plug>MarkSearchCurrentNext / <Plug>MarkSearchAnyNext.
	call call(a:0 ? a:1 : (s:lastSearch == -1 ? 'mark#SearchAnyMark' : 'mark#SearchCurrentMark'), [a:isBackward])
	return 1
endfunction



" Load mark patterns from list.
function! mark#Load( marks, enabled )
	if s:markNum > 0 && len(a:marks) > 0
		" Initialize mark patterns (and optional names) with the passed list.
		" Ensure that, regardless of the list length, s:pattern / s:names
		" contain exactly s:markNum elements.
		for l:index in range(s:markNum)
			call s:DeserializeMark(get(a:marks, l:index, ''), l:index)
		endfor

		let s:enabled = a:enabled

		call mark#UpdateScope()

		" The list of patterns may be sparse, return only the actual patterns.
		return mark#GetCount()
	endif
	return 0
endfunction

" Access the list of mark patterns.
function! s:SerializeMark( index )
	return (empty(s:names[a:index]) ? s:pattern[a:index] : {'pattern': s:pattern[a:index], 'name': s:names[a:index]})
endfunction
function! s:Deserialize( mark )
	return (type(a:mark) == type({}) ? [get(a:mark, 'pattern', ''), get(a:mark, 'name', '')] : [a:mark, ''])
endfunction
function! s:DeserializeMark( mark, index )
	let [s:pattern[a:index], s:names[a:index]] = s:Deserialize(a:mark)
endfunction
function! mark#ToList()
	" Trim unused patterns from the end of the list, the amount of available marks
	" may differ on the next invocation (e.g. due to a different number of
	" highlight groups in Vim and GVIM). We want to keep empty patterns in the
	" front and middle to maintain the mapping to highlight groups, though.
	let l:highestNonEmptyIndex = s:markNum - 1
	while l:highestNonEmptyIndex >= 0 && empty(s:pattern[l:highestNonEmptyIndex]) && empty(s:names[l:highestNonEmptyIndex])
		let l:highestNonEmptyIndex -= 1
	endwhile

	return (l:highestNonEmptyIndex < 0 ? [] : map(range(0, l:highestNonEmptyIndex), 's:SerializeMark(v:val)'))
endfunction

" Common functions for :MarkLoad and :MarkSave
function! mark#MarksVariablesComplete( ArgLead, CmdLine, CursorPos )
	return sort(map(filter(keys(g:), 'v:val !~# "^MARK_\\%(MARKS\\|ENABLED\\)$" && v:val =~# "\\V\\^MARK_' . (empty(a:ArgLead) ? '\\S' : escape(a:ArgLead, '\')) . '"'), 'v:val[5:]'))
endfunction
function! s:HasVariablePersistence()
	return (index(split(&viminfo, ','), '!') != -1)
endfunction

" :MarkLoad command.
function! mark#LoadCommand( isShowMessages, ... )
	if a:0
		let l:marksVariable = printf('g:MARK_%s', a:1)
		if exists(l:marksVariable)
			let l:marks = eval(l:marksVariable)
			let l:isEnabled = 1
		else
			call ingo#err#Set('No marks stored under ' . l:marksVariable . (s:HasVariablePersistence() || a:1 !~# '^\u\+$' ? '' : ", and persistence not configured via ! flag in 'viminfo'"))
			return 0
		endif
	else
		if exists('g:MARK_MARKS')
			let l:marks = g:MARK_MARKS
			let l:isEnabled = (exists('g:MARK_ENABLED') ? g:MARK_ENABLED : 1)
		else
			call ingo#err#Set('No persistent marks found' . (s:HasVariablePersistence() ? '' : ", and persistence not configured via ! flag in 'viminfo'"))
			return 0
		endif
	endif

	try
		" Persistent global variables cannot be of type List, so we actually store
		" the string representation, and eval() it back to a List.
		execute printf('let l:loadedMarkNum = mark#Load(%s, %d)', l:marks, l:isEnabled)
		if a:isShowMessages
			if l:loadedMarkNum == 0
				echomsg 'No persistent marks defined' . (exists('l:marksVariable') ? ' in ' . l:marksVariable : '')
			else
				echomsg printf('Loaded %d mark%s', l:loadedMarkNum, (l:loadedMarkNum == 1 ? '' : 's')) . (s:enabled ? '' : '; marks currently disabled')
			endif
		endif
		return 1
	catch /^Vim\%((\a\+)\)\=:/
		if exists('l:marksVariable')
			call ingo#err#Set(printf('Corrupted persistent mark info in %s', l:marksVariable))
			execute 'unlet!' l:marksVariable
		else
			call ingo#err#Set('Corrupted persistent mark info in g:MARK_MARKS and g:MARK_ENABLED')
			unlet! g:MARK_MARKS
			unlet! g:MARK_ENABLED
		endif
		return 0
	endtry
endfunction

" :MarkSave command.
function! s:SavePattern( ... )
	let l:savedMarks = mark#ToList()

	if a:0
		try
			if empty(l:savedMarks)
				unlet! g:MARK_{a:1}
			else
				let g:MARK_{a:1} = string(l:savedMarks)
			endif
		catch /^Vim\%((\a\+)\)\=:/
			call ingo#err#SetVimException()
			return 0
		endtry
	else
		let g:MARK_MARKS = string(l:savedMarks)
		let g:MARK_ENABLED = s:enabled
	endif
	return (empty(l:savedMarks) ? 2 : 1)
endfunction
function! mark#SaveCommand( ... )
	if ! s:HasVariablePersistence()
		if ! a:0
			call ingo#err#Set("Cannot persist marks, need ! flag in 'viminfo': :set viminfo+=!")
			return 0
		elseif a:1 =~# '^\u\+$'
			call ingo#msg#WarningMsg("Cannot persist marks, need ! flag in 'viminfo': :set viminfo+=!")
		endif
	endif

	let l:result = call('s:SavePattern', a:000)
	if l:result == 2
		call ingo#msg#WarningMsg('No marks defined')
	endif
	return l:result
endfunction

" :MarkYankDefinitions and :MarkYankDefinitionsOneLiner commands.
function! mark#YankDefinitions( isOneLiner, register )
	let l:marks = mark#ToList()
	if empty(l:marks)
		return 0
	endif

	let l:commands = []
	for l:i in range(len(l:marks))
		if ! empty(l:marks[l:i])
			let [l:pattern, l:name] = s:Deserialize(l:marks[l:i])
			call add(l:commands, printf('%dMark! /%s/%s', l:i + 1, escape(l:pattern, '/'), (empty(l:name) ? '' : ' as ' . l:name)))
		endif
	endfor

	let l:command = (a:isOneLiner ? join(map(l:commands, '"exe " . string(v:val)'), ' | ') : join(l:commands, "\n"))
	call setreg(a:register, l:command)

	return 1
endfunction

" :MarkName command.
function! s:HasNamedMarks()
	return (! empty(filter(copy(s:names), '! empty(v:val)')))
endfunction
function! mark#SetName( isClearAll, groupNum, name )
	if a:isClearAll
		if a:groupNum != 0
			call ingo#err#Set('Use either [!] to clear all names, or [N] to name a single group, but not both.')
			return 0
		endif
		let s:names = repeat([''], s:markNum)
	elseif a:groupNum > s:markNum
		call ingo#err#Set(printf('Only %d mark highlight groups', s:markNum))
		return 0
	else
		let s:names[a:groupNum - 1] = a:name
	endif
	return 1
endfunction


" Query mark group number.
function! s:GetNextGroupIndex()
	let l:nextGroupIndex = s:FreeGroupIndex()
	if l:nextGroupIndex == -1
		let l:nextGroupIndex = s:cycle
	endif
	return l:nextGroupIndex
endfunction
function! s:GetMarker( index, nextGroupIndex )
	let l:marker = ''
	if s:lastSearch == a:index
		let l:marker .= '/'
	endif
	if a:index == a:nextGroupIndex
		let l:marker .= '>'
	endif
	return l:marker
endfunction
function! s:GetAlternativeCount( pattern )
	return len(s:SplitIntoAlternatives(a:pattern))
endfunction
function! s:PrintMarkGroup( nextGroupIndex )
	for i in range(s:markNum)
		echon ' '
		execute 'echohl MarkWord' . (i + 1)
		let c = s:GetAlternativeCount(s:pattern[i])
		echon printf('%1s%s%2d ', s:GetMarker(i, a:nextGroupIndex), (c ? (c > 1 ? c : '') . '*' : ''), (i + 1))
		echohl None
	endfor
endfunction
function! mark#QueryMarkGroupNum()
	echohl Question
	echo 'Mark?'
	echohl None
	let l:nextGroupIndex = s:GetNextGroupIndex()
	call s:PrintMarkGroup(l:nextGroupIndex)

	let l:nr = 0
	while 1
		let l:char = nr2char(getchar())

		if l:char ==# "\<CR>"
			return (l:nr == 0 ? l:nextGroupIndex + 1 : l:nr)
		elseif l:char !~# '\d'
			return -1
		endif
		echon l:char

		let l:nr = 10 * l:nr + l:char
		if s:markNum < 10 * l:nr
			return l:nr
		endif
	endwhile
endfunction

" :Marks command.
function! mark#List()
	let l:hasNamedMarks = s:HasNamedMarks()
	echohl Title
	if l:hasNamedMarks
		echo "group:name\tpattern"
	else
		echo 'group     pattern'
	endif
	echohl None
	echon '   (N) # of alternatives   > next mark group    / current search mark'
	let l:nextGroupIndex = s:GetNextGroupIndex()
	for i in range(s:markNum)
		execute 'echohl MarkWord' . (i + 1)
		let l:alternativeCount = s:GetAlternativeCount(s:pattern[i])
		let l:alternativeCountString = (l:alternativeCount > 1 ? ' (' . l:alternativeCount . ')' : '')
		let [l:name, l:format] = (empty(s:names[i]) ? ['', '%-4s'] : [':' . s:names[i], '%-10s'])
		echo printf('%1s%3d' . l:format . ' ', s:GetMarker(i, l:nextGroupIndex), (i + 1), l:name . l:alternativeCountString)
		echohl None
		echon (l:hasNamedMarks ? "\t" : ' ') . s:pattern[i]
	endfor

	if ! s:enabled
		echo 'Marks are currently disabled.'
	endif
endfunction


" :Mark command completion.
function! mark#Complete( ArgLead, CmdLine, CursorPos )
	let l:cmdlineBeforeCursor = strpart(a:CmdLine, 0, a:CursorPos)
	let l:matches = matchlist(l:cmdlineBeforeCursor, '\C\(\d*\)\s*Mark!\?\s\+\V' . escape(a:ArgLead, '\'))
	if empty(l:matches)
		return []
	endif

	" Complete from the command's mark group, or all groups when none is
	" specified.
	let l:groupNum = 0 + l:matches[1]
	let l:patterns =(l:groupNum == 0 || empty(get(s:pattern, l:groupNum - 1, '')) ? s:GetUsedPatterns() : [s:pattern[l:groupNum - 1]])

	" Complete both the entire pattern as well as its individual alternatives.
	let l:expandedPatterns = []
	for l:pattern in l:patterns
		if index(l:expandedPatterns, l:pattern) == -1
			call add(l:expandedPatterns, l:pattern)
		endif
		let l:alternatives = s:SplitIntoAlternatives(l:pattern)
		if len(l:alternatives) > 1
			for l:alternative in l:alternatives
				if index(l:expandedPatterns, l:alternative) == -1
					call add(l:expandedPatterns, l:alternative)
				endif
			endfor
		endif
	endfor

	call map(l:expandedPatterns, '"/" . escape(v:val, "/") . "/"')

	" Filter according to the argument lead. Allow to omit the frequent initial
	" \< atom in the lead.
	return filter(l:expandedPatterns, "v:val =~ '^\\%(\\\\<\\)\\?\\V' . " . string(escape(a:ArgLead, '\')))
endfunction


"- integrations ----------------------------------------------------------------

" Access the number of possible marks.
function! mark#GetGroupNum()
	return s:markNum
endfunction

" Access the number of defined marks.
function! s:GetUsedPatterns()
	return filter(copy(s:pattern), '! empty(v:val)')
endfunction
function! mark#GetCount()
	return len(s:GetUsedPatterns())
endfunction

" Access the current / passed index pattern.
function! mark#GetPattern( ... )
	if a:0
		return s:pattern[a:1]
	else
		return (s:lastSearch == -1 ? '' : s:pattern[s:lastSearch])
	endif
endfunction


"- initializations ------------------------------------------------------------

augroup Mark
	autocmd!
	autocmd WinEnter * if ! exists('w:mwMatch') | call mark#UpdateMark() | endif
	autocmd TabEnter * call mark#UpdateScope()
augroup END

" Define global variables and initialize current scope.
function! mark#Init()
	let s:markNum = 0
	while hlexists('MarkWord' . (s:markNum + 1))
		let s:markNum += 1
	endwhile
	let s:pattern = repeat([''], s:markNum)
	let s:names = repeat([''], s:markNum)
	let s:cycle = 0
	let s:lastSearch = -1
	let s:enabled = 1
endfunction
function! mark#ReInit( newMarkNum )
	if a:newMarkNum < s:markNum " There are less marks than before.
		" Clear the additional highlight groups.
		for i in range(a:newMarkNum + 1, s:markNum)
			execute 'highlight clear MarkWord' . (i + 1)
		endfor

		" Truncate the mark patterns.
		let s:pattern = s:pattern[0 : (a:newMarkNum - 1)]
		let s:names = s:names[0 : (a:newMarkNum - 1)]

		" Correct any indices.
		let s:cycle = min([s:cycle, (a:newMarkNum - 1)])
		let s:lastSearch = (s:lastSearch < a:newMarkNum ? s:lastSearch : -1)
	elseif a:newMarkNum > s:markNum " There are more marks than before.
		" Expand the mark patterns.
		let s:pattern += repeat([''], (a:newMarkNum - s:markNum))
		let s:names += repeat([''], (a:newMarkNum - s:markNum))
	endif

	let s:markNum = a:newMarkNum
endfunction

call mark#Init()
if exists('g:mwDoDeferredLoad') && g:mwDoDeferredLoad
	unlet g:mwDoDeferredLoad
	call mark#LoadCommand(0)
else
	call mark#UpdateScope()
endif

" vim: ts=4 sts=0 sw=4 noet
