1
0
mirror of https://github.com/gryf/pentadactyl-pm.git synced 2025-12-22 08:07:59 +01:00

merge native / and ? search

This commit is contained in:
Doug Kearns
2007-09-03 05:12:50 +00:00
parent abd42d741c
commit b009a6160b
7 changed files with 129 additions and 419 deletions

3
NEWS
View File

@@ -1,7 +1,10 @@
<pre> <pre>
2007-xx-xx: 2007-xx-xx:
* version 0.5.1 * version 0.5.1
* native / and ? search and n and N working again
* the URL in the status line can be selected with the mouse again * the URL in the status line can be selected with the mouse again
* the Windows default RC file is now ~/_vimperatorrc and the plugin
directory is ~/vimperator/plugin
* commandline history now works properly on Windows * commandline history now works properly on Windows
* filename completion now works on Windows * filename completion now works on Windows
* the Bookmarks Toolbar Folder is now read when bookmarks are first * the Bookmarks Toolbar Folder is now read when bookmarks are first

View File

@@ -682,6 +682,16 @@ function Commands() //{{{
help: "If <code class=\"argument\">[arg]</code> is specified then limit the list to those marks mentioned." help: "If <code class=\"argument\">[arg]</code> is specified then limit the list to those marks mentioned."
} }
)); ));
addDefaultCommand(new Command(["noh[lsearch]"],
function(args)
{
vimperator.search.clear();
},
{
short_help: "Clear the current selection",
help: "TODO"
}
));
addDefaultCommand(new Command(["norm[al]"], addDefaultCommand(new Command(["norm[al]"],
function(args) function(args)
{ {

View File

@@ -11,9 +11,7 @@ WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the for the specific language governing rights and limitations under the
License. License.
The Initial Developer of the Original Code is Shawn Betts. (c) 2006-2007: Martin Stubenschrott <stubenschrott@gmx.net>
Portions created by the Initial Developer are Copyright (C) 2004,2005
by the Initial Developer. All Rights Reserved.
Alternatively, the contents of this file may be used under the terms of Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or either the GNU General Public License Version 2 or later (the "GPL"), or
@@ -28,443 +26,135 @@ the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL. the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/ }}} ***** END LICENSE BLOCK *****/
// Finder for vimperator
// Author: Nigel McNie <http://nigel.mcnie.name/>
// Original Author: Shawn Betts
//
// The algorithm was taken from conkeror <http://conkeror.mozdev.net/>, but
// extensively refactored and changed to behave like vim (naturally!)
// The window to search in (which frame)
//var gWin = null;
//var gSelCtrl = null;
//function highlight(range, node) {
// var startContainer = range.startContainer;
// var startOffset = range.startOffset;
// var endOffset = range.endOffset;
// var docfrag = range.extractContents();
// var before = startContainer.splitText(startOffset);
// var parent = before.parentNode;
// node.appendChild(docfrag);
// parent.insertBefore(node, before);
// return node;
//}
// Clears the current selection
// @todo this should be in vimperator.js, and not depend on searcher.gSelCtrl
function clearSelection() {
//var selctrl = gSelCtrl;
var selctrl = vimperator.search.gSelCtrl;
var sel = selctrl.getSelection(Components.interfaces.nsISelectionController.SELECTION_NORMAL);
sel.removeAllRanges();
gFindBar.highlightDoc();
}
// Sets what is currently selected
// @todo as for clearSelection
function setSelection(range) {
try {
var selctrlcomp = Components.interfaces.nsISelectionController;
//var selctrl = gSelCtrl;
var selctrl = vimperator.search.gSelCtrl;
var sel = selctrl.getSelection(selctrlcomp.SELECTION_NORMAL);
sel.removeAllRanges();
sel.addRange(range.cloneRange());
selctrl.scrollSelectionIntoView(selctrlcomp.SELECTION_NORMAL,
selctrlcomp.SELECTION_FOCUS_REGION,
true);
}
catch (e) {
alert("setSelection: " + e);
}
}
// Highlight find matches and move selection to the first occurrence
// starting from pt.
// @todo move into searcher and clean up
function highlightFind(str, color, wrapped, dir, pt)
{
try {
var gWin = vimperator.search.gWin;//document.commandDispatcher.focusedWindow;
if (!gWin) {
alert('gWin does not exist here...');
alert(vimperator.search.gWin);
}
var doc = gWin.document;
var finder = Components.classes["@mozilla.org/embedcomp/rangefind;1"].createInstance()
.QueryInterface(Components.interfaces.nsIFind);
var searchRange;
var startPt;
var endPt;
var body = doc.body;
finder.findBackwards = !dir;
searchRange = doc.createRange();
startPt = doc.createRange();
endPt = doc.createRange();
var count = body.childNodes.length;
// Search range in the doc
searchRange.setStart(body,0);
searchRange.setEnd(body, count);
if (!dir) {
if (pt == null) {
startPt.setStart(body, count);
startPt.setEnd(body, count);
} else {
startPt.setStart(pt.startContainer, pt.startOffset);
startPt.setEnd(pt.startContainer, pt.startOffset);
}
endPt.setStart(body, 0);
endPt.setEnd(body, 0);
} else {
if (pt == null) {
startPt.setStart(body, 0);
startPt.setEnd(body, 0);
} else {
startPt.setStart(pt.endContainer, pt.endOffset);
startPt.setEnd(pt.endContainer, pt.endOffset);
}
endPt.setStart(body, count);
endPt.setEnd(body, count);
}
// search the doc
var retRange = null;
var selectionRange = null;
if (!wrapped) {
do {
retRange = finder.Find(str, searchRange, startPt, endPt);
var keepSearching = false;
if (retRange) {
var sc = retRange.startContainer;
var ec = retRange.endContainer;
var scp = sc.parentNode;
var ecp = ec.parentNode;
var sy1 = abs_point(scp).y;
var ey2 = abs_point(ecp).y + ecp.offsetHeight;
startPt = retRange.startContainer.ownerDocument.createRange();
if (!dir) {
startPt.setStart(retRange.startContainer, retRange.startOffset);
startPt.setEnd(retRange.startContainer, retRange.startOffset);
} else {
startPt.setStart(retRange.endContainer, retRange.endOffset);
startPt.setEnd(retRange.endContainer, retRange.endOffset);
}
// We want to find a match that is completely
// visible, otherwise the view will scroll just a
// bit to fit the selection in completely.
// alert ("sy1: " + sy1 + " scry: " + gWin.scrollY);
// alert ("ey2: " + ey2 + " bot: " + (gWin.scrollY + gWin.innerHeight));
keepSearching = (dir && sy1 < gWin.scrollY)
|| (!dir && ey2 >= gWin.scrollY + gWin.innerHeight);
}
} while (retRange && keepSearching);
} else {
retRange = finder.Find(str, searchRange, startPt, endPt);
}
if (retRange) {
setSelection(retRange);
selectionRange = retRange.cloneRange();
// highlightAllBut(str, retRange, color);
} else {
}
return selectionRange;
} catch(e) { alert('highlightFind:'+e); }
}
function clearHighlight()
{
gFindBar.highlightDoc();
var win = window.content;
var doc = win.document;
if (!document)
return;
var elem = null;
while ((elem = doc.getElementById("__vimperator-findbar-search-id"))) {
var child = null;
var docfrag = doc.createDocumentFragment();
var next = elem.nextSibling;
var parent = elem.parentNode;
while ((child = elem.firstChild)) {
docfrag.appendChild(child);
}
parent.removeChild(elem);
parent.insertBefore(docfrag, next);
}
}
/*
* Finds the absolute X and Y co-ordinates of a given node from the top left of
* the document
*
* Taken from conkeror utils.js
*/
function abs_point (node) {
var orig = node;
var pt = {};
try {
pt.x = node.offsetLeft;
pt.y = node.offsetTop;
// Find imagemap's coordinates
if (node.tagName == "AREA") {
var coords = node.getAttribute("coords").split(",");
pt.x += Number(coords[0]);
pt.y += Number(coords[1]);
}
node = node.offsetParent;
while (node.tagName != "BODY") {
pt.x += node.offsetLeft;
pt.y += node.offsetTop;
node = node.offsetParent;
}
}
catch (e) {
// Ignore
}
return pt;
}
// Vimperator searcher
// make sure you only create this object when the "vimperator" object is ready // make sure you only create this object when the "vimperator" object is ready
//vimperator.search = new function() // vimperator.search = new function()
function Search() //{{{ function Search() //{{{
{ {
var self = this; // needed for callbacks since "this" is the "vimperator" object in a callback var self = this; // needed for callbacks since "this" is the "vimperator" object in a callback
this.gWin = null; var found = false; // true if the last search was successful
this.gSelCtrl = null; var backwards = false;
this.gFindState = []; var lastsearch = ""; // keep track of the last searched string
var lastsearch_backwards = false; // like "backwards", but for the last search, so if you cancel a search with <esc> this is not set
// Event handlers for search - closure is needed // Event handlers for search - closure is needed
vimperator.registerCallback("change", vimperator.modes.SEARCH_FORWARD, function(command){ self.searchKeyPressed(command); }); vimperator.registerCallback("change", vimperator.modes.SEARCH_FORWARD, function(command){ self.searchKeyPressed(command); });
vimperator.registerCallback("submit", vimperator.modes.SEARCH_FORWARD, function(command){ self.searchSubmitted(command); }); vimperator.registerCallback("submit", vimperator.modes.SEARCH_FORWARD, function(command){ self.searchSubmitted(command); });
vimperator.registerCallback("cancel", vimperator.modes.SEARCH_FORWARD, function(){ self.searchCancelled(); }); vimperator.registerCallback("cancel", vimperator.modes.SEARCH_FORWARD, function(){ self.searchCanceled(); });
// TODO: allow advanced modes in register/triggerCallback
vimperator.registerCallback("change", vimperator.modes.SEARCH_BACKWARD, function(command){ self.searchKeyPressed(command); });
vimperator.registerCallback("submit", vimperator.modes.SEARCH_BACKWARD, function(command){ self.searchSubmitted(command); });
vimperator.registerCallback("cancel", vimperator.modes.SEARCH_BACKWARD, function(){ self.searchCanceled(); });
// Called when the search dialog is asked for
// Called when the search dialog is asked for. Sets up everything necessary // If you omit "mode", it will default to forward searching
// for this round of searching this.openSearchDialog = function(mode)
this.openSearchDialog = function()
{ {
// Get a reference to the focused window if necessary if (mode == vimperator.modes.SEARCH_BACKWARD)
if (this.gWin == null) this.gWin = document.commandDispatcher.focusedWindow; {
vimperator.commandline.open('?', '', vimperator.modes.SEARCH_BACKWARD);
// Change the currently selected text to not be the attention colour backwards = true;
// @todo: check what this REALLY does
try {
this.gSelCtrl = this.getFocusedSelCtrl();
this.gSelCtrl.setDisplaySelection(Components.interfaces.nsISelectionController.SELECTION_ATTENTION);
this.gSelCtrl.repaintSelection(Components.interfaces.nsISelectionController.SELECTION_NORMAL);
} }
catch (e) { else
alert('Could not change the colour of the current selection:' + e); {
vimperator.commandline.open('/', '', vimperator.modes.SEARCH_FORWARD);
backwards = false;
} }
// Initialize the state list for this attempt at searching // TODO: focus the top of the currently visible screen
var state = this.createInitialFindState();
this.gFindState = [];
this.gFindState.push(state);
this.resumeFindState(state);
vimperator.commandline.open('/', '', vimperator.modes.SEARCH_FORWARD);
} }
// Called when the current search needs to be repeated in the forward // Finds text in a page
// direction // TODO: backwards seems impossible i fear :(
// @todo will need re-jigging when reverse search comes in this.find = function(str, backwards)
this.findNext = function() { {
this.find(this.lastFindState()["search-str"], true, this.lastFindState()["range"]); const FIND_NORMAL = 0;
this.resumeFindState(this.lastFindState()); const FIND_TYPEAHEAD = 1;
// if there is still a search result const FIND_LINKS = 2;
if (this.lastFindState()["range"]) {
if (this.lastFindState()["wrapped"]) { found = getBrowser().fastFind.find(str, false) != Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND;
vimperator.echoerr("search hit BOTTOM, continuing at TOP");
this.lastFindState()["wrapped"] = false; return found;
}
else {
// TODO: this could probably be done in a nicer way - perhaps
// echoErr could not clobber all of this information somehow?
vimperator.echo('/' + this.lastFindState()["search-str"]);
}
}
} }
// Called when the current search needs to be repeated in the backward // Called when the current search needs to be repeated
// direction this.findAgain = function(reverse)
this.findPrevious = function() { {
this.find(this.lastFindState()["search-str"], false, this.lastFindState()["range"]); // this hack is needed to make n/N work with the correct string, if
this.resumeFindState(this.lastFindState()); // we typed /foo<esc> after the original search
// if there is still a search result if (getBrowser().fastFind.searchString != lastsearch)
if (this.lastFindState()["range"]) { {
if (this.lastFindState()["wrapped"]) { this.clear();
vimperator.echoerr("search hit TOP, continuing at BOTTOM"); this.find(lastsearch, false);
this.lastFindState()["wrapped"] = false; gFindBar.highlightDoc("yellow", "black", lastsearch);
}
else {
vimperator.echo('/' + this.lastFindState()["search-str"]);
}
} }
var up = reverse ? !lastsearch_backwards : lastsearch_backwards;
var result;
if (up)
result = getBrowser().fastFind.findPrevious();
else
result = getBrowser().fastFind.findNext();
if (result == Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND)
vimperator.echoerr("E486: Pattern not found: " + lastsearch);
else if (result == Components.interfaces.nsITypeAheadFind.FIND_WRAPPED)
{
// hack needed, because wrapping causes a "scroll" event which clears
// our command line
setTimeout( function() {
if (up)
vimperator.echoerr("search hit TOP, continuing at BOTTOM");
else
vimperator.echoerr("search hit BOTTOM, continuing at TOP");
}, 10);
}
else // just clear the command line if something has been found
vimperator.echo("");
} }
// Called when the user types a key in the search dialog. Triggers a find attempt // Called when the user types a key in the search dialog. Triggers a find attempt
this.searchKeyPressed = function(command) { this.searchKeyPressed = function(command)
if (command != "") { {
var str = vimperator.commandline.getCommand(); // TODO: check for 'incsearch'
this.find(str, true, this.lastFindState()["point"]); var backward = vimperator.hasMode(vimperator.modes.SEARCH_BACKWARD);
this.resumeFindState(this.lastFindState()); this.find(command, backward);
}
else {
clearSelection();
}
} }
// Called when the enter key is pressed to trigger a search // Called when the enter key is pressed to trigger a search
this.searchSubmitted = function(command) { this.searchSubmitted = function(command)
//removeMode(MODE_SEARCH); {
this.clear();
gFindBar.highlightDoc("yellow", "black", command);
// need to find again to draw the highlight of the current search
// result over the "highlight all" search results
// very hacky, but seem to work
setTimeout(function() { self.findAgain(false); }, 10);
lastsearch_backwards = backwards;
lastsearch = command;
vimperator.setMode(vimperator.modes.NORMAL); vimperator.setMode(vimperator.modes.NORMAL);
if (this.lastFindState()["range"] == null) { vimperator.focusContent();
vimperator.echoerr("E492: Pattern not found: " + this.lastFindState()["search-str"]);
}
} }
// Called when the search is cancelled - for example if someone presses // Called when the search is cancelled - for example if someone presses
// escape while typing a search // escape while typing a search
this.searchCancelled = function() { this.searchCanceled = function()
{
//removeMode(MODE_SEARCH); //removeMode(MODE_SEARCH);
vimperator.setMode(vimperator.modes.NORMAL); vimperator.setMode(vimperator.modes.NORMAL);
clearSelection(); this.clear();
vimperator.focusContent(); vimperator.focusContent();
} }
this.clear = function()
//
// Helper methods
//
// Turn on the selection in all frames
// @todo to tell the truth, I have no idea what this does
this.getFocusedSelCtrl = function() {
var ds = getBrowser().docShell;
var dsEnum = ds.getDocShellEnumerator(Components.interfaces.nsIDocShellTreeItem.typeContent,
Components.interfaces.nsIDocShell.ENUMERATE_FORWARDS);
while (dsEnum.hasMoreElements()) {
ds = dsEnum.getNext().QueryInterface(Components.interfaces.nsIDocShell);
if (ds.hasFocus) {
var display = ds.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsISelectionDisplay);
if (!display) return null;
return display.QueryInterface(Components.interfaces.nsISelectionController);
}
}
// One last try
return getBrowser().docShell
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsISelectionDisplay)
.QueryInterface(Components.interfaces.nsISelectionController);
}
// Creates a default find state
this.createInitialFindState = function() {
var state = [];
state["screenx"] = this.gWin.scrollX;
state["screeny"] = this.gWin.scrollY;
state["search-str"] = "";
state["wrapped"] = false;
state["point"] = null;
state["range"] = document.createRange();
state["selection"] = null;
state["direction"] = true;
return state;
}
// Given a find state, moves the browser to the way it should be in the
// state - highlighting the correct thing and the screen scrolled to the
// correct location
this.resumeFindState = function(state) {
if (state["selection"]) {
setSelection(state["selection"]);
}
else {
clearSelection();
}
this.gWin.scrollTo(state["screenx"], state["screeny"]);
}
// Retrieves the current find state that we're in
// @todo rename to currentFindState?
this.lastFindState = function() {
return this.gFindState[this.gFindState.length - 1];
}
// Adds a find state to the stack of such states. This is done every time a find is successful
this.addFindState = function(screenX, screenY, searchStr, wrapped, point, range, selection, direction) {
var state = [];
state["screenx"] = screenX;
state["screeny"] = screenY;
state["search-str"] = searchStr;
state["wrapped"] = wrapped;
state["point"] = point;
state["range"] = range;
state["selection"] = selection;
state["direction"] = direction;
this.gFindState.push(state);
}
// Finds text in a page
this.find = function(str, dir, pt)
{ {
var norecurse = arguments[3]; gFindBar.highlightDoc();
var matchRange;
clearHighlight();
// Should we wrap this time?
var wrapped = this.lastFindState()["wrapped"];
var point = pt;
if (this.lastFindState()["wrapped"] == false
&& this.lastFindState()["range"] == null
&& this.lastFindState()["search-str"] == str
&& this.lastFindState()["direction"] == dir) {
wrapped = true;
point = null;
}
gFindBar.highlightDoc('yellow', 'black', str);
matchRange = highlightFind(str, "lightblue", wrapped, dir, point);
if (matchRange == null) {
// No more matches in this direction. So add the state and then find
// again to wrap around. But only find again once to prevent infinite
// recursion if an error occurs
this.addFindState(this.gWin.scrollX, this.gWin.scrollY, str, wrapped, point,
matchRange, this.lastFindState()["selection"], dir);
if (!norecurse)
this.find(str, dir, pt, true);
}
else {
this.addFindState(this.gWin.scrollX, this.gWin.scrollY, str, wrapped,
point, matchRange, matchRange, dir);
}
} }
} //}}} } //}}}
//// @TODO should be moved into commands.js
//vimperator.commands.add(new Command(["noh[lsearch]"],
// clearSelection,
// {
// short_help: "Clear the current selection"
// }
//));
// vim: set fdm=marker sw=4 ts=4 et: // vim: set fdm=marker sw=4 ts=4 et:

View File

@@ -938,27 +938,32 @@ function Mappings() //{{{
)); ));
// search management // search management
addDefaultMap(new Map(vimperator.modes.NORMAL, ["g/"], addDefaultMap(new Map(vimperator.modes.NORMAL, ["/"],
function() { vimperator.search.openSearchDialog(); }, function() { vimperator.search.openSearchDialog(vimperator.modes.SEARCH_FORWARD); },
{ {
short_help: "Search forward for a pattern", short_help: "Search forward for a pattern",
help: "Buggy on many sites, use / if you want a reliable search!" help: "TODO"
}
));
addDefaultMap(new Map(vimperator.modes.NORMAL, ["?"],
function() { vimperator.search.openSearchDialog(vimperator.modes.SEARCH_BACKWARD); },
{
short_help: "Search backwards for a pattern",
help: "TODO"
} }
)); ));
addDefaultMap(new Map(vimperator.modes.NORMAL, ["n"], addDefaultMap(new Map(vimperator.modes.NORMAL, ["n"],
function() { vimperator.search.findNext(); }, function() { vimperator.search.findAgain(false); },
{ {
short_help: "Find next", short_help: "Find next",
help: "Repeat the last \"g/\" 1 time (until count is supported). <br/>" + help: "Repeat the last search 1 time (until count is supported)."
"NOTE: As \"g/\" is a little broken right now, use &lt;F3&gt; to go to the next search item of the \"/\" search for now."
} }
)); ));
addDefaultMap(new Map(vimperator.modes.NORMAL, ["N"], addDefaultMap(new Map(vimperator.modes.NORMAL, ["N"],
function() { vimperator.search.findPrevious(); }, function() { vimperator.search.findAgain(true); },
{ {
short_help: "Find previous", short_help: "Find previous",
help: "Repeat the last \"g/\" 1 time (until count is supported) in the opposite direction.<br/>" + help: "Repeat the last search 1 time (until count is supported) in the opposite direction."
"NOTE: As \"g/\" is a little broken right now, use &lt;S-F3&gt; to go to the previous search item of the \"/\" search for now."
} }
)); ));

View File

@@ -415,7 +415,7 @@ function Options() //{{{
addOption(new Option(["hintstyle", "hs"], "string", addOption(new Option(["hintstyle", "hs"], "string",
{ {
short_help: "CSS specification of unfocused hints appearance", short_help: "CSS specification of unfocused hints appearance",
default_value: "z-index:5000; font-family:monospace; font-size:12px; color:black; background-color:yellow; " + default_value: "z-index:5000; font-family:monospace; font-size:12px; color:white; background-color:red; " +
"border-color:ButtonShadow; border-width:0px; border-style:solid; padding:0px 1px 0px 1px; position:absolute;" "border-color:ButtonShadow; border-width:0px; border-style:solid; padding:0px 1px 0px 1px; position:absolute;"
} }
)); ));

View File

@@ -528,8 +528,10 @@ function CommandLine() //{{{
// and blur the command line if there is no text left // and blur the command line if there is no text left
if (command.length == 0) if (command.length == 0)
{ {
this.clear(); vimperator.triggerCallback("cancel", cur_extended_mode);
vimperator.setMode(old_mode, old_extended_mode);
vimperator.focusContent(); vimperator.focusContent();
this.clear();
} }
} }
else // any other key else // any other key

View File

@@ -18,11 +18,11 @@ syn match vimperatorComment +".*$+ contains=vimperatorTodo,@Spell
syn keyword vimperatorCommand addo[ns] ba[ck] bd[elete] bw[ipeout] bun[load] tabc[lose] beep bma[dd] bmd[el] bookm[arks] bm syn keyword vimperatorCommand addo[ns] ba[ck] bd[elete] bw[ipeout] bun[load] tabc[lose] beep bma[dd] bmd[el] bookm[arks] bm
\ b[uffer] buffers files ls delm[arks] downl[oads] dl ec[ho] echoe[rr] exe[cute] exu[sage] fo[rward] fw ha[rdcopy] h[elp] \ b[uffer] buffers files ls delm[arks] downl[oads] dl ec[ho] echoe[rr] exe[cute] exu[sage] fo[rward] fw ha[rdcopy] h[elp]
\ hist[ory] hs javas[cript] js mapc[lear] ma[rk] map marks no[remap] o[pen] e[dit] pc[lose] pref[erences] prefs q[uit] \ hist[ory] hs javas[cript] js mapc[lear] ma[rk] map marks noh[lsearch] no[remap] o[pen] e[dit] pc[lose] pref[erences]
\ quita[ll] qa[ll] re[load] reloada[ll] res[tart] sav[eas] se[t] so[urce] st[op] tab tabl[ast] tabm[ove] tabn[ext] tn[ext] \ prefs q[uit] quita[ll] qa[ll] re[load] reloada[ll] res[tart] sav[eas] se[t] so[urce] st[op] tab tabl[ast] tabm[ove]
\ tabo[nly] tabopen t[open] tabnew tabe[dit] tabp[revious] tp[revious] tabN[ext] tN[ext] tabr[ewind] tabfir[st] u[ndo] \ tabn[ext] tn[ext] tabo[nly] tabopen t[open] tabnew tabe[dit] tabp[revious] tp[revious] tabN[ext] tN[ext] tabr[ewind]
\ qmarka[dd] qma[dd] qmarkd[el] qmd[el] qmarks qms unm[ap] ve[rsion] viu[sage] win[open] w[open] wine[dit] wqa[ll] wq \ tabfir[st] u[ndo] qmarka[dd] qma[dd] qmarkd[el] qmd[el] qmarks qms unm[ap] ve[rsion] viu[sage] win[open] w[open]
\ xa[ll] zo[om] noh[ilight] \ wine[dit] wqa[ll] wq xa[ll] zo[om]
\ contained \ contained
" FIXME " FIXME