]> git.donarmstrong.com Git - dactyl.git/blob - common/content/marks.js
finalize changelog for 7904
[dactyl.git] / common / content / marks.js
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>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 /**
10  * @scope modules
11  * @instance marks
12  */
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 });
17
18         try {
19             if (isArray(Iterator(this._localMarks).next()[1]))
20                 this._localMarks.clear();
21         }
22         catch (e) {}
23
24         this._pendingJumps = [];
25     },
26
27     /**
28      * @property {Array} Returns all marks, both local and URL, in a sorted
29      *     array.
30      */
31     get all() iter(this._localMarks.get(this.localURI) || {},
32                    this._urlMarks
33                   ).sort((a, b) => String.localeCompare(a[0], b[0])),
34
35     get localURI() buffer.focusedFrame.document.documentURI.replace(/#.*/, ""),
36
37     Mark: function Mark(params={}) {
38         let win = buffer.focusedFrame;
39         let doc = win.document;
40
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;
49         return params;
50     },
51
52     /**
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.
58      *
59      * @param {string} name The mark name.
60      * @param {boolean} silent Whether to output error messages.
61      */
62     add: function (name, silent) {
63         let mark = this.Mark();
64
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";
70         }
71         else if (Marks.isLocalMark(name)) {
72             this._localMarks.get(mark.location, {})[name] = mark;
73             this._localMarks.changed();
74             message = "mark.addLocal";
75         }
76
77         if (!silent)
78             dactyl.log(_(message, Marks.markToString(name, mark)), 5);
79         return mark;
80     },
81
82     /**
83      * Push the current buffer position onto the jump stack.
84      *
85      * @param {string} reason The reason for this scroll event. Multiple
86      *      scroll events for the same reason are coalesced. @optional
87      */
88     push: function push(reason) {
89         let store = buffer.localStore;
90         let jump  = store.jumps[store.jumpsIndex];
91
92         if (reason && jump && jump.reason == reason)
93             return;
94
95         let mark = this.add("'");
96         if (jump && mark.equals(jump.mark))
97             return;
98
99         if (!this.jumping) {
100             store.jumps[++store.jumpsIndex] = { mark: mark, reason: reason };
101             store.jumps.length = store.jumpsIndex + 1;
102
103             if (store.jumps.length > this.maxJumps) {
104                 store.jumps = store.jumps.slice(-this.maxJumps);
105                 store.jumpsIndex = store.jumps.length - 1;
106             }
107         }
108     },
109
110     maxJumps: 200,
111
112     /**
113      * Jump to the given offset in the jump stack.
114      *
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.
118      */
119     jump: function jump(offset) {
120         let store = buffer.localStore;
121         if (offset < 0 && store.jumpsIndex == store.jumps.length - 1)
122             this.push();
123
124         return this.withSavedValues(["jumping"], function _jump() {
125             this.jumping = true;
126             let idx = Math.constrain(store.jumpsIndex + offset, 0, store.jumps.length - 1);
127             let orig = store.jumpsIndex;
128
129             if (idx in store.jumps && !dactyl.trapErrors("_scrollTo", this, store.jumps[idx].mark))
130                 store.jumpsIndex = idx;
131             return store.jumpsIndex - orig;
132         });
133     },
134
135     get jumps() {
136         let store = buffer.localStore;
137         return {
138             index: store.jumpsIndex,
139             locations: store.jumps.map(j => j.mark)
140         };
141     },
142
143     /**
144      * Remove all marks matching *filter*. If *special* is given, removes all
145      * local marks.
146      *
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.
149      */
150     remove: function (filter, special) {
151         if (special)
152             this._localMarks.remove(this.localURI);
153         else {
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);
160                 }
161             });
162             try {
163                 Iterator(local).next();
164                 this._localMarks.changed();
165             }
166             catch (e) {
167                 this._localMarks.remove(this.localURI);
168             }
169         }
170     },
171
172     /**
173      * Jumps to the named mark. See {@link #add}
174      *
175      * @param {string} char The mark to jump to.
176      */
177     jumpTo: function (char) {
178         if (Marks.isURLMark(char)) {
179             let mark = this._urlMarks.get(char);
180             dactyl.assert(mark, _("mark.unset", char));
181
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)
186                         break;
187                     tab = null;
188                 }
189
190             if (tab) {
191                 tabs.select(tab);
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);
196                 }
197                 else {
198                     this._pendingJumps.push(mark);
199
200                     let sh = tab.linkedBrowser.sessionHistory;
201                     let items = array(util.range(0, sh.count));
202
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();
207
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);
212                     }
213                     dactyl.open(mark.location);
214                 }
215             }
216             else {
217                 this._pendingJumps.push(mark);
218                 dactyl.open(mark.location, dactyl.NEW_TAB);
219             }
220         }
221         else if (Marks.isLocalMark(char)) {
222             let mark = (this._localMarks.get(this.localURI) || {})[char];
223             dactyl.assert(mark, _("mark.unset", char));
224
225             dactyl.log(_("mark.jumpingToLocal", Marks.markToString(char, mark)), 5);
226             this._scrollTo(mark);
227         }
228         else
229             dactyl.echoerr(_("mark.invalid"));
230
231     },
232
233     _scrollTo: function _scrollTo(mark) {
234         if (!mark.path)
235             var node = buffer.findScrollable(0, (mark.offset || mark.position).x);
236         else
237             for (node in DOM.XPath(mark.path, buffer.focusedFrame.document))
238                 break;
239
240         util.assert(node);
241         if (node instanceof Element)
242             DOM(node).scrollIntoView();
243
244         if (mark.offset)
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);
248     },
249
250     /**
251      * List all marks matching *filter*.
252      *
253      * @param {string} filter List of marks to show, e.g. "ab A-I".
254      */
255     list: function (filter) {
256         let marks = this.all;
257
258         dactyl.assert(marks.length > 0, _("mark.none"));
259
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()));
264         }
265
266         commandline.commandOutput(
267             template.tabular(
268                 ["Mark",   "HPos",              "VPos",              "File"],
269                 ["",       "text-align: right", "text-align: right", "color: green"],
270                 ([name,
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) + "%",
275                   mark.location]
276                   for ([, [name, mark]] in Iterator(marks)))));
277     },
278
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);
285                 return;
286             }
287         }
288     },
289 }, {
290     markToString: function markToString(name, mark) {
291         let tab = mark.tab && mark.tab.get();
292         if (mark.offset)
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(", ");
298
299         if (mark.position)
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(", ");
305     },
306
307     isLocalMark: bind("test", /^[a-z`']$/),
308
309     isURLMark: bind("test", /^[A-Z]$/)
310 }, {
311     events: function () {
312         let appContent = document.getElementById("appcontent");
313         if (appContent)
314             events.listen(appContent, "load", marks.bound._onPageLoad, true);
315     },
316     mappings: function () {
317         var myModes = config.browserModes;
318
319         mappings.add(myModes,
320             ["m"], "Set mark at the cursor position",
321             function ({ arg }) {
322                 dactyl.assert(/^[a-zA-Z]$/.test(arg), _("mark.invalid"));
323                 marks.add(arg);
324             },
325             { arg: true });
326
327         mappings.add(myModes,
328             ["'", "`"], "Jump to the mark in the current buffer",
329             function ({ arg }) { marks.jumpTo(arg); },
330             { arg: true });
331     },
332
333     commands: function initCommands() {
334         commands.add(["delm[arks]"],
335             "Delete the specified marks",
336             function (args) {
337                 let special = args.bang;
338                 let arg = args[0] || "";
339
340                 // assert(special ^ args)
341                 dactyl.assert( special ||  arg, _("error.argumentRequired"));
342                 dactyl.assert(!special || !arg, _("error.invalidArgument"));
343
344                 marks.remove(arg, special);
345             },
346             {
347                 bang: true,
348                 completer: function (context) completion.mark(context),
349                 literal: 0
350             });
351
352         commands.add(["ma[rk]"],
353             "Mark current location within the web page",
354             function (args) {
355                 let mark = args[0] || "";
356                 dactyl.assert(mark.length <= 1, _("error.trailingCharacters"));
357                 dactyl.assert(/[a-zA-Z]/.test(mark), _("mark.invalid"));
358
359                 marks.add(mark);
360             },
361             { argCount: "1" });
362
363         commands.add(["marks"],
364             "Show the specified marks",
365             function (args) {
366                 marks.list(args[0] || "");
367             }, {
368                 completer: function (context) completion.mark(context),
369                 literal: 0
370             });
371     },
372
373     completion: function initCompletion() {
374         completion.mark = function mark(context) {
375             function percent(i) Math.round(i * 100);
376
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;
382         };
383     },
384     sanitizer: function initSanitizer() {
385         sanitizer.addItem("marks", {
386             description: "Local and URL marks",
387             persistent: true,
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)));
392
393                 for (let [url, local] in marks._localMarks)
394                     if (matchhost(url)) {
395                         for (let key in match(local))
396                             delete local[key];
397                         if (!Object.keys(local).length)
398                             marks._localMarks.remove(url);
399                     }
400                 marks._localMarks.changed();
401
402                 for (let key in match(marks._urlMarks))
403                     marks._urlMarks.remove(key);
404             }
405         });
406     }
407 });
408
409 // vim: set fdm=marker sw=4 sts=4 ts=8 et: