1
0
mirror of https://github.com/gryf/pentadactyl-pm.git synced 2025-12-22 09:27:59 +01:00
Files
pentadactyl-pm/common/content/marks.js
Kris Maglione 6a25312c7d Recfactoring:
* Standard module format. All modules are explicitly declared
   as modules, they're created via a constructor and
   instantiated automatically. They're dependency aware. They
   stringify properly.

 * Classes are declared the same way (rather like Structs
   already were). They also stringify properly. Plus, each
   instance has a rather nifty closure member that closes all
   of its methods around 'this', so you can pass them to map,
   forEach, setTimeout, etc. Modules are themselves classes,
   with a special metaclass, as it were.

 * Doug Crockford is dead, metaphorically speaking.
   Closure-based classes just don't fit into any of the common
   JavaScript frameworks, and they're inefficient and
   confusing. Now, all class and module members are accessed
   explicitly via 'this', which makes it very clear that
   they're class members and not (e.g.) local variables,
   without anything nasty like Hungarian notation.

 * Strictly one module per file. Classes that belong to a
   module live in the same file.

 * For the moment, there are quite a few utility functions
   sitting in base.c, because my class implementation used
   them, and I haven't had the time or inclination to sort them
   out. I plan to reconcile them with the current mess that is
   the util namespace.

 * Changed bracing style.
2009-11-08 20:54:31 -05:00

344 lines
13 KiB
JavaScript

// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
/**
* @scope modules
* @instance marks
*/
const Marks = Module("marks", {
requires: ["storage"],
init: function init() {
this._localMarks = storage.newMap("local-marks", { store: true, privateData: true });
this._urlMarks = storage.newMap("url-marks", { store: true, privateData: true });
this._pendingJumps = [];
var appContent = document.getElementById("appcontent");
if (appContent)
appContent.addEventListener("load", this.closure._onPageLoad, true);
},
/**
* @property {Array} Returns all marks, both local and URL, in a sorted
* array.
*/
get all() {
// local marks
let location = window.content.location.href;
let lmarks = [i for (i in this._localMarkIter()) if (i[1].location == location)];
lmarks.sort();
// URL marks
// FIXME: why does umarks.sort() cause a "Component is not available =
// NS_ERROR_NOT_AVAILABLE" exception when used here?
let umarks = [i for (i in this._urlMarks)];
umarks.sort(function (a, b) a[0].localeCompare(b[0]));
return lmarks.concat(umarks);
},
/**
* Add a named mark for the current buffer, at its current position.
* If mark matches [A-Z], it's considered a URL mark, and will jump to
* the same position at the same URL no matter what buffer it's
* selected from. If it matches [a-z'"], it's a local mark, and can
* only be recalled from a buffer with a matching URL.
*
* @param {string} mark The mark name.
* @param {boolean} silent Whether to output error messages.
*/
// TODO: add support for frameset pages
add: function (mark, silent) {
let win = window.content;
let doc = win.document;
if (!doc.body)
return;
if (doc.body instanceof HTMLFrameSetElement) {
if (!silent)
liberator.echoerr("Marks support for frameset pages not implemented yet");
return;
}
let x = win.scrollMaxX ? win.pageXOffset / win.scrollMaxX : 0;
let y = win.scrollMaxY ? win.pageYOffset / win.scrollMaxY : 0;
let position = { x: x, y: y };
if (Marks.isURLMark(mark)) {
this._urlMarks.set(mark, { location: win.location.href, position: position, tab: tabs.getTab() });
if (!silent)
liberator.log("Adding URL mark: " + Marks.markToString(mark, this._urlMarks.get(mark)), 5);
}
else if (Marks.isLocalMark(mark)) {
// remove any previous mark of the same name for this location
this._removeLocalMark(mark);
if (!this._localMarks.get(mark))
this._localMarks.set(mark, []);
let vals = { location: win.location.href, position: position };
this._localMarks.get(mark).push(vals);
if (!silent)
liberator.log("Adding local mark: " + Marks.markToString(mark, vals), 5);
}
},
/**
* Remove all marks matching <b>filter</b>. If <b>special</b> is
* given, removes all local marks.
*
* @param {string} filter A string containing one character for each
* mark to be removed.
* @param {boolean} special Whether to delete all local marks.
*/
// FIXME: Shouldn't special be replaced with a null filter?
remove: function (filter, special) {
if (special) {
// :delmarks! only deletes a-z marks
for (let [mark, ] in this._localMarks)
this._removeLocalMark(mark);
}
else {
for (let [mark, ] in this._urlMarks) {
if (filter.indexOf(mark) >= 0)
this._removeURLMark(mark);
}
for (let [mark, ] in this._localMarks) {
if (filter.indexOf(mark) >= 0)
this._removeLocalMark(mark);
}
}
},
/**
* Jumps to the named mark. See {@link #add}
*
* @param {string} mark The mark to jump to.
*/
jumpTo: function (mark) {
let ok = false;
if (Marks.isURLMark(mark)) {
let slice = this._urlMarks.get(mark);
if (slice && slice.tab && slice.tab.linkedBrowser) {
if (slice.tab.parentNode != getBrowser().tabContainer) {
this._pendingJumps.push(slice);
// NOTE: this obviously won't work on generated pages using
// non-unique URLs :(
liberator.open(slice.location, liberator.NEW_TAB);
return;
}
let index = tabs.index(slice.tab);
if (index != -1) {
tabs.select(index);
let win = slice.tab.linkedBrowser.contentWindow;
if (win.location.href != slice.location) {
this._pendingJumps.push(slice);
win.location.href = slice.location;
return;
}
liberator.log("Jumping to URL mark: " + Marks.markToString(mark, slice), 5);
buffer.scrollToPercent(slice.position.x * 100, slice.position.y * 100);
ok = true;
}
}
}
else if (Marks.isLocalMark(mark)) {
let win = window.content;
let slice = this._localMarks.get(mark) || [];
for (let [, lmark] in Iterator(slice)) {
if (win.location.href == lmark.location) {
liberator.log("Jumping to local mark: " + Marks.markToString(mark, lmark), 5);
buffer.scrollToPercent(lmark.position.x * 100, lmark.position.y * 100);
ok = true;
break;
}
}
}
if (!ok)
liberator.echoerr("E20: Mark not set");
},
/**
* List all marks matching <b>filter</b>.
*
* @param {string} filter
*/
list: function (filter) {
let marks = this.all;
liberator.assert(marks.length > 0, "No marks set");
if (filter.length > 0) {
marks = marks.filter(function (mark) filter.indexOf(mark[0]) >= 0);
liberator.assert(marks.length > 0, "E283: No marks matching " + filter.quote());
}
let list = template.tabular(
["Mark", "Line", "Column", "File"],
["", "text-align: right", "text-align: right", "color: green"],
([mark[0],
Math.round(mark[1].position.x * 100) + "%",
Math.round(mark[1].position.y * 100) + "%",
mark[1].location]
for ([, mark] in Iterator(marks))));
commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
},
_onPageLoad: function _onPageLoad(event) {
let win = event.originalTarget.defaultView;
for (let [i, mark] in Iterator(this._pendingJumps)) {
if (win && win.location.href == mark.location) {
buffer.scrollToPercent(mark.position.x * 100, mark.position.y * 100);
this._pendingJumps.splice(i, 1);
return;
}
}
},
_removeLocalMark: function _removeLocalMark(mark) {
let localmark = this._localMarks.get(mark);
if (localmark) {
let win = window.content;
for (let [i, ] in Iterator(localmark)) {
if (localmark[i].location == win.location.href) {
liberator.log("Deleting local mark: " + Marks.markToString(mark, localmark[i]), 5);
localmark.splice(i, 1);
if (localmark.length == 0)
this._localMarks.remove(mark);
break;
}
}
}
},
_removeURLMark: function _removeURLMark(mark) {
let urlmark = this._urlMarks.get(mark);
if (urlmark) {
liberator.log("Deleting URL mark: " + Marks.markToString(mark, urlmark), 5);
this._urlMarks.remove(mark);
}
},
_localMarkIter: function _localMarkIter() {
for (let [mark, value] in this._localMarks)
for (let [, val] in Iterator(value))
yield [mark, val];
},
}, {
markToString: function markToString(name, mark) {
return name + ", " + mark.location +
", (" + Math.round(mark.position.x * 100) +
"%, " + Math.round(mark.position.y * 100) + "%)" +
(("tab" in mark) ? ", tab: " + tabs.index(mark.tab) : "");
},
isLocalMark: function isLocalMark(mark) /^['`a-z]$/.test(mark),
isURLMark: function isURLMark(mark) /^[A-Z0-9]$/.test(mark),
}, {
mappings: function () {
var myModes = config.browserModes;
mappings.add(myModes,
["m"], "Set mark at the cursor position",
function (arg) {
if (/[^a-zA-Z]/.test(arg))
return void liberator.beep();
marks.add(arg);
},
{ arg: true });
mappings.add(myModes,
["'", "`"], "Jump to the mark in the current buffer",
function (arg) { marks.jumpTo(arg); },
{ arg: true });
},
commands: function () {
commands.add(["delm[arks]"],
"Delete the specified marks",
function (args) {
let special = args.bang;
args = args.string;
// assert(special ^ args)
liberator.assert( special || args, "E471: Argument required");
liberator.assert(!special || !args, "E474: Invalid argument");
let matches;
if (matches = args.match(/(?:(?:^|[^a-zA-Z0-9])-|-(?:$|[^a-zA-Z0-9])|[^a-zA-Z0-9 -]).*/)) {
// NOTE: this currently differs from Vim's behavior which
// deletes any valid marks in the arg list, up to the first
// invalid arg, as well as giving the error message.
liberator.echoerr("E475: Invalid argument: " + matches[0]);
return;
}
// check for illegal ranges - only allow a-z A-Z 0-9
if (matches = args.match(/[a-zA-Z0-9]-[a-zA-Z0-9]/g)) {
for (let i = 0; i < matches.length; i++) {
let start = matches[i][0];
let end = matches[i][2];
if (/[a-z]/.test(start) != /[a-z]/.test(end) ||
/[A-Z]/.test(start) != /[A-Z]/.test(end) ||
/[0-9]/.test(start) != /[0-9]/.test(end) ||
start > end)
{
liberator.echoerr("E475: Invalid argument: " + args.match(matches[i] + ".*")[0]);
return;
}
}
}
marks.remove(args, special);
},
{
bang: true,
completer: function (context) completion.mark(context)
});
commands.add(["ma[rk]"],
"Mark current location within the web page",
function (args) {
let mark = args[0];
liberator.assert(mark.length <= 1, "E488: Trailing characters");
liberator.assert(/[a-zA-Z]/.test(mark),
"E191: Argument must be a letter or forward/backward quote");
marks.add(mark);
},
{ argCount: "1" });
commands.add(["marks"],
"Show all location marks of current web page",
function (args) {
args = args.string;
// ignore invalid mark characters unless there are no valid mark chars
liberator.assert(!args || /[a-zA-Z]/.test(args),
"E283: No marks matching " + args.quote());
let filter = args.replace(/[^a-zA-Z]/g, "");
marks.list(filter);
});
},
completion: function () {
completion.mark = function mark(context) {
function percent(i) Math.round(i * 100);
// FIXME: Line/Column doesn't make sense with %
context.title = ["Mark", "Line Column File"];
context.keys.description = function ([, m]) percent(m.position.y) + "% " + percent(m.position.x) + "% " + m.location;
context.completions = marks.all;
};
},
});
// vim: set fdm=marker sw=4 ts=4 et: