// Copyright (c) 2006-2008 by Martin Stubenschrott // Copyright (c) 2007-2011 by Doug Kearns // Copyright (c) 2008-2011 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. "use strict"; /** * @scope modules * @instance marks */ var Marks = Module("marks", { init: function init() { this._localMarks = storage.newMap("local-marks", { privateData: true, replacer: Storage.Replacer.skipXpcom, store: true }); this._urlMarks = storage.newMap("url-marks", { privateData: true, replacer: Storage.Replacer.skipXpcom, store: true }); try { if (isArray(Iterator(this._localMarks).next()[1])) this._localMarks.clear(); } catch (e) {} this._pendingJumps = []; }, /** * @property {Array} Returns all marks, both local and URL, in a sorted * array. */ get all() iter(this._localMarks.get(this.localURI) || {}, this._urlMarks ).sort(function (a, b) String.localeCompare(a[0], b[0])), get localURI() buffer.focusedFrame.document.documentURI, Mark: function Mark(params) { let win = buffer.focusedFrame; let doc = win.document; params = params || {}; params.location = doc.documentURI, params.offset = buffer.scrollPosition; params.path = util.generateXPath(buffer.findScrollable(0, params.offset.x)); params.timestamp = Date.now() * 1000; params.equals = function (m) this.location == m.location && this.offset.x == m.offset.x && this.offset.y == m.offset.y && this.path == m.path; return params; }, /** * 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} name The mark name. * @param {boolean} silent Whether to output error messages. */ add: function (name, silent) { let mark = this.Mark(); if (Marks.isURLMark(name)) { mark.tab = Cu.getWeakReference(tabs.getTab()); this._urlMarks.set(name, mark); var message = "mark.addURL"; } else if (Marks.isLocalMark(name)) { this._localMarks.get(mark.location, {})[name] = mark; this._localMarks.changed(); message = "mark.addLocal"; } if (!silent) dactyl.log(_(message, Marks.markToString(name, mark)), 5); return mark; }, /** * Push the current buffer position onto the jump stack. * * @param {string} reason The reason for this scroll event. Multiple * scroll events for the same reason are coalesced. @optional */ push: function push(reason) { let store = buffer.localStore; let jump = store.jumps[store.jumpsIndex]; if (reason && jump && jump.reason == reason) return; let mark = this.add("'"); if (jump && mark.equals(jump.mark)) return; if (!this.jumping) { store.jumps[++store.jumpsIndex] = { mark: mark, reason: reason }; store.jumps.length = store.jumpsIndex + 1; if (store.jumps.length > this.maxJumps) { store.jumps = store.jumps.slice(-this.maxJumps); store.jumpsIndex = store.jumps.length - 1; } } }, maxJumps: 200, /** * Jump to the given offset in the jump stack. * * @param {number} offset The offset from the current position in * the jump stack to jump to. * @returns {number} The actual change in offset. */ jump: function jump(offset) { let store = buffer.localStore; if (offset < 0 && store.jumpsIndex == store.jumps.length - 1) this.push(); return this.withSavedValues(["jumping"], function _jump() { this.jumping = true; let idx = Math.constrain(store.jumpsIndex + offset, 0, store.jumps.length - 1); let orig = store.jumpsIndex; if (idx in store.jumps && !dactyl.trapErrors("_scrollTo", this, store.jumps[idx].mark)) store.jumpsIndex = idx; return store.jumpsIndex - orig; }); }, /** * Remove all marks matching *filter*. If *special* is given, removes all * local marks. * * @param {string} filter The list of marks to delete, e.g. "aA b C-I" * @param {boolean} special Whether to delete all local marks. */ remove: function (filter, special) { if (special) this._localMarks.remove(this.localURI); else { let pattern = util.charListToRegexp(filter, "a-zA-Z"); let local = this._localMarks.get(this.localURI); this.all.forEach(function ([k, ]) { if (pattern.test(k)) { local && delete local[k]; marks._urlMarks.remove(k); } }); try { Iterator(local).next(); this._localMarks.changed(); } catch (e) { this._localMarks.remove(this.localURI); } } }, /** * Jumps to the named mark. See {@link #add} * * @param {string} char The mark to jump to. */ jumpTo: function (char) { if (Marks.isURLMark(char)) { let mark = this._urlMarks.get(char); dactyl.assert(mark, _("mark.unset", char)); let tab = mark.tab && mark.tab.get(); if (!tab || !tab.linkedBrowser || tabs.allTabs.indexOf(tab) == -1) for ([, tab] in iter(tabs.visibleTabs, tabs.allTabs)) { if (tab.linkedBrowser.contentDocument.documentURI === mark.location) break; tab = null; } if (tab) { tabs.select(tab); let doc = tab.linkedBrowser.contentDocument; if (doc.documentURI == mark.location) { dactyl.log(_("mark.jumpingToURL", Marks.markToString(char, mark)), 5); this._scrollTo(mark); } else { this._pendingJumps.push(mark); let sh = tab.linkedBrowser.sessionHistory; let items = array(util.range(0, sh.count)); let a = items.slice(0, sh.index).reverse(); let b = items.slice(sh.index); a.length = b.length = Math.max(a.length, b.length); items = array(a).zip(b).flatten().compact(); for (let i in items.iterValues()) { let entry = sh.getEntryAtIndex(i, false); if (entry.URI.spec.replace(/#.*/, "") == mark.location) return void tab.linkedBrowser.webNavigation.gotoIndex(i); } dactyl.open(mark.location); } } else { this._pendingJumps.push(mark); dactyl.open(mark.location, dactyl.NEW_TAB); } } else if (Marks.isLocalMark(char)) { let mark = (this._localMarks.get(this.localURI) || {})[char]; dactyl.assert(mark, _("mark.unset", char)); dactyl.log(_("mark.jumpingToLocal", Marks.markToString(char, mark)), 5); this._scrollTo(mark); } else dactyl.echoerr(_("mark.invalid")); }, _scrollTo: function _scrollTo(mark) { if (!mark.xpath) var node = buffer.findScrollable(0, (mark.offset || mark.position).x) else for (node in util.evaluateXPath(mark.xpath, buffer.focusedFrame.document)) break; util.assert(node); util.scrollIntoView(node); if (mark.offset) Buffer.scrollToPosition(node, mark.offset.x, mark.offset.y); else if (mark.position) Buffer.scrollToPercent(node, mark.position.x * 100, mark.position.y * 100); }, /** * List all marks matching *filter*. * * @param {string} filter List of marks to show, e.g. "ab A-I". */ list: function (filter) { let marks = this.all; dactyl.assert(marks.length > 0, _("mark.none")); if (filter.length > 0) { let pattern = util.charListToRegexp(filter, "a-zA-Z"); marks = marks.filter(function ([k, ]) pattern.test(k)); dactyl.assert(marks.length > 0, _("mark.noMatching", filter.quote())); } commandline.commandOutput( template.tabular( ["Mark", "HPos", "VPos", "File"], ["", "text-align: right", "text-align: right", "color: green"], ([name, mark.offset ? Math.round(mark.offset.x) : Math.round(mark.position.x * 100) + "%", mark.offset ? Math.round(mark.offset.y) : Math.round(mark.position.y * 100) + "%", mark.location] for ([, [name, mark]] in Iterator(marks))))); }, _onPageLoad: function _onPageLoad(event) { let win = event.originalTarget.defaultView; for (let [i, mark] in Iterator(this._pendingJumps)) { if (win && win.location.href == mark.location) { this._scrollTo(mark); this._pendingJumps.splice(i, 1); return; } } }, }, { markToString: function markToString(name, mark) { let tab = mark.tab && mark.tab.get(); if (mark.offset) return [name, mark.location, "(" + Math.round(mark.offset.x * 100), Math.round(mark.offset.y * 100) + ")", (tab && "tab: " + tabs.index(tab)) ].filter(util.identity).join(", "); if (mark.position) return [name, mark.location, "(" + Math.round(mark.position.x * 100) + "%", Math.round(mark.position.y * 100) + "%)", (tab && "tab: " + tabs.index(tab)) ].filter(util.identity).join(", "); }, isLocalMark: function isLocalMark(mark) /^[a-z`']$/.test(mark), isURLMark: function isURLMark(mark) /^[A-Z]$/.test(mark) }, { events: function () { let appContent = document.getElementById("appcontent"); if (appContent) events.listen(appContent, "load", marks.closure._onPageLoad, true); }, mappings: function () { var myModes = config.browserModes; mappings.add(myModes, ["m"], "Set mark at the cursor position", function ({ arg }) { dactyl.assert(/^[a-zA-Z]$/.test(arg), _("mark.invalid")); 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; let arg = args[0] || ""; // assert(special ^ args) dactyl.assert( special || arg, _("error.argumentRequired")); dactyl.assert(!special || !arg, _("error.invalidArgument")); marks.remove(arg, special); }, { bang: true, completer: function (context) completion.mark(context), literal: 0 }); commands.add(["ma[rk]"], "Mark current location within the web page", function (args) { let mark = args[0] || ""; dactyl.assert(mark.length <= 1, _("error.trailingCharacters")); dactyl.assert(/[a-zA-Z]/.test(mark), _("mark.invalid")); marks.add(mark); }, { argCount: "1" }); commands.add(["marks"], "Show the specified marks", function (args) { marks.list(args[0] || ""); }, { completer: function (context) completion.mark(context), literal: 0 }); }, completion: function () { completion.mark = function mark(context) { function percent(i) Math.round(i * 100); context.title = ["Mark", "HPos VPos File"]; context.keys.description = function ([, m]) (m.offset ? Math.round(m.offset.x) + " " + Math.round(m.offset.y) : percent(m.position.x) + "% " + percent(m.position.y) + "%" ) + " " + m.location; context.completions = marks.all; }; }, sanitizer: function () { sanitizer.addItem("marks", { description: "Local and URL marks", persistent: true, contains: ["history"], action: function (timespan, host) { function matchhost(url) !host || util.isDomainURL(url, host); function match(marks) (k for ([k, v] in Iterator(marks)) if (timespan.contains(v.timestamp) && matchhost(v.location))); for (let [url, local] in marks._localMarks) if (matchhost(url)) { for (let key in match(local)) delete local[key]; if (!Object.keys(local).length) marks._localMarks.remove(url); } marks._localMarks.changed(); for (let key in match(marks._urlMarks)) marks._urlMarks.remove(key); } }); } }); // vim: set fdm=marker sw=4 ts=4 et: