forked from mozilla/brackets
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathEditor.js
2649 lines (2355 loc) · 109 KB
/
Editor.js
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
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright (c) 2012 - present Adobe Systems Incorporated. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/**
* Editor is a 1-to-1 wrapper for a CodeMirror editor instance. It layers on Brackets-specific
* functionality and provides APIs that cleanly pass through the bits of CodeMirror that the rest
* of our codebase may want to interact with. An Editor is always backed by a Document, and stays
* in sync with its content; because Editor keeps the Document alive, it's important to always
* destroy() an Editor that's going away so it can release its Document ref.
*
* For now, there's a distinction between the "master" Editor for a Document - which secretly acts
* as the Document's internal model of the text state - and the multitude of "slave" secondary Editors
* which, via Document, sync their changes to and from that master.
*
* For now, direct access to the underlying CodeMirror object is still possible via `_codeMirror` --
* but this is considered deprecated and may go away.
*
* The Editor object dispatches the following events:
* - keydown, keypress, keyup -- When any key event happens in the editor (whether it changes the
* text or not). Handlers are passed `(BracketsEvent, Editor, KeyboardEvent)`. The 3nd arg is the
* raw DOM event. Note: most listeners will only want to listen for "keypress".
* - cursorActivity -- When the user moves the cursor or changes the selection, or an edit occurs.
* Note: do not listen to this in order to be generally informed of edits--listen to the
* "change" event on Document instead.
* - scroll -- When the editor is scrolled, either by user action or programmatically.
* - lostContent -- When the backing Document changes in such a way that this Editor is no longer
* able to display accurate text. This occurs if the Document's file is deleted, or in certain
* Document->editor syncing edge cases that we do not yet support (the latter cause will
* eventually go away).
* - optionChange -- Triggered when an option for the editor is changed. The 2nd arg to the listener
* is a string containing the editor option that is changing. The 3rd arg, which can be any
* data type, is the new value for the editor option.
* - beforeDestroy - Triggered before the object is about to dispose of all its internal state data
* so that listeners can cache things like scroll pos, etc...
*
* The Editor also dispatches "change" events internally, but you should listen for those on
* Documents, not Editors.
*
* To listen for events, do something like this: (see EventDispatcher for details on this pattern)
* `editorInstance.on("eventname", handler);`
*/
define(function (require, exports, module) {
"use strict";
var AnimationUtils = require("utils/AnimationUtils"),
Async = require("utils/Async"),
CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"),
LanguageManager = require("language/LanguageManager"),
EventDispatcher = require("utils/EventDispatcher"),
Menus = require("command/Menus"),
PerfUtils = require("utils/PerfUtils"),
PopUpManager = require("widgets/PopUpManager"),
PreferencesManager = require("preferences/PreferencesManager"),
Strings = require("strings"),
TextRange = require("document/TextRange").TextRange,
TokenUtils = require("utils/TokenUtils"),
ValidationUtils = require("utils/ValidationUtils"),
ViewUtils = require("utils/ViewUtils"),
MainViewManager = require("view/MainViewManager"),
_ = require("thirdparty/lodash");
/** Editor preferences */
var CLOSE_BRACKETS = "closeBrackets",
CLOSE_TAGS = "closeTags",
DRAG_DROP = "dragDropText",
HIGHLIGHT_MATCHES = "highlightMatches",
LINEWISE_COPY_CUT = "lineWiseCopyCut",
SCROLL_PAST_END = "scrollPastEnd",
SHOW_CURSOR_SELECT = "showCursorWhenSelecting",
SHOW_LINE_NUMBERS = "showLineNumbers",
SMART_INDENT = "smartIndent",
SOFT_TABS = "softTabs",
SPACE_UNITS = "spaceUnits",
STYLE_ACTIVE_LINE = "styleActiveLine",
TAB_SIZE = "tabSize",
UPPERCASE_COLORS = "uppercaseColors",
USE_TAB_CHAR = "useTabChar",
WORD_WRAP = "wordWrap",
INDENT_LINE_COMMENT = "indentLineComment",
ALLOW_JAVASCRIPT = "allowJavaScript",
AUTO_UPDATE = "autoUpdate";
var cmOptions = {};
/**
* Constants
* @type {number}
*/
var MIN_SPACE_UNITS = 1,
MIN_TAB_SIZE = 1,
DEFAULT_SPACE_UNITS = 4,
DEFAULT_TAB_SIZE = 4,
MAX_SPACE_UNITS = 10,
MAX_TAB_SIZE = 10;
// Mappings from Brackets preferences to CodeMirror options
cmOptions[CLOSE_BRACKETS] = "autoCloseBrackets";
cmOptions[CLOSE_TAGS] = "autoCloseTags";
cmOptions[DRAG_DROP] = "dragDrop";
cmOptions[HIGHLIGHT_MATCHES] = "highlightSelectionMatches";
cmOptions[LINEWISE_COPY_CUT] = "lineWiseCopyCut";
cmOptions[SCROLL_PAST_END] = "scrollPastEnd";
cmOptions[SHOW_CURSOR_SELECT] = "showCursorWhenSelecting";
cmOptions[SHOW_LINE_NUMBERS] = "lineNumbers";
cmOptions[SMART_INDENT] = "smartIndent";
cmOptions[SPACE_UNITS] = "indentUnit";
cmOptions[STYLE_ACTIVE_LINE] = "styleActiveLine";
cmOptions[TAB_SIZE] = "tabSize";
cmOptions[USE_TAB_CHAR] = "indentWithTabs";
cmOptions[WORD_WRAP] = "lineWrapping";
cmOptions[ALLOW_JAVASCRIPT] = "allowJavaScript";
cmOptions[AUTO_UPDATE] = "autoUpdate";
PreferencesManager.definePreference(CLOSE_BRACKETS, "boolean", true, {
description: Strings.DESCRIPTION_CLOSE_BRACKETS
});
PreferencesManager.definePreference(CLOSE_TAGS, "object", { whenOpening: true, whenClosing: true, indentTags: [] }, {
description: Strings.DESCRIPTION_CLOSE_TAGS,
keys: {
dontCloseTags: {
type: "array",
description: Strings.DESCRIPTION_CLOSE_TAGS_DONT_CLOSE_TAGS
},
whenOpening: {
type: "boolean",
description: Strings.DESCRIPTION_CLOSE_TAGS_WHEN_OPENING,
initial: true
},
whenClosing: {
type: "boolean",
description: Strings.DESCRIPTION_CLOSE_TAGS_WHEN_CLOSING,
initial: true
},
indentTags: {
type: "array",
description: Strings.DESCRIPTION_CLOSE_TAGS_INDENT_TAGS
}
}
});
PreferencesManager.definePreference(DRAG_DROP, "boolean", false, {
description: Strings.DESCRIPTION_DRAG_DROP_TEXT
});
PreferencesManager.definePreference(HIGHLIGHT_MATCHES, "boolean", false, {
description: Strings.DESCRIPTION_HIGHLIGHT_MATCHES,
keys: {
showToken: {
type: "boolean",
description: Strings.DESCRIPTION_HIGHLIGHT_MATCHES_SHOW_TOKEN,
initial: false
},
wordsOnly: {
type: "boolean",
description: Strings.DESCRIPTION_HIGHLIGHT_MATCHES_WORDS_ONLY,
initial: false
}
}
});
PreferencesManager.definePreference(LINEWISE_COPY_CUT, "boolean", true, {
description: Strings.DESCRIPTION_LINEWISE_COPY_CUT
});
PreferencesManager.definePreference(SCROLL_PAST_END, "boolean", false, {
description: Strings.DESCRIPTION_SCROLL_PAST_END
});
PreferencesManager.definePreference(SHOW_CURSOR_SELECT, "boolean", false, {
description: Strings.DESCRIPTION_SHOW_CURSOR_WHEN_SELECTING
});
PreferencesManager.definePreference(SHOW_LINE_NUMBERS, "boolean", true, {
description: Strings.DESCRIPTION_SHOW_LINE_NUMBERS
});
PreferencesManager.definePreference(SMART_INDENT, "boolean", true, {
description: Strings.DESCRIPTION_SMART_INDENT
});
PreferencesManager.definePreference(SOFT_TABS, "boolean", true, {
description: Strings.DESCRIPTION_SOFT_TABS
});
PreferencesManager.definePreference(SPACE_UNITS, "number", DEFAULT_SPACE_UNITS, {
validator: _.partialRight(ValidationUtils.isIntegerInRange, MIN_SPACE_UNITS, MAX_SPACE_UNITS),
description: Strings.DESCRIPTION_SPACE_UNITS
});
PreferencesManager.definePreference(STYLE_ACTIVE_LINE, "boolean", false, {
description: Strings.DESCRIPTION_STYLE_ACTIVE_LINE
});
PreferencesManager.definePreference(TAB_SIZE, "number", DEFAULT_TAB_SIZE, {
validator: _.partialRight(ValidationUtils.isIntegerInRange, MIN_TAB_SIZE, MAX_TAB_SIZE),
description: Strings.DESCRIPTION_TAB_SIZE
});
PreferencesManager.definePreference(UPPERCASE_COLORS, "boolean", false, {
description: Strings.DESCRIPTION_UPPERCASE_COLORS
});
PreferencesManager.definePreference(USE_TAB_CHAR, "boolean", false, {
description: Strings.DESCRIPTION_USE_TAB_CHAR
});
PreferencesManager.definePreference(WORD_WRAP, "boolean", true, {
description: Strings.DESCRIPTION_WORD_WRAP
});
PreferencesManager.definePreference(INDENT_LINE_COMMENT, "boolean", false, {
description: Strings.DESCRIPTION_INDENT_LINE_COMMENT
});
PreferencesManager.definePreference(ALLOW_JAVASCRIPT, "boolean", true, {
description: Strings.DESCRIPTION_ALLOW_JAVASCRIPT
});
PreferencesManager.definePreference(AUTO_UPDATE, "boolean", true, {
description: Strings.DESCRIPTION_AUTO_UPDATE
});
var editorOptions = Object.keys(cmOptions);
/** Editor preferences */
/**
* Guard flag to prevent focus() reentrancy (via blur handlers), even across Editors
* @type {boolean}
*/
var _duringFocus = false;
/**
* Constant: ignore upper boundary when centering text
* @type {number}
*/
var BOUNDARY_CHECK_NORMAL = 0,
BOUNDARY_IGNORE_TOP = 1;
/**
* @private
* Create a copy of the given CodeMirror position
* @param {!CodeMirror.Pos} pos
* @return {CodeMirror.Pos}
*/
function _copyPos(pos) {
return new CodeMirror.Pos(pos.line, pos.ch);
}
/**
* Helper functions to check options.
* @param {number} options BOUNDARY_CHECK_NORMAL or BOUNDARY_IGNORE_TOP
*/
function _checkTopBoundary(options) {
return (options !== BOUNDARY_IGNORE_TOP);
}
function _checkBottomBoundary(options) {
return true;
}
/**
* Helper function to build preferences context based on the full path of
* the file.
*
* @param {string} fullPath Full path of the file
*
* @return {*} A context for the specified file name
*/
function _buildPreferencesContext(fullPath) {
return PreferencesManager._buildContext(fullPath,
fullPath ? LanguageManager.getLanguageForPath(fullPath).getId() : undefined);
}
/**
* List of all current (non-destroy()ed) Editor instances. Needed when changing global preferences
* that affect all editors, e.g. tabbing or color scheme settings.
* @type {Array.<Editor>}
*/
var _instances = [];
/**
* Creates a new CodeMirror editor instance bound to the given Document. The Document need not have
* a "master" Editor realized yet, even if makeMasterEditor is false; in that case, the first time
* an edit occurs we will automatically ask EditorManager to create a "master" editor to render the
* Document modifiable.
*
* ALWAYS call destroy() when you are done with an Editor - otherwise it will leak a Document ref.
*
* @constructor
*
* @param {!Document} document
* @param {!boolean} makeMasterEditor If true, this Editor will set itself as the (secret) "master"
* Editor for the Document. If false, this Editor will attach to the Document as a "slave"/
* secondary editor.
* @param {!jQueryObject|DomNode} container Container to add the editor to.
* @param {{startLine: number, endLine: number}=} range If specified, range of lines within the document
* to display in this editor. Inclusive.
* @param {!Object} options If specified, contains editor options that can be passed to CodeMirror
*/
function Editor(document, makeMasterEditor, container, range, options) {
var self = this;
var isReadOnly = options && options.isReadOnly;
_instances.push(this);
// Attach to document: add ref & handlers
this.document = document;
document.addRef();
if (container.jquery) {
// CodeMirror wants a DOM element, not a jQuery wrapper
container = container.get(0);
}
var $container = $(container);
if (range) { // attach this first: want range updated before we process a change
this._visibleRange = new TextRange(document, range.startLine, range.endLine);
}
// store this-bound version of listeners so we can remove them later
this._handleDocumentChange = this._handleDocumentChange.bind(this);
this._handleDocumentDeleted = this._handleDocumentDeleted.bind(this);
this._handleDocumentLanguageChanged = this._handleDocumentLanguageChanged.bind(this);
this._doWorkingSetSync = this._doWorkingSetSync.bind(this);
document.on("change", this._handleDocumentChange);
document.on("deleted", this._handleDocumentDeleted);
document.on("languageChanged", this._handleDocumentLanguageChanged);
// To sync working sets if the view is for same doc across panes
document.on("_dirtyFlagChange", this._doWorkingSetSync);
var mode = this._getModeFromDocument();
// (if makeMasterEditor, we attach the Doc back to ourselves below once we're fully initialized)
this._inlineWidgets = [];
this._inlineWidgetQueues = {};
this._hideMarks = [];
this._lastEditorWidth = null;
this._$messagePopover = null;
// To track which pane the editor is being attached to if it's a full editor
this._paneId = null;
// To track the parent editor ( host editor at that time of creation) of an inline editor
this._hostEditor = null;
// Editor supplies some standard keyboard behavior extensions of its own
var codeMirrorKeyMap = {
"Tab": function () { self._handleTabKey(); },
"Shift-Tab": "indentLess",
"Left": function (instance) {
self._handleSoftTabNavigation(-1, "moveH");
},
"Right": function (instance) {
self._handleSoftTabNavigation(1, "moveH");
},
"Backspace": function (instance) {
self._handleSoftTabNavigation(-1, "deleteH");
},
"Delete": function (instance) {
self._handleSoftTabNavigation(1, "deleteH");
},
"Esc": function (instance) {
if (self.getSelections().length > 1) {
CodeMirror.commands.singleSelection(instance);
} else {
self.removeAllInlineWidgets();
}
},
"Home": "goLineLeftSmart",
"Cmd-Left": "goLineLeftSmart",
"End": "goLineRight",
"Cmd-Right": "goLineRight"
};
var currentOptions = this._currentOptions = _.zipObject(
editorOptions,
_.map(editorOptions, function (prefName) {
return self._getOption(prefName);
})
);
// When panes are created *after* the showLineNumbers option has been turned off
// we need to apply the show-line-padding class or the text will be juxtaposed
// to the edge of the editor which makes it not easy to read. The code below to handle
// that the option change only applies the class to panes that have already been created
// This line ensures that the class is applied to any editor created after the fact
$container.toggleClass("show-line-padding", Boolean(!this._getOption("showLineNumbers")));
// Create the CodeMirror instance
// (note: CodeMirror doesn't actually require using 'new', but jslint complains without it)
this._codeMirror = new CodeMirror(container, {
autoCloseBrackets : currentOptions[CLOSE_BRACKETS],
autoCloseTags : currentOptions[CLOSE_TAGS],
coverGutterNextToScrollbar : true,
cursorScrollMargin : 3,
dragDrop : currentOptions[DRAG_DROP],
electricChars : true,
extraKeys : codeMirrorKeyMap,
highlightSelectionMatches : currentOptions[HIGHLIGHT_MATCHES],
indentUnit : currentOptions[USE_TAB_CHAR] ? currentOptions[TAB_SIZE] : currentOptions[SPACE_UNITS],
indentWithTabs : currentOptions[USE_TAB_CHAR],
inputStyle : "textarea", // the "contenteditable" mode used on mobiles could cause issues
lineNumbers : currentOptions[SHOW_LINE_NUMBERS],
lineWiseCopyCut : currentOptions[LINEWISE_COPY_CUT],
lineWrapping : currentOptions[WORD_WRAP],
allowJavaScript : currentOptions[ALLOW_JAVASCRIPT],
autoUpdate : currentOptions[AUTO_UPDATE],
matchBrackets : { maxScanLineLength: 50000, maxScanLines: 1000 },
matchTags : { bothTags: true },
scrollPastEnd : !range && currentOptions[SCROLL_PAST_END],
showCursorWhenSelecting : currentOptions[SHOW_CURSOR_SELECT],
smartIndent : currentOptions[SMART_INDENT],
styleActiveLine : currentOptions[STYLE_ACTIVE_LINE],
tabSize : currentOptions[TAB_SIZE],
readOnly : isReadOnly
});
// Can't get CodeMirror's focused state without searching for
// CodeMirror-focused. Instead, track focus via onFocus and onBlur
// options and track state with this._focused
this._focused = false;
this._installEditorListeners();
this.on("cursorActivity", function (event, editor) {
self._handleCursorActivity(event);
});
this.on("keypress", function (event, editor, domEvent) {
self._handleKeypressEvents(domEvent);
});
this.on("change", function (event, editor, changeList) {
self._handleEditorChange(changeList);
});
this.on("focus", function (event, editor) {
if (self._hostEditor) {
// Mark the host editor as the master editor for the hosting document
self._hostEditor.document._toggleMasterEditor(self._hostEditor);
} else {
// Set this full editor as master editor for the document
self.document._toggleMasterEditor(self);
}
});
// Set code-coloring mode BEFORE populating with text, to avoid a flash of uncolored text
this._codeMirror.setOption("mode", mode);
// Initially populate with text. This will send a spurious change event, so need to make
// sure this is understood as a 'sync from document' case, not a genuine edit
this._duringSync = true;
this._resetText(document.getText());
this._duringSync = false;
if (range) {
this._updateHiddenLines();
this.setCursorPos(range.startLine, 0);
}
// Now that we're fully initialized, we can point the document back at us if needed
if (makeMasterEditor) {
document._makeEditable(this);
}
// Add scrollTop property to this object for the scroll shadow code to use
Object.defineProperty(this, "scrollTop", {
get: function () {
return this._codeMirror.getScrollInfo().top;
}
});
// Add an $el getter for Pane Views
Object.defineProperty(this, "$el", {
get: function () {
return $(this.getRootElement());
}
});
}
EventDispatcher.makeEventDispatcher(Editor.prototype);
EventDispatcher.markDeprecated(Editor.prototype, "keyEvent", "'keydown/press/up'");
Editor.prototype.markPaneId = function (paneId) {
this._paneId = paneId;
// Also add this to the pool of full editors
this.document._associateEditor(this);
// In case this Editor is initialized not as the first full editor for the document
// and the document is already dirty and present in another working set, make sure
// to add this documents to the new panes working set.
this._doWorkingSetSync(null, this.document);
};
Editor.prototype._doWorkingSetSync = function (event, doc) {
if (doc === this.document && this._paneId && this.document.isDirty) {
MainViewManager.addToWorkingSet(this._paneId, this.document.file, -1, false);
}
};
/**
* Removes this editor from the DOM and detaches from the Document. If this is the "master"
* Editor that is secretly providing the Document's backing state, then the Document reverts to
* a read-only string-backed mode.
*/
Editor.prototype.destroy = function () {
this.trigger("beforeDestroy", this);
// CodeMirror docs for getWrapperElement() say all you have to do is "Remove this from your
// tree to delete an editor instance."
$(this.getRootElement()).remove();
_instances.splice(_instances.indexOf(this), 1);
// Disconnect from Document
this.document.releaseRef();
this.document.off("change", this._handleDocumentChange);
this.document.off("deleted", this._handleDocumentDeleted);
this.document.off("languageChanged", this._handleDocumentLanguageChanged);
this.document.off("_dirtyFlagChange", this._doWorkingSetSync);
if (this._visibleRange) { // TextRange also refs the Document
this._visibleRange.dispose();
}
// If we're the Document's master editor, disconnecting from it has special meaning
if (this.document._masterEditor === this) {
this.document._makeNonEditable();
} else {
this.document._disassociateEditor(this);
}
// Destroying us destroys any inline widgets we're hosting. Make sure their closeCallbacks
// run, at least, since they may also need to release Document refs
var self = this;
this._inlineWidgets.forEach(function (inlineWidget) {
self._removeInlineWidgetInternal(inlineWidget);
});
};
/**
* @private
* Handle any cursor movement in editor, including selecting and unselecting text.
* @param {!Event} event
*/
Editor.prototype._handleCursorActivity = function (event) {
this._updateStyleActiveLine();
};
/**
* @private
* Removes any whitespace after one of ]{}) to prevent trailing whitespace when auto-indenting
*/
Editor.prototype._handleWhitespaceForElectricChars = function () {
var self = this,
instance = this._codeMirror,
selections,
lineStr;
selections = this.getSelections().map(function (sel) {
lineStr = instance.getLine(sel.end.line);
if (lineStr && !/\S/.test(lineStr)) {
// if the line is all whitespace, move the cursor to the end of the line
// before indenting so that embedded whitespace such as indents are not
// orphaned to the right of the electric char being inserted
sel.end.ch = self.document.getLine(sel.end.line).length;
}
return sel;
});
this.setSelections(selections);
};
/**
* @private
* Handle CodeMirror key events.
* @param {!Event} event
*/
Editor.prototype._handleKeypressEvents = function (event) {
var keyStr = String.fromCharCode(event.which || event.keyCode);
if (/[\]\{\}\)]/.test(keyStr)) {
this._handleWhitespaceForElectricChars();
}
};
/**
* @private
* Helper function for `_handleTabKey()` (case 2) - see comment in that function.
* @param {Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}>} selections
* The selections to indent.
*/
Editor.prototype._addIndentAtEachSelection = function (selections) {
var instance = this._codeMirror,
usingTabs = instance.getOption("indentWithTabs"),
indentUnit = instance.getOption("indentUnit"),
edits = [];
_.each(selections, function (sel) {
var indentStr = "", i, numSpaces;
if (usingTabs) {
indentStr = "\t";
} else {
numSpaces = indentUnit - (sel.start.ch % indentUnit);
for (i = 0; i < numSpaces; i++) {
indentStr += " ";
}
}
edits.push({edit: {text: indentStr, start: sel.start}});
});
this.document.doMultipleEdits(edits);
};
/**
* @private
* Helper function for `_handleTabKey()` (case 3) - see comment in that function.
* @param {Array.<{start:{line:number, ch:number}, end:{line:number, ch:number}, reversed:boolean, primary:boolean}>} selections
* The selections to indent.
*/
Editor.prototype._autoIndentEachSelection = function (selections) {
// Capture all the line lengths, so we can tell if anything changed.
// Note that this function should only be called if all selections are within a single line.
var instance = this._codeMirror,
lineLengths = {};
_.each(selections, function (sel) {
lineLengths[sel.start.line] = instance.getLine(sel.start.line).length;
});
// First, try to do a smart indent on all selections.
CodeMirror.commands.indentAuto(instance);
// If there were no code or selection changes, then indent each selection one more indent.
var changed = false,
newSelections = this.getSelections();
if (newSelections.length === selections.length) {
_.each(selections, function (sel, index) {
var newSel = newSelections[index];
if (CodeMirror.cmpPos(sel.start, newSel.start) !== 0 ||
CodeMirror.cmpPos(sel.end, newSel.end) !== 0 ||
instance.getLine(sel.start.line).length !== lineLengths[sel.start.line]) {
changed = true;
// Bail - we don't need to look any further once we've found a change.
return false;
}
});
} else {
changed = true;
}
if (!changed) {
CodeMirror.commands.indentMore(instance);
}
};
/**
* @private
* Handle Tab key press.
*/
Editor.prototype._handleTabKey = function () {
// Tab key handling is done as follows:
// 1. If any of the selections are multiline, just add one indent level to the
// beginning of all lines that intersect any selection.
// 2. Otherwise, if any of the selections is a cursor or single-line range that
// ends at or after the first non-whitespace character in a line:
// - if indentation is set to tabs, just insert a hard tab before each selection.
// - if indentation is set to spaces, insert the appropriate number of spaces before
// each selection to get to its next soft tab stop.
// 3. Otherwise (all selections are cursors or single-line, and are in the whitespace
// before their respective lines), try to autoindent each line based on the mode.
// If none of the cursors moved and no space was added, then add one indent level
// to the beginning of all lines.
// Note that in case 2, we do the "dumb" insertion even if the cursor is immediately
// before the first non-whitespace character in a line. It might seem more convenient
// to do autoindent in that case. However, the problem is if that line is already
// indented past its "proper" location. In that case, we don't want Tab to
// *outdent* the line. If we had more control over the autoindent algorithm or
// implemented it ourselves, we could handle that case separately.
var instance = this._codeMirror,
selectionType = "indentAuto",
selections = this.getSelections();
_.each(selections, function (sel) {
if (sel.start.line !== sel.end.line) {
// Case 1 - we found a multiline selection. We can bail as soon as we find one of these.
selectionType = "indentAtBeginning";
return false;
} else if (sel.end.ch > 0 && sel.end.ch >= instance.getLine(sel.end.line).search(/\S/)) {
// Case 2 - we found a selection that ends at or after the first non-whitespace
// character on the line. We need to keep looking in case we find a later multiline
// selection though.
selectionType = "indentAtSelection";
}
});
switch (selectionType) {
case "indentAtBeginning":
// Case 1
CodeMirror.commands.indentMore(instance);
break;
case "indentAtSelection":
// Case 2
this._addIndentAtEachSelection(selections);
break;
case "indentAuto":
// Case 3
this._autoIndentEachSelection(selections);
break;
}
};
/**
* @private
* Handle left arrow, right arrow, backspace and delete keys when soft tabs are used.
* @param {number} direction Direction of movement: 1 for forward, -1 for backward
* @param {string} functionName name of the CodeMirror function to call if we handle the key
*/
Editor.prototype._handleSoftTabNavigation = function (direction, functionName) {
var instance = this._codeMirror,
overallJump = null;
if (!instance.getOption("indentWithTabs") && PreferencesManager.get(SOFT_TABS)) {
var indentUnit = instance.getOption("indentUnit");
_.each(this.getSelections(), function (sel) {
if (CodeMirror.cmpPos(sel.start, sel.end) !== 0) {
// This is a range - it will just collapse/be deleted regardless of the jump we set, so
// we can just ignore it and continue. (We don't want to return false in this case since
// we want to keep looking at other ranges.)
return;
}
var cursor = sel.start,
jump = (indentUnit === 0) ? 1 : cursor.ch % indentUnit,
line = instance.getLine(cursor.line);
// Don't do any soft tab handling if there are non-whitespace characters before the cursor in
// any of the selections.
if (line.substr(0, cursor.ch).search(/\S/) !== -1) {
jump = null;
} else if (direction === 1) { // right
if (indentUnit) {
jump = indentUnit - jump;
}
// Don't jump if it would take us past the end of the line, or if there are
// non-whitespace characters within the jump distance.
if (cursor.ch + jump > line.length || line.substr(cursor.ch, jump).search(/\S/) !== -1) {
jump = null;
}
} else { // left
// If we are on the tab boundary, jump by the full amount,
// but not beyond the start of the line.
if (jump === 0) {
jump = indentUnit;
}
if (cursor.ch - jump < 0) {
jump = null;
} else {
// We're moving left, so negate the jump.
jump = -jump;
}
}
// Did we calculate a jump, and is this jump value either the first one or
// consistent with all the other jumps? If so, we're good. Otherwise, bail
// out of the foreach, since as soon as we hit an inconsistent jump we don't
// have to look any further.
if (jump !== null &&
(overallJump === null || overallJump === jump)) {
overallJump = jump;
} else {
overallJump = null;
return false;
}
});
}
if (overallJump === null) {
// Just do the default move, which is one char in the given direction.
overallJump = direction;
}
instance[functionName](overallJump, "char");
};
/**
* Determine the mode to use from the document's language
* Uses "text/plain" if the language does not define a mode
* @return {string} The mode to use
*/
Editor.prototype._getModeFromDocument = function () {
// We'd like undefined/null/"" to mean plain text mode. CodeMirror defaults to plaintext for any
// unrecognized mode, but it complains on the console in that fallback case: so, convert
// here so we're always explicit, avoiding console noise.
return this.document.getLanguage().getMode() || "text/plain";
};
/**
* Selects all text and maintains the current scroll position.
*/
Editor.prototype.selectAllNoScroll = function () {
var cm = this._codeMirror,
info = this._codeMirror.getScrollInfo();
// Note that we do not have to check for the visible range here. This
// concern is handled internally by code mirror.
cm.operation(function () {
cm.scrollTo(info.left, info.top);
cm.execCommand("selectAll");
});
};
/**
* @return {boolean} True if editor is not showing the entire text of the document (i.e. an inline editor)
*/
Editor.prototype.isTextSubset = function () {
return Boolean(this._visibleRange);
};
/**
* Ensures that the lines that are actually hidden in the inline editor correspond to
* the desired visible range.
*/
Editor.prototype._updateHiddenLines = function () {
if (this._visibleRange) {
var cm = this._codeMirror,
self = this;
cm.operation(function () {
self._hideMarks.forEach(function (mark) {
if (mark) {
mark.clear();
}
});
self._hideMarks = [];
self._hideMarks.push(self._hideLines(0, self._visibleRange.startLine));
self._hideMarks.push(self._hideLines(self._visibleRange.endLine + 1, self.lineCount()));
});
}
};
Editor.prototype._applyChanges = function (changeList) {
// _visibleRange has already updated via its own Document listener. See if this change caused
// it to lose sync. If so, our whole view is stale - signal our owner to close us.
if (this._visibleRange) {
if (this._visibleRange.startLine === null || this._visibleRange.endLine === null) {
this.trigger("lostContent");
return;
}
}
// Apply text changes to CodeMirror editor
var cm = this._codeMirror;
cm.operation(function () {
var change, newText, i;
for (i = 0; i < changeList.length; i++) {
change = changeList[i];
newText = change.text.join('\n');
if (!change.from || !change.to) {
if (change.from || change.to) {
console.error("Change record received with only one end undefined--replacing entire text");
}
cm.setValue(newText);
} else {
cm.replaceRange(newText, change.from, change.to, change.origin);
}
}
});
// The update above may have inserted new lines - must hide any that fall outside our range
this._updateHiddenLines();
};
/**
* Responds to changes in the CodeMirror editor's text, syncing the changes to the Document.
* There are several cases where we want to ignore a CodeMirror change:
* - if we're the master editor, editor changes can be ignored because Document is already listening
* for our changes
* - if we're a secondary editor, editor changes should be ignored if they were caused by us reacting
* to a Document change
*/
Editor.prototype._handleEditorChange = function (changeList) {
// we're currently syncing from the Document, so don't echo back TO the Document
if (this._duringSync) {
return;
}
// Secondary editor: force creation of "master" editor backing the model, if doesn't exist yet
this.document._ensureMasterEditor();
if (this.document._masterEditor !== this) {
// Secondary editor:
// we're not the ground truth; if we got here, this was a real editor change (not a
// sync from the real ground truth), so we need to sync from us into the document
// (which will directly push the change into the master editor).
// FUTURE: Technically we should add a replaceRange() method to Document and go through
// that instead of talking to its master editor directly. It's not clear yet exactly
// what the right Document API would be, though.
this._duringSync = true;
this.document._masterEditor._applyChanges(changeList);
this._duringSync = false;
// Update which lines are hidden inside our editor, since we're not going to go through
// _applyChanges() in our own editor.
this._updateHiddenLines();
}
// Else, Master editor:
// we're the ground truth; nothing else to do, since Document listens directly to us
// note: this change might have been a real edit made by the user, OR this might have
// been a change synced from another editor
// The "editorChange" event is mostly for the use of the CodeHintManager.
// It differs from the normal "change" event, that it's actually publicly usable,
// whereas the "change" event should be listened to on the document. Also the
// Editor dispatches a change event before this event is dispatched, because
// CodeHintManager needs to hook in here when other things are already done.
this.trigger("editorChange", this, changeList);
};
/**
* Responds to changes in the Document's text, syncing the changes into our CodeMirror instance.
* There are several cases where we want to ignore a Document change:
* - if we're the master editor, Document changes should be ignored because we already have the right
* text (either the change originated with us, or it has already been set into us by Document)
* - if we're a secondary editor, Document changes should be ignored if they were caused by us sending
* the document an editor change that originated with us
*/
Editor.prototype._handleDocumentChange = function (event, doc, changeList) {
// we're currently syncing to the Document, so don't echo back FROM the Document
if (this._duringSync) {
return;
}
if (this.document._masterEditor !== this) {
// Secondary editor:
// we're not the ground truth; and if we got here, this was a Document change that
// didn't come from us (e.g. a sync from another editor, a direct programmatic change
// to the document, or a sync from external disk changes)... so sync from the Document
this._duringSync = true;
this._applyChanges(changeList);
this._duringSync = false;
}
// Else, Master editor:
// we're the ground truth; nothing to do since Document change is just echoing our
// editor changes
};
/**
* Responds to the Document's underlying file being deleted. The Document is now basically dead,
* so we must close.
*/
Editor.prototype._handleDocumentDeleted = function (event) {
// Pass the delete event along as the cause (needed in MultiRangeInlineEditor)
this.trigger("lostContent", event);
};
/**
* Responds to language changes, for instance when the file extension is changed.
*/
Editor.prototype._handleDocumentLanguageChanged = function (event) {
this._codeMirror.setOption("mode", this._getModeFromDocument());
};
/**
* Install event handlers on the CodeMirror instance, translating them into
* jQuery events on the Editor instance.
*/
Editor.prototype._installEditorListeners = function () {
var self = this;
// Redispatch these CodeMirror key events as Editor events
function _onKeyEvent(instance, event) {
self.trigger("keyEvent", self, event); // deprecated
self.trigger(event.type, self, event);
return event.defaultPrevented; // false tells CodeMirror we didn't eat the event
}
this._codeMirror.on("keydown", _onKeyEvent);
this._codeMirror.on("keypress", _onKeyEvent);
this._codeMirror.on("keyup", _onKeyEvent);
// FUTURE: if this list grows longer, consider making this a more generic mapping
// NOTE: change is a "private" event--others shouldn't listen to it on Editor, only on
// Document