1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2014 Kris Maglione <maglione.k@gmail.com>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
13 var Marks = Module("marks", {
14 init: function init() {
15 this._localMarks = storage.newMap("local-marks", { privateData: true, replacer: Storage.Replacer.skipXpcom, store: true });
16 this._urlMarks = storage.newMap("url-marks", { privateData: true, replacer: Storage.Replacer.skipXpcom, store: true });
19 if (isArray(Iterator(this._localMarks).next()[1]))
20 this._localMarks.clear();
24 this._pendingJumps = [];
28 * @property {Array} Returns all marks, both local and URL, in a sorted
31 get all() iter(this._localMarks.get(this.localURI) || {},
33 ).sort((a, b) => String.localeCompare(a[0], b[0])),
35 get localURI() buffer.focusedFrame.document.documentURI.replace(/#.*/, ""),
37 Mark: function Mark(params={}) {
38 let win = buffer.focusedFrame;
39 let doc = win.document;
41 params.location = doc.documentURI.replace(/#.*/, ""),
42 params.offset = buffer.scrollPosition;
43 params.path = DOM(buffer.findScrollable(0, false)).xpath;
44 params.timestamp = Date.now() * 1000;
45 params.equals = function (m) this.location == m.location
46 && this.offset.x == m.offset.x
47 && this.offset.y == m.offset.y
48 && this.path == m.path;
53 * Add a named mark for the current buffer, at its current position.
54 * If mark matches [A-Z], it's considered a URL mark, and will jump to
55 * the same position at the same URL no matter what buffer it's
56 * selected from. If it matches [a-z], it's a local mark, and can
57 * only be recalled from a buffer with a matching URL.
59 * @param {string} name The mark name.
60 * @param {boolean} silent Whether to output error messages.
62 add: function (name, silent) {
63 let mark = this.Mark();
65 if (Marks.isURLMark(name)) {
66 // FIXME: Disabled due to cross-compartment magic.
67 // mark.tab = util.weakReference(tabs.getTab());
68 this._urlMarks.set(name, mark);
69 var message = "mark.addURL";
71 else if (Marks.isLocalMark(name)) {
72 this._localMarks.get(mark.location, {})[name] = mark;
73 this._localMarks.changed();
74 message = "mark.addLocal";
78 dactyl.log(_(message, Marks.markToString(name, mark)), 5);
83 * Push the current buffer position onto the jump stack.
85 * @param {string} reason The reason for this scroll event. Multiple
86 * scroll events for the same reason are coalesced. @optional
88 push: function push(reason) {
89 let store = buffer.localStore;
90 let jump = store.jumps[store.jumpsIndex];
92 if (reason && jump && jump.reason == reason)
95 let mark = this.add("'");
96 if (jump && mark.equals(jump.mark))
100 store.jumps[++store.jumpsIndex] = { mark: mark, reason: reason };
101 store.jumps.length = store.jumpsIndex + 1;
103 if (store.jumps.length > this.maxJumps) {
104 store.jumps = store.jumps.slice(-this.maxJumps);
105 store.jumpsIndex = store.jumps.length - 1;
113 * Jump to the given offset in the jump stack.
115 * @param {number} offset The offset from the current position in
116 * the jump stack to jump to.
117 * @returns {number} The actual change in offset.
119 jump: function jump(offset) {
120 let store = buffer.localStore;
121 if (offset < 0 && store.jumpsIndex == store.jumps.length - 1)
124 return this.withSavedValues(["jumping"], function _jump() {
126 let idx = Math.constrain(store.jumpsIndex + offset, 0, store.jumps.length - 1);
127 let orig = store.jumpsIndex;
129 if (idx in store.jumps && !dactyl.trapErrors("_scrollTo", this, store.jumps[idx].mark))
130 store.jumpsIndex = idx;
131 return store.jumpsIndex - orig;
136 let store = buffer.localStore;
138 index: store.jumpsIndex,
139 locations: store.jumps.map(j => j.mark)
144 * Remove all marks matching *filter*. If *special* is given, removes all
147 * @param {string} filter The list of marks to delete, e.g. "aA b C-I"
148 * @param {boolean} special Whether to delete all local marks.
150 remove: function (filter, special) {
152 this._localMarks.remove(this.localURI);
154 let pattern = util.charListToRegexp(filter, "a-zA-Z");
155 let local = this._localMarks.get(this.localURI);
156 this.all.forEach(function ([k, ]) {
157 if (pattern.test(k)) {
158 local && delete local[k];
159 marks._urlMarks.remove(k);
163 Iterator(local).next();
164 this._localMarks.changed();
167 this._localMarks.remove(this.localURI);
173 * Jumps to the named mark. See {@link #add}
175 * @param {string} char The mark to jump to.
177 jumpTo: function (char) {
178 if (Marks.isURLMark(char)) {
179 let mark = this._urlMarks.get(char);
180 dactyl.assert(mark, _("mark.unset", char));
182 let tab = mark.tab && mark.tab.get();
183 if (!tab || !tab.linkedBrowser || tabs.allTabs.indexOf(tab) == -1)
184 for ([, tab] in iter(tabs.visibleTabs, tabs.allTabs)) {
185 if (tab.linkedBrowser.contentDocument.documentURI.replace(/#.*/, "") === mark.location)
192 let doc = tab.linkedBrowser.contentDocument;
193 if (doc.documentURI.replace(/#.*/, "") == mark.location) {
194 dactyl.log(_("mark.jumpingToURL", Marks.markToString(char, mark)), 5);
195 this._scrollTo(mark);
198 this._pendingJumps.push(mark);
200 let sh = tab.linkedBrowser.sessionHistory;
201 let items = array(util.range(0, sh.count));
203 let a = items.slice(0, sh.index).reverse();
204 let b = items.slice(sh.index);
205 a.length = b.length = Math.max(a.length, b.length);
206 items = array(a).zip(b).flatten().compact();
208 for (let i in items.iterValues()) {
209 let entry = sh.getEntryAtIndex(i, false);
210 if (entry.URI.spec.replace(/#.*/, "") == mark.location)
211 return void tab.linkedBrowser.webNavigation.gotoIndex(i);
213 dactyl.open(mark.location);
217 this._pendingJumps.push(mark);
218 dactyl.open(mark.location, dactyl.NEW_TAB);
221 else if (Marks.isLocalMark(char)) {
222 let mark = (this._localMarks.get(this.localURI) || {})[char];
223 dactyl.assert(mark, _("mark.unset", char));
225 dactyl.log(_("mark.jumpingToLocal", Marks.markToString(char, mark)), 5);
226 this._scrollTo(mark);
229 dactyl.echoerr(_("mark.invalid"));
233 _scrollTo: function _scrollTo(mark) {
235 var node = buffer.findScrollable(0, (mark.offset || mark.position).x);
237 for (node in DOM.XPath(mark.path, buffer.focusedFrame.document))
241 if (node instanceof Element)
242 DOM(node).scrollIntoView();
245 Buffer.scrollToPosition(node, mark.offset.x, mark.offset.y);
246 else if (mark.position)
247 Buffer.scrollToPercent(node, mark.position.x * 100, mark.position.y * 100);
251 * List all marks matching *filter*.
253 * @param {string} filter List of marks to show, e.g. "ab A-I".
255 list: function (filter) {
256 let marks = this.all;
258 dactyl.assert(marks.length > 0, _("mark.none"));
260 if (filter.length > 0) {
261 let pattern = util.charListToRegexp(filter, "a-zA-Z");
262 marks = marks.filter(([k]) => (pattern.test(k)));
263 dactyl.assert(marks.length > 0, _("mark.noMatching", filter.quote()));
266 commandline.commandOutput(
268 ["Mark", "HPos", "VPos", "File"],
269 ["", "text-align: right", "text-align: right", "color: green"],
271 mark.offset ? Math.round(mark.offset.x)
272 : Math.round(mark.position.x * 100) + "%",
273 mark.offset ? Math.round(mark.offset.y)
274 : Math.round(mark.position.y * 100) + "%",
276 for ([, [name, mark]] in Iterator(marks)))));
279 _onPageLoad: function _onPageLoad(event) {
280 let win = event.originalTarget.defaultView;
281 for (let [i, mark] in Iterator(this._pendingJumps)) {
282 if (win && win.location.href == mark.location) {
283 this._scrollTo(mark);
284 this._pendingJumps.splice(i, 1);
290 markToString: function markToString(name, mark) {
291 let tab = mark.tab && mark.tab.get();
293 return [name, mark.location,
294 "(" + Math.round(mark.offset.x * 100),
295 Math.round(mark.offset.y * 100) + ")",
296 (tab && "tab: " + tabs.index(tab))
297 ].filter(util.identity).join(", ");
300 return [name, mark.location,
301 "(" + Math.round(mark.position.x * 100) + "%",
302 Math.round(mark.position.y * 100) + "%)",
303 (tab && "tab: " + tabs.index(tab))
304 ].filter(util.identity).join(", ");
307 isLocalMark: bind("test", /^[a-z`']$/),
309 isURLMark: bind("test", /^[A-Z]$/)
311 events: function () {
312 let appContent = document.getElementById("appcontent");
314 events.listen(appContent, "load", marks.bound._onPageLoad, true);
316 mappings: function () {
317 var myModes = config.browserModes;
319 mappings.add(myModes,
320 ["m"], "Set mark at the cursor position",
322 dactyl.assert(/^[a-zA-Z]$/.test(arg), _("mark.invalid"));
327 mappings.add(myModes,
328 ["'", "`"], "Jump to the mark in the current buffer",
329 function ({ arg }) { marks.jumpTo(arg); },
333 commands: function initCommands() {
334 commands.add(["delm[arks]"],
335 "Delete the specified marks",
337 let special = args.bang;
338 let arg = args[0] || "";
340 // assert(special ^ args)
341 dactyl.assert( special || arg, _("error.argumentRequired"));
342 dactyl.assert(!special || !arg, _("error.invalidArgument"));
344 marks.remove(arg, special);
348 completer: function (context) completion.mark(context),
352 commands.add(["ma[rk]"],
353 "Mark current location within the web page",
355 let mark = args[0] || "";
356 dactyl.assert(mark.length <= 1, _("error.trailingCharacters"));
357 dactyl.assert(/[a-zA-Z]/.test(mark), _("mark.invalid"));
363 commands.add(["marks"],
364 "Show the specified marks",
366 marks.list(args[0] || "");
368 completer: function (context) completion.mark(context),
373 completion: function initCompletion() {
374 completion.mark = function mark(context) {
375 function percent(i) Math.round(i * 100);
377 context.title = ["Mark", "HPos VPos File"];
378 context.keys.description = ([, m]) => (m.offset ? Math.round(m.offset.x) + " " + Math.round(m.offset.y)
379 : percent(m.position.x) + "% " + percent(m.position.y) + "%"
380 ) + " " + m.location;
381 context.completions = marks.all;
384 sanitizer: function initSanitizer() {
385 sanitizer.addItem("marks", {
386 description: "Local and URL marks",
388 contains: ["history"],
389 action: function (timespan, host) {
390 function matchhost(url) !host || util.isDomainURL(url, host);
391 function match(marks) (k for ([k, v] in Iterator(marks)) if (timespan.contains(v.timestamp) && matchhost(v.location)));
393 for (let [url, local] in marks._localMarks)
394 if (matchhost(url)) {
395 for (let key in match(local))
397 if (!Object.keys(local).length)
398 marks._localMarks.remove(url);
400 marks._localMarks.changed();
402 for (let key in match(marks._urlMarks))
403 marks._urlMarks.remove(key);
409 // vim: set fdm=marker sw=4 sts=4 ts=8 et: