-
-
Notifications
You must be signed in to change notification settings - Fork 193
Expand file tree
/
Copy pathEditor.js
More file actions
3204 lines (2872 loc) · 137 KB
/
Editor.js
File metadata and controls
3204 lines (2872 loc) · 137 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
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
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
* Original work Copyright (c) 2012 - 2021 Adobe Systems Incorporated. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/
// @INCLUDE_IN_API_DOCS
/**
* 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 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: (available as `Editor.EVENT_*` constants. see below)
* - 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".
* - change - Triggered with an array of change objects. Parameters: (editor, changeList)
* - beforeChange - (self, changeObj)
* - beforeSelectionChange - (selectionObj)
* - focus - Fired when an editor is focused
* - blur - Fired when an editor loses focused
* - update - Will be fired whenever Editor updates its DOM display.
* - 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.
* - viewportChange - (from: number, to: number) Fires whenever the view port of the editor changes
* (due to scrolling, editing, or any other factor). The from and to arguments give the new start
* and end of the viewport. This is combination with `editorInstance.getViewPort()` can be used to
* selectively redraw visual elements in code like syntax analyze only parts of code instead
* of the full code everytime.
* - 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) {
let CommandManager = require("command/CommandManager"),
Commands = require("command/Commands"),
CodeMirror = require("thirdparty/CodeMirror/lib/codemirror"),
LanguageManager = require("language/LanguageManager"),
EventDispatcher = require("utils/EventDispatcher"),
PerfUtils = require("utils/PerfUtils"),
PreferencesManager = require("preferences/PreferencesManager"),
StateManager = require("preferences/StateManager"),
TextRange = require("document/TextRange").TextRange,
TokenUtils = require("utils/TokenUtils"),
HTMLUtils = require("language/HTMLUtils"),
MainViewManager = require("view/MainViewManager"),
Metrics = require("utils/Metrics"),
_ = require("thirdparty/lodash");
const tabSpacesStateManager = StateManager._createInternalStateManager(StateManager._INTERNAL_STATES.TAB_SPACES);
/* Editor helpers */
let IndentHelper = require("./EditorHelper/IndentHelper"),
EditorPreferences = require("./EditorHelper/EditorPreferences"),
ChangeHelper = require("./EditorHelper/ChangeHelper"),
ErrorPopupHelper = require("./EditorHelper/ErrorPopupHelper"),
InlineWidgetHelper = require("./EditorHelper/InlineWidgetHelper");
/* Editor preferences */
/**
* A list of gutter name and priorities currently registered for editors.
* The line number gutter is defined as \{ name: LINE_NUMBER_GUTTER, priority: 100 }
* @private
* @type {Array<Object>} items - An array of objects, where each object contains the following properties:
* @property {string} name - The name of the item.
* @property {number} priority - The priority of the item.
* @property {Array} languageIds - An array of language IDs.
*/
let registeredGutters = [];
let cmOptions = {};
EditorPreferences.init(cmOptions);
const CLOSE_BRACKETS = EditorPreferences.CLOSE_BRACKETS,
CLOSE_TAGS = EditorPreferences.CLOSE_TAGS,
DRAG_DROP = EditorPreferences.DRAG_DROP,
HIGHLIGHT_MATCHES = EditorPreferences.HIGHLIGHT_MATCHES,
LINEWISE_COPY_CUT = EditorPreferences.LINEWISE_COPY_CUT,
SCROLL_PAST_END = EditorPreferences.SCROLL_PAST_END,
SHOW_CURSOR_SELECT = EditorPreferences.SHOW_CURSOR_SELECT,
SHOW_LINE_NUMBERS = EditorPreferences.SHOW_LINE_NUMBERS,
SMART_INDENT = EditorPreferences.SMART_INDENT,
SPACE_UNITS = EditorPreferences.SPACE_UNITS,
STYLE_ACTIVE_LINE = EditorPreferences.STYLE_ACTIVE_LINE,
TAB_SIZE = EditorPreferences.TAB_SIZE,
AUTO_TAB_SPACES = EditorPreferences.AUTO_TAB_SPACES,
USE_TAB_CHAR = EditorPreferences.USE_TAB_CHAR,
WORD_WRAP = EditorPreferences.WORD_WRAP,
INDENT_LINE_COMMENT = EditorPreferences.INDENT_LINE_COMMENT,
INPUT_STYLE = EditorPreferences.INPUT_STYLE;
const LINE_NUMBER_GUTTER = EditorPreferences.LINE_NUMBER_GUTTER,
LINE_NUMBER_GUTTER_PRIORITY = EditorPreferences.LINE_NUMBER_GUTTER_PRIORITY,
CODE_FOLDING_GUTTER_PRIORITY = EditorPreferences.CODE_FOLDING_GUTTER_PRIORITY,
MOUSE_WHEEL_SCROLL_SENSITIVITY = EditorPreferences.MOUSE_WHEEL_SCROLL_SENSITIVITY;
let editorOptions = [...Object.keys(cmOptions), AUTO_TAB_SPACES];
/* Editor preferences */
/**
* Guard flag to prevent focus() reentrancy (via blur handlers), even across Editors
* @private
* @type {boolean}
*/
var _duringFocus = false;
/**
* Cached mouse wheel scroll sensitivity value from preferences
* @private
* @type {number}
*/
let _mouseWheelScrollSensitivity = 1;
/**
* Constant: Normal boundary check when centering text.
* @type {number}
*/
const BOUNDARY_CHECK_NORMAL = 0;
/**
* Constant: Ignore the upper boundary when centering text.
* @type {number}
*/
const BOUNDARY_IGNORE_TOP = 1;
/**
* Constant: Bulls-eye mode, strictly center the text always.
* @type {number}
*/
const BOUNDARY_BULLSEYE = 2;
/**
* @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.
* @private
* @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.
* @private
* @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.
* @private
* @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) || !document.editable;
_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);
}
let $container = $(container);
$container.addClass("editor-holder");
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._markTypesMap = {};
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.canConsumeEscapeKeyEvent()) {
return;
}
if (self.getSelections().length > 1) { // multi cursor
self.clearSelection();
} else if (self.hasSelection()) {
self.clearSelection();
} 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);
})
);
//cm: CodeMirror, repeat: "single" | "double" | "triple", event: Event
// The function is called when the left mouse button is pressed in codemirror
function _mouseHandlerOverride(_cm, _repeat, event) {
if (event.ctrlKey || event.metaKey) {
setTimeout(() => {
CommandManager.execute(Commands.NAVIGATE_JUMPTO_DEFINITION);
Metrics.countEvent(Metrics.EVENT_TYPE.EDITOR, "ctrlClick", _cm.getMode().name);
}, 100);
}
return {
addNew: event.altKey // alt key will init multi cursor instead of ctrl-key
};
}
// 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,
continueComments: true,
cursorScrollMargin: 3,
dragDrop: currentOptions[DRAG_DROP],
electricChars: true,
configureMouse: _mouseHandlerOverride,
extraKeys: codeMirrorKeyMap,
highlightSelectionMatches: currentOptions[HIGHLIGHT_MATCHES],
indentUnit: currentOptions[USE_TAB_CHAR] ? currentOptions[TAB_SIZE] : currentOptions[SPACE_UNITS],
indentWithTabs: currentOptions[USE_TAB_CHAR],
inputStyle: currentOptions[INPUT_STYLE],
lineNumbers: currentOptions[SHOW_LINE_NUMBERS],
lineWiseCopyCut: currentOptions[LINEWISE_COPY_CUT],
lineWrapping: currentOptions[WORD_WRAP],
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
});
// Override default drag image in Safari (and harmless in others):
// Safari shows a text image by default when dragging from CodeMirror,
// which can be visually distracting. Use a 1x1 transparent image instead.
// Note: The CodeMirror "wrapper" element (returned by getWrapperElement()) is NOT the same
// as the "container" we pass to new CodeMirror(container, ...). CodeMirror creates its own
// wrapper inside that container. We attach the drag listener to the wrapper so it captures
// drags originating from the editor surface reliably across browsers.
try {
const wrapperEl = self._codeMirror.getWrapperElement();
if (wrapperEl) {
// Create once per editor instance
const transparentImg = new Image();
transparentImg.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
function handleCMDragStart(e) {
if (e && e.dataTransfer && typeof e.dataTransfer.setDragImage === "function") {
e.dataTransfer.setDragImage(transparentImg, 0, 0);
}
}
wrapperEl.addEventListener("dragstart", handleCMDragStart);
// No explicit removal necessary since the wrapper element is removed on editor.destroy().
}
} catch (err) {
// Fail silently; drag image override is non-critical.
}
// 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._renderGutters();
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());
}
});
const $cmElement = this.$el;
$cmElement[0].addEventListener("wheel", (event) => {
const $editor = $cmElement.find(".CodeMirror-scroll");
// We need to scale the scroll by the factor of line height. This became a problem after we added
// the custom line height feature causing jumping scrolls esp in safari and mac if we dont do
// this scroll scaling.
const lineHeight = parseFloat(getComputedStyle($editor[0]).lineHeight);
const defaultHeight = 14, scrollScaleFactor = lineHeight / defaultHeight;
// when user is pressing the 'Shift' key, we need to convert the vertical scroll to horizontal scroll
if (event.shiftKey) {
let horizontalDelta = event.deltaX;
if (event.deltaY !== 0) {
horizontalDelta = event.deltaY;
}
// apply the horizontal scrolling
if (horizontalDelta !== 0) {
$editor[0].scrollLeft += horizontalDelta;
event.preventDefault();
return;
}
}
// apply horizontal scrolling if present. for the diagonal scrolling
if (event.deltaX !== 0) {
$editor[0].scrollLeft += event.deltaX;
}
// apply the vertical scrolling normally
if (event.deltaY !== 0) {
let scrollAmount;
if (event.deltaMode === 0) {
// Pixel mode - browser reports delta in pixels, system scroll settings already applied.
// Scale by line height factor to normalize for custom line heights.
scrollAmount = event.deltaY / scrollScaleFactor;
} else if (event.deltaMode === 1) {
// Line mode - delta is in lines, convert to pixels using actual line height
scrollAmount = event.deltaY * defaultHeight;
} else {
// Page mode - delta is in pages, convert to viewport height
scrollAmount = event.deltaY * $editor[0].clientHeight;
}
$editor[0].scrollTop += scrollAmount * _mouseWheelScrollSensitivity;
event.preventDefault();
}
});
}
EventDispatcher.makeEventDispatcher(Editor.prototype);
EventDispatcher.markDeprecated(Editor.prototype, "keyEvent", "'keydown/press/up'");
IndentHelper.addHelpers(Editor);
ChangeHelper.addHelpers(Editor);
InlineWidgetHelper.addHelpers(Editor);
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);
};
/**
* Gets the inline widgets below the current cursor position or null.
* @return {boolean}
*/
Editor.prototype.getInlineWidgetsBelowCursor = function () {
let self = this;
let cursor = self.getCursorPos();
let line = cursor.line;
return self.getAllInlineWidgetsForLine(line);
};
/**
* returns true if the editor can do something an escape key event. Eg. Disable multi cursor escape
*/
Editor.prototype.canConsumeEscapeKeyEvent = function () {
let self = this;
return (self.getSelections().length > 1) // multi cursor should go away on escape
|| (self.hasSelection()) // selection should go away on escape
|| self.getInlineWidgetsBelowCursor() // inline widget is below cursor
|| self.getFocusedInlineWidget(); // inline widget
};
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();
}
};
/**
* Determine the mode to use from the document's language
* Uses "text/plain" if the language does not define a mode
* @private
* @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.
* @private
*/
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()));
});
}
};
/**
* Sets the contents of the editor, clears the undo/redo history and marks the document clean. Dispatches a change event.
* Semi-private: only Document should call this.
* @private
* @param {!string} text
*/
Editor.prototype._resetText = function (text) {
var currentText = this._codeMirror.getValue();
// compare with ignoring line-endings, issue #11826
var textLF = text ? text.replace(/(\r\n|\r|\n)/g, "\n") : null;
var currentTextLF = currentText ? currentText.replace(/(\r\n|\r|\n)/g, "\n") : null;
if (textLF === currentTextLF) {
// there's nothing to reset
return;
}
var perfTimerName = PerfUtils.markStart("Editor._resetText()\t" + (!this.document || this.document.file.fullPath));
var cursorPos = this.getCursorPos(),
scrollPos = this.getScrollPos();
// This *will* fire a change event, but we clear the undo immediately afterward
this._codeMirror.setValue(text);
this._codeMirror.refresh();
// Make sure we can't undo back to the empty state before setValue(), and mark
// the document clean.
this._codeMirror.clearHistory();
this._codeMirror.markClean();
// restore cursor and scroll positions
this.setCursorPos(cursorPos);
this.setScrollPos(scrollPos.x, scrollPos.y);
PerfUtils.addMeasurement(perfTimerName);
};
/**
* Gets the file associated with this editor
* This is a required Pane-View interface method
* @return {!File} the file associated with this editor
*/
Editor.prototype.getFile = function () {
return this.document.file;
};
/**
* Gets the current cursor position within the editor.
*
* Cursor positions can be converted to index(0 based character offsets in editor text string)
* using `editor.indexFromPos` API.
* @param {boolean} [expandTabs] If true, return the actual visual column number instead of the character offset in
* the "ch" property.
* @param {string} [which] Optional string indicating which end of the
* selection to return. It may be "start", "end", "head" (the side of the
* selection that moves when you press shift+arrow), or "anchor" (the
* fixed side of the selection). Omitting the argument is the same as
* passing "head". A {'line', 'ch'} object will be returned.)
* @return {{line:number, ch:number}}
*/
Editor.prototype.getCursorPos = function (expandTabs, which) {
// Translate "start" and "end" to the official CM names (it actually
// supports them as-is, but that isn't documented and we don't want to
// rely on it).
if (which === "start") {
which = "from";
} else if (which === "end") {
which = "to";
}
var cursor = _copyPos(this._codeMirror.getCursor(which));
if (expandTabs) {
cursor.ch = this.getColOffset(cursor);
}
return cursor;
};
/**
* Gets the cursor position of the last charected in the editor.
* @param {boolean} [expandTabs] If true, return the actual visual column number instead of the character offset in
* the "ch" property.
* @return {{line:number, ch:number}}
*/
Editor.prototype.getEndingCursorPos = function (expandTabs) {
let lastLine = this._codeMirror.lastLine();
let cursor = {
line: lastLine,
ch: this._codeMirror.getLine(lastLine).length
};
if (expandTabs) {
cursor.ch = this.getColOffset(cursor);
}
return cursor;
};
/**
* Returns the display column (zero-based) for a given string-based pos. Differs from pos.ch only
* when the line contains preceding \t chars. Result depends on the current tab size setting.
* @param {!{line:number, ch:number}} pos
* @return {number}
*/
Editor.prototype.getColOffset = function (pos) {
var line = this._codeMirror.getRange({ line: pos.line, ch: 0 }, pos),
tabSize = null,
column = 0,
i;
for (i = 0; i < line.length; i++) {
if (line[i] === '\t') {
if (tabSize === null) {
tabSize = Editor.getTabSize();
}
if (tabSize > 0) {
column += (tabSize - (column % tabSize));
}
} else {
column++;
}
}
return column;
};
/**
* Returns the string-based pos for a given display column (zero-based) in given line. Differs from column
* only when the line contains preceding \t chars. Result depends on the current tab size setting.
* @param {number} lineNum Line number
* @param {number} column Display column number
* @return {number}
*/
Editor.prototype.getCharIndexForColumn = function (lineNum, column) {
var line = this._codeMirror.getLine(lineNum),
tabSize = null,
iCol = 0,
i;
for (i = 0; iCol < column; i++) {
if (line[i] === '\t') {
if (tabSize === null) {
tabSize = Editor.getTabSize();
}
if (tabSize > 0) {
iCol += (tabSize - (iCol % tabSize));
}
} else {
iCol++;
}
}
return i;
};
/**
* Sets the cursor position within the editor. Removes any selection.
* @param {number} line The 0 based line number.
* @param {number} ch The 0 based character position; treated as 0 if unspecified.
* @param {boolean=} center True if the view should be centered on the new cursor position.
* @param {boolean=} expandTabs If true, use the actual visual column number instead of the character offset as
* the "ch" parameter.
*/
Editor.prototype.setCursorPos = function (line, ch, center, expandTabs) {
if (expandTabs) {
ch = this.getColOffset({ line: line, ch: ch });
}
this._codeMirror.setCursor(line, ch);
if (center) {
this.centerOnCursor();
}
};
/**
* Set the editor size in pixels or percentage
* @param {(number|string)} width
* @param {(number|string)} height
*/
Editor.prototype.setSize = function (width, height) {
this._codeMirror.setSize(width, height);
};
/**
* Returns a {'from', 'to'} object indicating the start (inclusive) and end (exclusive) of the currently rendered
* part of the document. In big documents, when most content is scrolled out of view, Editor will only render
* the visible part, and a margin around it. See also the `viewportChange` event fired on the editor.
*
* This is combination with `viewportChange` event can be used to selectively redraw visual elements in code
* like syntax analyze only parts of code instead of the full code everytime.
* @return {{from: number, to: number}}
*/
Editor.prototype.getViewport = function () {
return this._codeMirror.getViewport();
};
/** @const */
var CENTERING_MARGIN = 0.15;
/**
* Scrolls the editor viewport to vertically center the line with the cursor,
* but only if the cursor is currently near the edges of the viewport or
* entirely outside the viewport.
*
* This does not alter the horizontal scroll position.
*
* @param {number} centerOptions Option value, or 0 for no options; one of the BOUNDARY_* constants above.
*/
Editor.prototype.centerOnCursor = function (centerOptions) {
let $scrollerElement = $(this.getScrollerElement());
let editorHeight = $scrollerElement.height();
// we need to make adjustments for the statusbar's padding on the bottom and the menu bar on top.
let statusBarHeight = $("#status-bar").height();
let documentCursorPosition = this._codeMirror.cursorCoords(null, "local").bottom;
let screenCursorPosition = this._codeMirror.cursorCoords(null, "page").bottom;
if (centerOptions === BOUNDARY_BULLSEYE) {
let pos = documentCursorPosition - editorHeight / 2 + statusBarHeight;
this.setScrollPos(null, pos);
return;
}
// If the cursor is already reasonably centered, we won't
// make any change. "Reasonably centered" is defined as
// not being within CENTERING_MARGIN of the top or bottom
// of the editor (where CENTERING_MARGIN is a percentage
// of the editor height).
// For finding the first item (i.e. find while typing), do
// not center if hit is in first half of screen because this
// appears to be an unnecesary scroll.
if ((_checkTopBoundary(centerOptions) && (screenCursorPosition < editorHeight * CENTERING_MARGIN)) ||
(_checkBottomBoundary(centerOptions) && (screenCursorPosition > editorHeight * (1 - CENTERING_MARGIN)))) {
var pos = documentCursorPosition - editorHeight / 2 + statusBarHeight;
var info = this._codeMirror.getScrollInfo();
pos = Math.min(Math.max(pos, 0), (info.height - info.clientHeight));
this.setScrollPos(null, pos);
}
};
/**
* Given a position, returns its index within the text (assuming \n newlines)
* @param {{line:number, ch:number}} cursorPos
* @return {number}
*/
Editor.prototype.indexFromPos = function (cursorPos) {
return this._codeMirror.indexFromPos(cursorPos);
};
/**
* Given a position, returns its index within the text (assuming \n newlines)
* @param {number} index
* @return {{line:number, ch:number}}
*/
Editor.prototype.posFromIndex = function (index) {
return this._codeMirror.posFromIndex(index);
};
/**
* Returns true if pos is between start and end (INclusive at start; EXclusive at end by default,
* but overridable via the endInclusive flag).
* @param {{line:number, ch:number}} pos
* @param {{line:number, ch:number}} start
* @param {{line:number, ch:number}} end
* @param {boolean} endInclusive
*
*/
Editor.prototype.posWithinRange = function (pos, start, end, endInclusive) {
if (start.line <= pos.line && end.line >= pos.line) {
if (endInclusive) {
return (start.line < pos.line || start.ch <= pos.ch) && // inclusive
(end.line > pos.line || end.ch >= pos.ch); // inclusive
}
return (start.line < pos.line || start.ch <= pos.ch) && // inclusive
(end.line > pos.line || end.ch > pos.ch); // exclusive
}
return false;
};
/**
* @return {boolean} True if there's a text selection; false if there's just an insertion point
*/
Editor.prototype.hasSelection = function () {
return this._codeMirror.somethingSelected();
};
/**
* Takes an anchor/head pair and returns a start/end pair where the start is guaranteed to be <= end,