forked from adobe/brackets
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfoldgutter.js
More file actions
414 lines (386 loc) · 17.3 KB
/
foldgutter.js
File metadata and controls
414 lines (386 loc) · 17.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Based on http://codemirror.net/addon/fold/foldgutter.js
// Modified by Patrick Oladimeji for Brackets
/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */
/*global define, brackets, document, window, $*/
define(function (require, exports, module) {
"use strict";
var CodeMirror = brackets.getModule("thirdparty/CodeMirror/lib/codemirror"),
prefs = require("Prefs");
function State(options) {
this.options = options;
this.from = this.to = 0;
}
function parseOptions(opts) {
if (opts === true) { opts = {}; }
if (!opts.gutter) { opts.gutter = "CodeMirror-foldgutter"; }
if (!opts.indicatorOpen) { opts.indicatorOpen = "CodeMirror-foldgutter-open"; }
if (!opts.indicatorFolded) { opts.indicatorFolded = "CodeMirror-foldgutter-folded"; }
return opts;
}
/**
* Utility for creating fold markers in fold gutter
* @param {string} spec the className for the marker
* @return {HTMLElement} a htmlelement representing the fold marker
*/
function marker(spec) {
var elt = document.createElement("div");
elt.className = spec;
return elt;
}
/**
* Checks whether or not a marker is a code-folding marker
* @param {Object} m a CodeMirror TextMarker object
* @returns {boolean} true if the marker is a codefolding range marker or false otherwise
*/
function isFold(m) {
return m.__isFold;
}
/**
* Updates the gutter markers for the specified range
* @param {!CodeMirror} cm the CodeMirror instance for the active editor
* @param {!number} from the starting line for the update
* @param {!number} to the ending line for the update
*/
function updateFoldInfo(cm, from, to) {
var minFoldSize = prefs.getSetting("minFoldSize") || 2;
var opts = cm.state.foldGutter.options;
var fade = prefs.getSetting("hideUntilMouseover");
var $gutter = $(cm.getGutterElement());
var i = from;
function clear(m) {
return m.clear();
}
/**
* @private
* helper function to check if the given line is in a folded region in the editor.
* @param {number} line the
* @return {Object} the range that hides the specified line or undefine if the line is not hidden
*/
function _isCurrentlyFolded(line) {
var keys = Object.keys(cm._lineFolds), i = 0, range;
while (i < keys.length) {
range = cm._lineFolds[keys[i]];
if (range.from.line < line && range.to.line >= line) {
return range;
}
i++;
}
}
/**
This case is needed when unfolding a region that does not cause the viewport to change.
For instance in a file with about 15 lines, if some code regions are folded and unfolded, the
viewport change event isn't fired by CodeMirror. The setTimeout is a workaround to trigger the
gutter update after the viewport has been drawn.
*/
if (i === to) {
window.setTimeout(function () {
var vp = cm.getViewport();
updateFoldInfo(cm, vp.from, vp.to);
}, 200);
}
while (i < to) {
var sr = _isCurrentlyFolded(i), // surrounding range for the current line if one exists
range;
var mark = marker("CodeMirror-foldgutter-blank");
var pos = CodeMirror.Pos(i),
func = opts.rangeFinder || CodeMirror.fold.auto;
// don't look inside collapsed ranges
if (sr) {
i = sr.to.line + 1;
} else {
range = cm._lineFolds[i] || (func && func(cm, pos));
if (!fade || (fade && $gutter.is(":hover"))) {
if (cm.isFolded(i)) {
// expand fold if invalid
if (range) {
mark = marker(opts.indicatorFolded);
} else {
cm.findMarksAt(pos).filter(isFold)
.forEach(clear);
}
} else {
if (range && range.to.line - range.from.line >= minFoldSize) {
mark = marker(opts.indicatorOpen);
}
}
}
cm.setGutterMarker(i, opts.gutter, mark);
i++;
}
}
}
/**
* Updates the fold information in the viewport for the specified range
* @param {CodeMirror} cm the instance of the CodeMirror object
* @param {?number} from the starting line number for the update
* @param {?number} to the end line number for the update
*/
function updateInViewport(cm, from, to) {
var vp = cm.getViewport(), state = cm.state.foldGutter;
from = isNaN(from) ? vp.from : from;
to = isNaN(to) ? vp.to : to;
if (!state) { return; }
cm.operation(function () {
updateFoldInfo(cm, from, to);
});
state.from = from;
state.to = to;
}
/**
* Clears the code folding gutter
* @param {!CodeMirror} cm the CodeMirror instance for the active editor
*/
function clearGutter(cm) {
var opts = cm.state.foldGutter.options;
cm.clearGutter(opts.gutter);
var blank = marker("CodeMirror-foldgutter-blank");
var vp = cm.getViewport();
cm.operation(function () {
cm.eachLine(vp.from, vp.to, function (line) {
cm.setGutterMarker(line.lineNo(), opts.gutter, blank);
});
});
}
/**
* Helper function to return the fold text marker on a line in an editor
* @param {CodeMirror} cm The CodeMirror instance for the active editor
* @param {Number} line The line number representing the position of the fold marker
* @returns {TextMarker} A CodeMirror TextMarker object
*/
function getFoldOnLine(cm, line) {
var pos = CodeMirror.Pos(line);
var folds = cm.findMarksAt(pos) || [];
folds = folds.filter(isFold);
return folds.length ? folds[0] : undefined;
}
/**
* Synchronises the code folding states in the CM doc to cm._lineFolds cache.
* When an undo operation is done, if folded code fragments are restored, then
* we need to update cm._lineFolds with the fragments
* @param {Object} cm cm the CodeMirror instance for the active editor
* @param {Object} from starting position in the doc to sync the fold states from
* @param {[[Type]]} lineAdded a number to show how many lines where added to the document
*/
function syncDocToFoldsCache(cm, from, lineAdded) {
var minFoldSize = prefs.getSetting("minFoldSize") || 2;
var i, fold, range;
if (lineAdded <= 0) {
return;
}
for (i = from; i <= from + lineAdded; i = i + 1) {
fold = getFoldOnLine(cm, i);
if (fold) {
range = fold.find();
if (range && range.to.line - range.from.line >= minFoldSize) {
cm._lineFolds[i] = range;
i = range.to.line;
} else {
delete cm._lineFolds[i];
}
}
}
}
/**
* Helper function to move a fold range object by the specified number of lines
* @param {Object} range An object specifying the fold range to move. It contains {from, to} which are CodeMirror.Pos objects.
* @param {Number} numLines A positive or negative number representing the numbe of lines to move the range by
*/
function moveRange(range, numLines) {
return {from: CodeMirror.Pos(range.from.line + numLines, range.from.ch),
to: CodeMirror.Pos(range.to.line + numLines, range.to.ch)};
}
/**
* Updates the line folds cache usually when the document changes.
* The following cases are accounted for:
* 1. When the change does not add a new line to the document we check if the line being modified
* is folded. If that is the case, changes to this line might affect the range stored in the cache
* so we update the range using the range finder function.
* 2. If lines have been added, we need to update the records for all lines in the folds cache
* which are greater than the line position at which we are adding the new line(s). When existing
* folds are above the addition we keep the original position in the cache.
* 3. If lines are being removed, we need to update the records for all lines in the folds cache which are
* greater than the line position at which we are removing the new lines, while making sure to
* not include any folded lines in the cache that are part of the removed chunk.
* @param {!CodeMirror} cm the CodeMirror instance for the active editor
* @param {!number} from the line number designating the start position of the change
* @param {!number} linesDiff a number to show how many lines where removed or added to the document.
* This value is negative for deletions and positive for additions.
*/
function updateFoldsCache(cm, from, linesDiff) {
var oldRange, newRange;
var minFoldSize = prefs.getSetting("minFoldSize") || 2;
var foldedLines = Object.keys(cm._lineFolds).map(function (d) {
return +d;
});
var opts = cm.state.foldGutter.options || {};
var rf = opts.rangeFinder || CodeMirror.fold.auto;
if (linesDiff === 0) {
if (foldedLines.indexOf(from) >= 0) {
newRange = rf(cm, CodeMirror.Pos(from));
if (newRange && newRange.to.line - newRange.from.line >= minFoldSize) {
cm._lineFolds[from] = newRange;
} else {
delete cm._lineFolds[from];
}
}
} else if (foldedLines.length) {
var newFolds = {};
foldedLines.forEach(function (line) {
oldRange = cm._lineFolds[line];
//update range with lines-diff
newRange = moveRange(oldRange, linesDiff);
// for removed lines we want to check lines that lie outside the deleted range
if (linesDiff < 0) {
if (line < from) {
newFolds[line] = oldRange;
} else if (line >= from + Math.abs(linesDiff)) {
newFolds[line + linesDiff] = newRange;
}
} else {
if (line < from) {
newFolds[line] = oldRange;
} else if (line >= from) {
newFolds[line + linesDiff] = newRange;
}
}
});
cm._lineFolds = newFolds;
}
}
/**
* Triggered when the content of the document changes. When the entire content of the document
* is changed - e.g., changes made from a different editor, the same lineFolds are kept only if
* they are still valid in the context of the new document content.
* @param {!CodeMirror} cm the CodeMirror instance for the active editor
* @param {!Object} changeObj detailed information about the change that occurred in the document
*/
function onChange(cm, changeObj) {
if (changeObj.origin === "setValue") {//text content has changed outside of brackets
var folds = cm.getValidFolds(cm._lineFolds);
cm._lineFolds = folds;
Object.keys(folds).forEach(function (line) {
cm.foldCode(+line);
});
} else {
var state = cm.state.foldGutter;
var lineChanges = changeObj.text.length - changeObj.removed.length;
// for undo actions that add new line(s) to the document first update the folds cache as normal
// and then update the folds cache with any line folds that exist in the new lines
if (changeObj.origin === "undo" && lineChanges > 0) {
updateFoldsCache(cm, changeObj.from.line, lineChanges);
syncDocToFoldsCache(cm, changeObj.from.line, lineChanges);
} else {
updateFoldsCache(cm, changeObj.from.line, lineChanges);
}
if (lineChanges !== 0) {
updateFoldInfo(cm, Math.max(0, changeObj.from.line + lineChanges), Math.max(0, changeObj.from.line + lineChanges) + 1);
}
state.from = changeObj.from.line;
state.to = 0;
window.clearTimeout(state.changeUpdate);
state.changeUpdate = window.setTimeout(function () {
updateInViewport(cm);
}, 600);
}
}
/**
* Triggered on viewport changes e.g., user scrolls or resizes the viewport.
* @param {!CodeMirror} cm the CodeMirror instance for the active editor
*/
function onViewportChange(cm) {
var state = cm.state.foldGutter;
window.clearTimeout(state.changeUpdate);
state.changeUpdate = window.setTimeout(function () {
var vp = cm.getViewport();
if (state.from === state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {
updateInViewport(cm);
} else {
cm.operation(function () {
if (vp.from < state.from) {
updateFoldInfo(cm, vp.from, state.from);
state.from = vp.from;
}
if (vp.to > state.to) {
updateFoldInfo(cm, state.to, vp.to);
state.to = vp.to;
} else {
updateFoldInfo(cm, vp.from, vp.to);
state.to = vp.to;
state.from = vp.from;
}
});
}
}, 400);
}
/**
* Triggered when the cursor moves in the editor and used to detect text selection changes
* in the editor.
* @param {!CodeMirror} cm the CodeMirror instance for the active editor
*/
function onCursorActivity(cm) {
var state = cm.state.foldGutter;
var vp = cm.getViewport();
window.clearTimeout(state.changeUpdate);
state.changeUpdate = window.setTimeout(function () {
//need to render the entire visible viewport to remove fold marks rendered from previous selections if any
updateInViewport(cm, vp.from, vp.to);
}, 400);
}
/**
* Triggered when a code segment is folded.
* @param {!CodeMirror} cm the CodeMirror instance for the active editor
* @param {!Object} from the ch and line position that designates the start of the region
* @param {!Object} to the ch and line position that designates the end of the region
*/
function onFold(cm, from, to) {
var state = cm.state.foldGutter;
updateFoldInfo(cm, from.line, from.line + 1);
}
/**
* Triggered when a folded code segment is unfolded.
* @param {!CodeMirror} cm the CodeMirror instance for the active editor
* @param {!{line:number, ch:number}} from the ch and line position that designates the start of the region
* @param {!{line:number, ch:number}} to the ch and line position that designates the end of the region
*/
function onUnFold(cm, from, to) {
var state = cm.state.foldGutter;
var vp = cm.getViewport();
delete cm._lineFolds[from.line];
updateFoldInfo(cm, from.line, to.line || vp.to);
}
/**
* Initialises the fold gutter and registers event handlers for changes to document, viewport
* and user interactions.
*/
function init() {
CodeMirror.defineOption("foldGutter", false, function (cm, val, old) {
if (old && old !== CodeMirror.Init) {
cm.clearGutter(cm.state.foldGutter.options.gutter);
cm.state.foldGutter = null;
cm.off("gutterClick", old.onGutterClick);
cm.off("change", onChange);
cm.off("viewportChange", onViewportChange);
cm.off("cursorActivity", onCursorActivity);
cm.off("fold", onFold);
cm.off("unfold", onUnFold);
cm.off("swapDoc", updateInViewport);
}
if (val) {
cm.state.foldGutter = new State(parseOptions(val));
updateInViewport(cm);
cm.on("gutterClick", val.onGutterClick);
cm.on("change", onChange);
cm.on("viewportChange", onViewportChange);
cm.on("cursorActivity", onCursorActivity);
cm.on("fold", onFold);
cm.on("unfold", onUnFold);
cm.on("swapDoc", updateInViewport);
}
});
}
exports.init = init;
exports.clearGutter = clearGutter;
exports.updateInViewport = updateInViewport;
});