]> git.donarmstrong.com Git - dactyl.git/blob - common/content/marks.js
Import r6948 from upstream hg supporting Firefox up to 24.*
[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-2012 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(function (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 = params || {};
42
43         params.location = doc.documentURI.replace(/#.*/, ""),
44         params.offset = buffer.scrollPosition;
45         params.path = DOM(buffer.findScrollable(0, false)).xpath;
46         params.timestamp = Date.now() * 1000;
47         params.equals = function (m) this.location == m.location
48                                   && this.offset.x == m.offset.x
49                                   && this.offset.y == m.offset.y
50                                   && this.path == m.path;
51         return params;
52     },
53
54     /**
55      * Add a named mark for the current buffer, at its current position.
56      * If mark matches [A-Z], it's considered a URL mark, and will jump to
57      * the same position at the same URL no matter what buffer it's
58      * selected from. If it matches [a-z], it's a local mark, and can
59      * only be recalled from a buffer with a matching URL.
60      *
61      * @param {string} name The mark name.
62      * @param {boolean} silent Whether to output error messages.
63      */
64     add: function (name, silent) {
65         let mark = this.Mark();
66
67         if (Marks.isURLMark(name)) {
68             mark.tab = util.weakReference(tabs.getTab());
69             this._urlMarks.set(name, mark);
70             var message = "mark.addURL";
71         }
72         else if (Marks.isLocalMark(name)) {
73             this._localMarks.get(mark.location, {})[name] = mark;
74             this._localMarks.changed();
75             message = "mark.addLocal";
76         }
77
78         if (!silent)
79             dactyl.log(_(message, Marks.markToString(name, mark)), 5);
80         return mark;
81     },
82
83     /**
84      * Push the current buffer position onto the jump stack.
85      *
86      * @param {string} reason The reason for this scroll event. Multiple
87      *      scroll events for the same reason are coalesced. @optional
88      */
89     push: function push(reason) {
90         let store = buffer.localStore;
91         let jump  = store.jumps[store.jumpsIndex];
92
93         if (reason && jump && jump.reason == reason)
94             return;
95
96         let mark = this.add("'");
97         if (jump && mark.equals(jump.mark))
98             return;
99
100         if (!this.jumping) {
101             store.jumps[++store.jumpsIndex] = { mark: mark, reason: reason };
102             store.jumps.length = store.jumpsIndex + 1;
103
104             if (store.jumps.length > this.maxJumps) {
105                 store.jumps = store.jumps.slice(-this.maxJumps);
106                 store.jumpsIndex = store.jumps.length - 1;
107             }
108         }
109     },
110
111     maxJumps: 200,
112
113     /**
114      * Jump to the given offset in the jump stack.
115      *
116      * @param {number} offset The offset from the current position in
117      *      the jump stack to jump to.
118      * @returns {number} The actual change in offset.
119      */
120     jump: function jump(offset) {
121         let store = buffer.localStore;
122         if (offset < 0 && store.jumpsIndex == store.jumps.length - 1)
123             this.push();
124
125         return this.withSavedValues(["jumping"], function _jump() {
126             this.jumping = true;
127             let idx = Math.constrain(store.jumpsIndex + offset, 0, store.jumps.length - 1);
128             let orig = store.jumpsIndex;
129
130             if (idx in store.jumps && !dactyl.trapErrors("_scrollTo", this, store.jumps[idx].mark))
131                 store.jumpsIndex = idx;
132             return store.jumpsIndex - orig;
133         });
134     },
135
136     get jumps() {
137         let store = buffer.localStore;
138         return {
139             index: store.jumpsIndex,
140             locations: store.jumps.map(function (j) j.mark)
141         };
142     },
143
144     /**
145      * Remove all marks matching *filter*. If *special* is given, removes all
146      * local marks.
147      *
148      * @param {string} filter The list of marks to delete, e.g. "aA b C-I"
149      * @param {boolean} special Whether to delete all local marks.
150      */
151     remove: function (filter, special) {
152         if (special)
153             this._localMarks.remove(this.localURI);
154         else {
155             let pattern = util.charListToRegexp(filter, "a-zA-Z");
156             let local = this._localMarks.get(this.localURI);
157             this.all.forEach(function ([k, ]) {
158                 if (pattern.test(k)) {
159                     local && delete local[k];
160                     marks._urlMarks.remove(k);
161                 }
162             });
163             try {
164                 Iterator(local).next();
165                 this._localMarks.changed();
166             }
167             catch (e) {
168                 this._localMarks.remove(this.localURI);
169             }
170         }
171     },
172
173     /**
174      * Jumps to the named mark. See {@link #add}
175      *
176      * @param {string} char The mark to jump to.
177      */
178     jumpTo: function (char) {
179         if (Marks.isURLMark(char)) {
180             let mark = this._urlMarks.get(char);
181             dactyl.assert(mark, _("mark.unset", char));
182
183             let tab = mark.tab && mark.tab.get();
184             if (!tab || !tab.linkedBrowser || tabs.allTabs.indexOf(tab) == -1)
185                 for ([, tab] in iter(tabs.visibleTabs, tabs.allTabs)) {
186                     if (tab.linkedBrowser.contentDocument.documentURI.replace(/#.*/, "") === mark.location)
187                         break;
188                     tab = null;
189                 }
190
191             if (tab) {
192                 tabs.select(tab);
193                 let doc = tab.linkedBrowser.contentDocument;
194                 if (doc.documentURI.replace(/#.*/, "") == mark.location) {
195                     dactyl.log(_("mark.jumpingToURL", Marks.markToString(char, mark)), 5);
196                     this._scrollTo(mark);
197                 }
198                 else {
199                     this._pendingJumps.push(mark);
200
201                     let sh = tab.linkedBrowser.sessionHistory;
202                     let items = array(util.range(0, sh.count));
203
204                     let a = items.slice(0, sh.index).reverse();
205                     let b = items.slice(sh.index);
206                     a.length = b.length = Math.max(a.length, b.length);
207                     items = array(a).zip(b).flatten().compact();
208
209                     for (let i in items.iterValues()) {
210                         let entry = sh.getEntryAtIndex(i, false);
211                         if (entry.URI.spec.replace(/#.*/, "") == mark.location)
212                             return void tab.linkedBrowser.webNavigation.gotoIndex(i);
213                     }
214                     dactyl.open(mark.location);
215                 }
216             }
217             else {
218                 this._pendingJumps.push(mark);
219                 dactyl.open(mark.location, dactyl.NEW_TAB);
220             }
221         }
222         else if (Marks.isLocalMark(char)) {
223             let mark = (this._localMarks.get(this.localURI) || {})[char];
224             dactyl.assert(mark, _("mark.unset", char));
225
226             dactyl.log(_("mark.jumpingToLocal", Marks.markToString(char, mark)), 5);
227             this._scrollTo(mark);
228         }
229         else
230             dactyl.echoerr(_("mark.invalid"));
231
232     },
233
234     _scrollTo: function _scrollTo(mark) {
235         if (!mark.path)
236             var node = buffer.findScrollable(0, (mark.offset || mark.position).x);
237         else
238             for (node in DOM.XPath(mark.path, buffer.focusedFrame.document))
239                 break;
240
241         util.assert(node);
242         if (node instanceof Element)
243             DOM(node).scrollIntoView();
244
245         if (mark.offset)
246             Buffer.scrollToPosition(node, mark.offset.x, mark.offset.y);
247         else if (mark.position)
248             Buffer.scrollToPercent(node, mark.position.x * 100, mark.position.y * 100);
249     },
250
251     /**
252      * List all marks matching *filter*.
253      *
254      * @param {string} filter List of marks to show, e.g. "ab A-I".
255      */
256     list: function (filter) {
257         let marks = this.all;
258
259         dactyl.assert(marks.length > 0, _("mark.none"));
260
261         if (filter.length > 0) {
262             let pattern = util.charListToRegexp(filter, "a-zA-Z");
263             marks = marks.filter(function ([k, ]) pattern.test(k));
264             dactyl.assert(marks.length > 0, _("mark.noMatching", filter.quote()));
265         }
266
267         commandline.commandOutput(
268             template.tabular(
269                 ["Mark",   "HPos",              "VPos",              "File"],
270                 ["",       "text-align: right", "text-align: right", "color: green"],
271                 ([name,
272                   mark.offset ? Math.round(mark.offset.x)
273                               : Math.round(mark.position.x * 100) + "%",
274                   mark.offset ? Math.round(mark.offset.y)
275                               : Math.round(mark.position.y * 100) + "%",
276                   mark.location]
277                   for ([, [name, mark]] in Iterator(marks)))));
278     },
279
280     _onPageLoad: function _onPageLoad(event) {
281         let win = event.originalTarget.defaultView;
282         for (let [i, mark] in Iterator(this._pendingJumps)) {
283             if (win && win.location.href == mark.location) {
284                 this._scrollTo(mark);
285                 this._pendingJumps.splice(i, 1);
286                 return;
287             }
288         }
289     },
290 }, {
291     markToString: function markToString(name, mark) {
292         let tab = mark.tab && mark.tab.get();
293         if (mark.offset)
294             return [name, mark.location,
295                     "(" + Math.round(mark.offset.x * 100),
296                           Math.round(mark.offset.y * 100) + ")",
297                     (tab && "tab: " + tabs.index(tab))
298             ].filter(util.identity).join(", ");
299
300         if (mark.position)
301             return [name, mark.location,
302                     "(" + Math.round(mark.position.x * 100) + "%",
303                           Math.round(mark.position.y * 100) + "%)",
304                     (tab && "tab: " + tabs.index(tab))
305             ].filter(util.identity).join(", ");
306     },
307
308     isLocalMark: bind("test", /^[a-z`']$/),
309
310     isURLMark: bind("test", /^[A-Z]$/)
311 }, {
312     events: function () {
313         let appContent = document.getElementById("appcontent");
314         if (appContent)
315             events.listen(appContent, "load", marks.closure._onPageLoad, true);
316     },
317     mappings: function () {
318         var myModes = config.browserModes;
319
320         mappings.add(myModes,
321             ["m"], "Set mark at the cursor position",
322             function ({ arg }) {
323                 dactyl.assert(/^[a-zA-Z]$/.test(arg), _("mark.invalid"));
324                 marks.add(arg);
325             },
326             { arg: true });
327
328         mappings.add(myModes,
329             ["'", "`"], "Jump to the mark in the current buffer",
330             function ({ arg }) { marks.jumpTo(arg); },
331             { arg: true });
332     },
333
334     commands: function initCommands() {
335         commands.add(["delm[arks]"],
336             "Delete the specified marks",
337             function (args) {
338                 let special = args.bang;
339                 let arg = args[0] || "";
340
341                 // assert(special ^ args)
342                 dactyl.assert( special ||  arg, _("error.argumentRequired"));
343                 dactyl.assert(!special || !arg, _("error.invalidArgument"));
344
345                 marks.remove(arg, special);
346             },
347             {
348                 bang: true,
349                 completer: function (context) completion.mark(context),
350                 literal: 0
351             });
352
353         commands.add(["ma[rk]"],
354             "Mark current location within the web page",
355             function (args) {
356                 let mark = args[0] || "";
357                 dactyl.assert(mark.length <= 1, _("error.trailingCharacters"));
358                 dactyl.assert(/[a-zA-Z]/.test(mark), _("mark.invalid"));
359
360                 marks.add(mark);
361             },
362             { argCount: "1" });
363
364         commands.add(["marks"],
365             "Show the specified marks",
366             function (args) {
367                 marks.list(args[0] || "");
368             }, {
369                 completer: function (context) completion.mark(context),
370                 literal: 0
371             });
372     },
373
374     completion: function initCompletion() {
375         completion.mark = function mark(context) {
376             function percent(i) Math.round(i * 100);
377
378             context.title = ["Mark", "HPos VPos File"];
379             context.keys.description = function ([, m]) (m.offset ? Math.round(m.offset.x) + " " + Math.round(m.offset.y)
380                                                                   : percent(m.position.x) + "% " + percent(m.position.y) + "%"
381                                                         ) + " " + m.location;
382             context.completions = marks.all;
383         };
384     },
385     sanitizer: function initSanitizer() {
386         sanitizer.addItem("marks", {
387             description: "Local and URL marks",
388             persistent: true,
389             contains: ["history"],
390             action: function (timespan, host) {
391                 function matchhost(url) !host || util.isDomainURL(url, host);
392                 function match(marks) (k for ([k, v] in Iterator(marks)) if (timespan.contains(v.timestamp) && matchhost(v.location)));
393
394                 for (let [url, local] in marks._localMarks)
395                     if (matchhost(url)) {
396                         for (let key in match(local))
397                             delete local[key];
398                         if (!Object.keys(local).length)
399                             marks._localMarks.remove(url);
400                     }
401                 marks._localMarks.changed();
402
403                 for (let key in match(marks._urlMarks))
404                     marks._urlMarks.remove(key);
405             }
406         });
407     }
408 });
409
410 // vim: set fdm=marker sw=4 sts=4 ts=8 et: