mirror of
https://github.com/gryf/pentadactyl-pm.git
synced 2025-12-22 09:27:59 +01:00
* 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.
344 lines
13 KiB
JavaScript
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:
|