editor.ts 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import * as CodeMirror
  4. from 'codemirror';
  5. import {
  6. JSONExt
  7. } from '@phosphor/coreutils';
  8. import {
  9. ArrayExt
  10. } from '@phosphor/algorithm';
  11. import {
  12. IDisposable, DisposableDelegate
  13. } from '@phosphor/disposable';
  14. import {
  15. Signal
  16. } from '@phosphor/signaling';
  17. import {
  18. CodeEditor
  19. } from '@jupyterlab/codeeditor';
  20. import {
  21. IObservableMap, IObservableString, uuid, ICollaborator
  22. } from '@jupyterlab/coreutils';
  23. import {
  24. Mode
  25. } from './mode';
  26. import 'codemirror/addon/edit/matchbrackets.js';
  27. import 'codemirror/addon/edit/closebrackets.js';
  28. import 'codemirror/addon/comment/comment.js';
  29. import 'codemirror/keymap/emacs.js';
  30. import 'codemirror/keymap/sublime.js';
  31. import 'codemirror/keymap/vim.js';
  32. /**
  33. * The class name added to CodeMirrorWidget instances.
  34. */
  35. const EDITOR_CLASS = 'jp-CodeMirrorEditor';
  36. /**
  37. * The class name added to read only cell editor widgets.
  38. */
  39. const READ_ONLY_CLASS = 'jp-mod-readOnly';
  40. /**
  41. * The class name for the hover box for collaborator cursors.
  42. */
  43. const COLLABORATOR_CURSOR_CLASS = 'jp-CollaboratorCursor';
  44. /**
  45. * The class name for the hover box for collaborator cursors.
  46. */
  47. const COLLABORATOR_HOVER_CLASS = 'jp-CollaboratorCursor-hover';
  48. /**
  49. * The key code for the up arrow key.
  50. */
  51. const UP_ARROW = 38;
  52. /**
  53. * The key code for the down arrow key.
  54. */
  55. const DOWN_ARROW = 40;
  56. /**
  57. * The time that a collaborator name hover persists.
  58. */
  59. const HOVER_TIMEOUT = 1000;
  60. /**
  61. * CodeMirror editor.
  62. */
  63. export
  64. class CodeMirrorEditor implements CodeEditor.IEditor {
  65. /**
  66. * Construct a CodeMirror editor.
  67. */
  68. constructor(options: CodeMirrorEditor.IOptions) {
  69. let host = this.host = options.host;
  70. host.classList.add(EDITOR_CLASS);
  71. host.addEventListener('focus', this, true);
  72. host.addEventListener('scroll', this, true);
  73. this._uuid = options.uuid || uuid();
  74. this._selectionStyle = options.selectionStyle || {};
  75. let model = this._model = options.model;
  76. let editor = this._editor = CodeMirror(host, {});
  77. Private.handleConfig(editor, options.config || {});
  78. let doc = editor.getDoc();
  79. // Handle initial values for text, mimetype, and selections.
  80. doc.setValue(model.value.text);
  81. this._onMimeTypeChanged();
  82. this._onCursorActivity();
  83. // Connect to changes.
  84. model.value.changed.connect(this._onValueChanged, this);
  85. model.mimeTypeChanged.connect(this._onMimeTypeChanged, this);
  86. model.selections.changed.connect(this._onSelectionsChanged, this);
  87. CodeMirror.on(editor, 'keydown', (editor, event) => {
  88. let index = ArrayExt.findFirstIndex(this._keydownHandlers, handler => {
  89. if (handler(this, event) === true) {
  90. event.preventDefault();
  91. return true;
  92. }
  93. });
  94. if (index === -1) {
  95. this.onKeydown(event);
  96. }
  97. });
  98. CodeMirror.on(editor, 'cursorActivity', () => this._onCursorActivity());
  99. CodeMirror.on(editor.getDoc(), 'beforeChange', (instance, change) => {
  100. this._beforeDocChanged(instance, change);
  101. });
  102. CodeMirror.on(editor.getDoc(), 'change', (instance, change) => {
  103. // Manually refresh after setValue to make sure editor is properly sized.
  104. if (change.origin === 'setValue' && this.hasFocus()) {
  105. this.refresh();
  106. }
  107. if (this._model.value.text !== editor.getDoc().getValue()) {
  108. console.error('Uh oh, the string model is out of sync: ', {
  109. model: this._model.value.text,
  110. view: editor.getDoc().getValue()
  111. });
  112. }
  113. });
  114. // Manually refresh on paste to make sure editor is properly sized.
  115. editor.getWrapperElement().addEventListener('paste', () => {
  116. if (this.hasFocus()) {
  117. this.refresh();
  118. }
  119. });
  120. }
  121. /**
  122. * A signal emitted when either the top or bottom edge is requested.
  123. */
  124. readonly edgeRequested = new Signal<this, CodeEditor.EdgeLocation>(this);
  125. /**
  126. * The DOM node that hosts the editor.
  127. */
  128. readonly host: HTMLElement;
  129. /**
  130. * The uuid of this editor;
  131. */
  132. get uuid(): string {
  133. return this._uuid;
  134. }
  135. set uuid(value: string) {
  136. this._uuid = value;
  137. }
  138. /**
  139. * The selection style of this editor.
  140. */
  141. get selectionStyle(): CodeEditor.ISelectionStyle {
  142. return this._selectionStyle;
  143. }
  144. set selectionStyle(value: CodeEditor.ISelectionStyle) {
  145. this._selectionStyle = value;
  146. }
  147. /**
  148. * Get the codemirror editor wrapped by the editor.
  149. */
  150. get editor(): CodeMirror.Editor {
  151. return this._editor;
  152. }
  153. /**
  154. * Get the codemirror doc wrapped by the widget.
  155. */
  156. get doc(): CodeMirror.Doc {
  157. return this._editor.getDoc();
  158. }
  159. /**
  160. * Get the number of lines in the editor.
  161. */
  162. get lineCount(): number {
  163. return this.doc.lineCount();
  164. }
  165. /**
  166. * Returns a model for this editor.
  167. */
  168. get model(): CodeEditor.IModel {
  169. return this._model;
  170. }
  171. /**
  172. * The height of a line in the editor in pixels.
  173. */
  174. get lineHeight(): number {
  175. return this._editor.defaultTextHeight();
  176. }
  177. /**
  178. * The widget of a character in the editor in pixels.
  179. */
  180. get charWidth(): number {
  181. return this._editor.defaultCharWidth();
  182. }
  183. /**
  184. * Tests whether the editor is disposed.
  185. */
  186. get isDisposed(): boolean {
  187. return this._editor === null;
  188. }
  189. /**
  190. * Dispose of the resources held by the widget.
  191. */
  192. dispose(): void {
  193. if (this._editor === null) {
  194. return;
  195. }
  196. this.host.removeEventListener('focus', this, true);
  197. this.host.removeEventListener('scroll', this, true);
  198. this._editor = null;
  199. this._model = null;
  200. this._keydownHandlers.length = 0;
  201. Signal.clearData(this);
  202. }
  203. /**
  204. * Get a config option for the editor.
  205. */
  206. getOption<K extends keyof CodeMirrorEditor.IConfig>(option: K): CodeMirrorEditor.IConfig[K] {
  207. return Private.getOption(this.editor, option);
  208. }
  209. /**
  210. * Set a config option for the editor.
  211. */
  212. setOption<K extends keyof CodeMirrorEditor.IConfig>(option: K, value: CodeMirrorEditor.IConfig[K]): void {
  213. Private.setOption(this.editor, option, value);
  214. }
  215. /**
  216. * Returns the content for the given line number.
  217. */
  218. getLine(line: number): string | undefined {
  219. return this.doc.getLine(line);
  220. }
  221. /**
  222. * Find an offset for the given position.
  223. */
  224. getOffsetAt(position: CodeEditor.IPosition): number {
  225. return this.doc.indexFromPos({
  226. ch: position.column,
  227. line: position.line
  228. });
  229. }
  230. /**
  231. * Find a position fot the given offset.
  232. */
  233. getPositionAt(offset: number): CodeEditor.IPosition {
  234. const { ch, line } = this.doc.posFromIndex(offset);
  235. return { line, column: ch };
  236. }
  237. /**
  238. * Undo one edit (if any undo events are stored).
  239. */
  240. undo(): void {
  241. this.doc.undo();
  242. }
  243. /**
  244. * Redo one undone edit.
  245. */
  246. redo(): void {
  247. this.doc.redo();
  248. }
  249. /**
  250. * Clear the undo history.
  251. */
  252. clearHistory(): void {
  253. this.doc.clearHistory();
  254. }
  255. /**
  256. * Brings browser focus to this editor text.
  257. */
  258. focus(): void {
  259. this._editor.focus();
  260. }
  261. /**
  262. * Test whether the editor has keyboard focus.
  263. */
  264. hasFocus(): boolean {
  265. return this._editor.hasFocus();
  266. }
  267. /**
  268. * Explicitly blur the editor.
  269. */
  270. blur(): void {
  271. this._editor.getInputField().blur();
  272. }
  273. /**
  274. * Repaint editor.
  275. */
  276. refresh(): void {
  277. this._editor.refresh();
  278. this._needsRefresh = false;
  279. }
  280. /**
  281. * Refresh the editor if it is focused;
  282. * otherwise postpone refreshing till focusing.
  283. */
  284. resizeToFit(): void {
  285. if (this.hasFocus()) {
  286. this.refresh();
  287. } else {
  288. this._needsRefresh = true;
  289. }
  290. this._clearHover();
  291. }
  292. /**
  293. * Add a keydown handler to the editor.
  294. *
  295. * @param handler - A keydown handler.
  296. *
  297. * @returns A disposable that can be used to remove the handler.
  298. */
  299. addKeydownHandler(handler: CodeEditor.KeydownHandler): IDisposable {
  300. this._keydownHandlers.push(handler);
  301. return new DisposableDelegate(() => {
  302. ArrayExt.removeAllWhere(this._keydownHandlers, val => val === handler);
  303. });
  304. }
  305. /**
  306. * Set the size of the editor in pixels.
  307. */
  308. setSize(dimension: CodeEditor.IDimension | null): void {
  309. if (dimension) {
  310. this._editor.setSize(dimension.width, dimension.height);
  311. } else {
  312. this._editor.setSize(null, null);
  313. }
  314. this._needsRefresh = false;
  315. }
  316. /**
  317. * Reveal the given position in the editor.
  318. */
  319. revealPosition(position: CodeEditor.IPosition): void {
  320. const cmPosition = this._toCodeMirrorPosition(position);
  321. this._editor.scrollIntoView(cmPosition);
  322. }
  323. /**
  324. * Reveal the given selection in the editor.
  325. */
  326. revealSelection(selection: CodeEditor.IRange): void {
  327. const range = this._toCodeMirrorRange(selection);
  328. this._editor.scrollIntoView(range);
  329. }
  330. /**
  331. * Get the window coordinates given a cursor position.
  332. */
  333. getCoordinateForPosition(position: CodeEditor.IPosition): CodeEditor.ICoordinate {
  334. const pos = this._toCodeMirrorPosition(position);
  335. const rect = this.editor.charCoords(pos, 'page');
  336. return rect as CodeEditor.ICoordinate;
  337. }
  338. /**
  339. * Get the cursor position given window coordinates.
  340. *
  341. * @param coordinate - The desired coordinate.
  342. *
  343. * @returns The position of the coordinates, or null if not
  344. * contained in the editor.
  345. */
  346. getPositionForCoordinate(coordinate: CodeEditor.ICoordinate): CodeEditor.IPosition | null {
  347. return this._toPosition(this.editor.coordsChar(coordinate)) || null;
  348. }
  349. /**
  350. * Returns the primary position of the cursor, never `null`.
  351. */
  352. getCursorPosition(): CodeEditor.IPosition {
  353. const cursor = this.doc.getCursor();
  354. return this._toPosition(cursor);
  355. }
  356. /**
  357. * Set the primary position of the cursor.
  358. *
  359. * #### Notes
  360. * This will remove any secondary cursors.
  361. */
  362. setCursorPosition(position: CodeEditor.IPosition): void {
  363. const cursor = this._toCodeMirrorPosition(position);
  364. this.doc.setCursor(cursor);
  365. // If the editor does not have focus, this cursor change
  366. // will get screened out in _onCursorsChanged(). Make an
  367. // exception for this method.
  368. if (!this.editor.hasFocus()) {
  369. this.model.selections.set(this.uuid, this.getSelections());
  370. }
  371. }
  372. /**
  373. * Returns the primary selection, never `null`.
  374. */
  375. getSelection(): CodeEditor.ITextSelection {
  376. return this.getSelections()[0];
  377. }
  378. /**
  379. * Set the primary selection. This will remove any secondary cursors.
  380. */
  381. setSelection(selection: CodeEditor.IRange): void {
  382. this.setSelections([selection]);
  383. }
  384. /**
  385. * Gets the selections for all the cursors, never `null` or empty.
  386. */
  387. getSelections(): CodeEditor.ITextSelection[] {
  388. const selections = this.doc.listSelections();
  389. if (selections.length > 0) {
  390. return selections.map(selection => this._toSelection(selection));
  391. }
  392. const cursor = this.doc.getCursor();
  393. const selection = this._toSelection({ anchor: cursor, head: cursor });
  394. return [selection];
  395. }
  396. /**
  397. * Sets the selections for all the cursors, should not be empty.
  398. * Cursors will be removed or added, as necessary.
  399. * Passing an empty array resets a cursor position to the start of a document.
  400. */
  401. setSelections(selections: CodeEditor.IRange[]): void {
  402. const cmSelections = this._toCodeMirrorSelections(selections);
  403. this.doc.setSelections(cmSelections, 0);
  404. }
  405. /**
  406. * Handle keydown events from the editor.
  407. */
  408. protected onKeydown(event: KeyboardEvent): boolean {
  409. let position = this.getCursorPosition();
  410. let { line, column } = position;
  411. if (line === 0 && column === 0 && event.keyCode === UP_ARROW) {
  412. if (!event.shiftKey) {
  413. this.edgeRequested.emit('top');
  414. }
  415. return false;
  416. }
  417. let lastLine = this.lineCount - 1;
  418. let lastCh = this.getLine(lastLine).length;
  419. if (line === lastLine && column === lastCh
  420. && event.keyCode === DOWN_ARROW) {
  421. if (!event.shiftKey) {
  422. this.edgeRequested.emit('bottom');
  423. }
  424. return false;
  425. }
  426. return false;
  427. }
  428. /**
  429. * Converts selections to code mirror selections.
  430. */
  431. private _toCodeMirrorSelections(selections: CodeEditor.IRange[]): CodeMirror.Selection[] {
  432. if (selections.length > 0) {
  433. return selections.map(selection => this._toCodeMirrorSelection(selection));
  434. }
  435. const position = { line: 0, ch: 0 };
  436. return [{ anchor: position, head: position }];
  437. }
  438. /**
  439. * Handles a mime type change.
  440. */
  441. private _onMimeTypeChanged(): void {
  442. const mime = this._model.mimeType;
  443. let editor = this._editor;
  444. Mode.ensure(mime).then(spec => {
  445. editor.setOption('mode', spec.mime);
  446. });
  447. let isCode = (mime !== 'text/plain') && (mime !== 'text/x-ipythongfm');
  448. editor.setOption('matchBrackets', isCode);
  449. editor.setOption('autoCloseBrackets', isCode);
  450. let extraKeys = editor.getOption('extraKeys') || {};
  451. if (isCode) {
  452. extraKeys['Backspace'] = 'delSpaceToPrevTabStop';
  453. } else {
  454. delete extraKeys['Backspace'];
  455. }
  456. editor.setOption('extraKeys', extraKeys);
  457. }
  458. /**
  459. * Handles a selections change.
  460. */
  461. private _onSelectionsChanged(selections: IObservableMap<CodeEditor.ITextSelection[]>, args: IObservableMap.IChangedArgs<CodeEditor.ITextSelection[]>): void {
  462. const uuid = args.key;
  463. if (uuid !== this.uuid) {
  464. this._cleanSelections(uuid);
  465. if (args.type !== 'remove') {
  466. this._markSelections(uuid, args.newValue);
  467. }
  468. }
  469. }
  470. /**
  471. * Clean selections for the given uuid.
  472. */
  473. private _cleanSelections(uuid: string) {
  474. const markers = this.selectionMarkers[uuid];
  475. if (markers) {
  476. markers.forEach(marker => { marker.clear(); });
  477. }
  478. delete this.selectionMarkers[uuid];
  479. }
  480. /**
  481. * Marks selections.
  482. */
  483. private _markSelections(uuid: string, selections: CodeEditor.ITextSelection[]) {
  484. const markers: CodeMirror.TextMarker[] = [];
  485. // If we are marking selections corresponding to an active hover,
  486. // remove it.
  487. if (uuid === this._hoverId) {
  488. this._clearHover();
  489. }
  490. // If we can id the selection to a specific collaborator,
  491. // use that information.
  492. let collaborator: ICollaborator;
  493. if (this._model.modelDB.collaborators) {
  494. collaborator = this._model.modelDB.collaborators.get(uuid);
  495. }
  496. // Style each selection for the uuid.
  497. selections.forEach(selection => {
  498. // Only render selections if the start is not equal to the end.
  499. // In that case, we don't need to render the cursor.
  500. if (!JSONExt.deepEqual(selection.start, selection.end)) {
  501. const { anchor, head } = this._toCodeMirrorSelection(selection);
  502. let markerOptions: CodeMirror.TextMarkerOptions;
  503. if (collaborator) {
  504. markerOptions = this._toTextMarkerOptions({
  505. ...selection.style,
  506. color: collaborator.color
  507. });
  508. } else {
  509. markerOptions = this._toTextMarkerOptions(selection.style);
  510. }
  511. markers.push(this.doc.markText(anchor, head, markerOptions));
  512. } else {
  513. let caret = this._getCaret(collaborator);
  514. markers.push(this.doc.setBookmark(
  515. this._toCodeMirrorPosition(selection.end), {widget: caret}));
  516. }
  517. });
  518. this.selectionMarkers[uuid] = markers;
  519. }
  520. /**
  521. * Handles a cursor activity event.
  522. */
  523. private _onCursorActivity(): void {
  524. // Only add selections if the editor has focus. This avoids unwanted
  525. // triggering of cursor activity due to collaborator actions.
  526. if (this._editor.hasFocus()) {
  527. const selections = this.getSelections();
  528. this.model.selections.set(this.uuid, selections);
  529. }
  530. }
  531. /**
  532. * Converts a code mirror selection to an editor selection.
  533. */
  534. private _toSelection(selection: CodeMirror.Selection): CodeEditor.ITextSelection {
  535. return {
  536. uuid: this.uuid,
  537. start: this._toPosition(selection.anchor),
  538. end: this._toPosition(selection.head),
  539. style: this.selectionStyle
  540. };
  541. }
  542. /**
  543. * Converts the selection style to a text marker options.
  544. */
  545. private _toTextMarkerOptions(style: CodeEditor.ISelectionStyle | undefined): CodeMirror.TextMarkerOptions | undefined {
  546. if (style) {
  547. let css: string;
  548. if (style.color) {
  549. let r = parseInt(style.color.slice(1,3), 16);
  550. let g = parseInt(style.color.slice(3,5), 16);
  551. let b = parseInt(style.color.slice(5,7), 16);
  552. css = `background-color: rgba( ${r}, ${g}, ${b}, 0.15)`;
  553. }
  554. return {
  555. className: style.className,
  556. title: style.displayName,
  557. css
  558. };
  559. }
  560. return undefined;
  561. }
  562. /**
  563. * Converts an editor selection to a code mirror selection.
  564. */
  565. private _toCodeMirrorSelection(selection: CodeEditor.IRange): CodeMirror.Selection {
  566. // Selections only appear to render correctly if the anchor
  567. // is before the head in the document. That is, reverse selections
  568. // do not appear as intended.
  569. let forward: boolean = (selection.start.line < selection.end.line) ||
  570. (selection.start.line === selection.end.line &&
  571. selection.start.column <= selection.end.column);
  572. let anchor = forward ? selection.start : selection.end;
  573. let head = forward ? selection.end : selection.start;
  574. return {
  575. anchor: this._toCodeMirrorPosition(anchor),
  576. head: this._toCodeMirrorPosition(head)
  577. };
  578. }
  579. /**
  580. * Converts an editor selection to a code mirror selection.
  581. */
  582. private _toCodeMirrorRange(range: CodeEditor.IRange): CodeMirror.Range {
  583. return {
  584. from: this._toCodeMirrorPosition(range.start),
  585. to: this._toCodeMirrorPosition(range.end)
  586. };
  587. }
  588. /**
  589. * Convert a code mirror position to an editor position.
  590. */
  591. private _toPosition(position: CodeMirror.Position) {
  592. return {
  593. line: position.line,
  594. column: position.ch
  595. };
  596. }
  597. /**
  598. * Convert an editor position to a code mirror position.
  599. */
  600. private _toCodeMirrorPosition(position: CodeEditor.IPosition) {
  601. return {
  602. line: position.line,
  603. ch: position.column
  604. };
  605. }
  606. /**
  607. * Handle model value changes.
  608. */
  609. private _onValueChanged(value: IObservableString, args: IObservableString.IChangedArgs): void {
  610. if (this._changeGuard) {
  611. return;
  612. }
  613. this._changeGuard = true;
  614. let doc = this.doc;
  615. switch (args.type) {
  616. case 'insert':
  617. let pos = doc.posFromIndex(args.start);
  618. doc.replaceRange(args.value, pos, pos);
  619. break;
  620. case 'remove':
  621. let from = doc.posFromIndex(args.start);
  622. let to = doc.posFromIndex(args.end);
  623. doc.replaceRange('', from, to);
  624. break;
  625. case 'set':
  626. doc.setValue(args.value);
  627. break;
  628. default:
  629. break;
  630. }
  631. this._changeGuard = false;
  632. }
  633. /**
  634. * Handles document changes.
  635. */
  636. private _beforeDocChanged(doc: CodeMirror.Doc, change: CodeMirror.EditorChange) {
  637. if (this._changeGuard) {
  638. return;
  639. }
  640. this._changeGuard = true;
  641. let value = this._model.value;
  642. let start = doc.indexFromPos(change.from);
  643. let end = doc.indexFromPos(change.to);
  644. let inserted = change.text.join('\n');
  645. if (end !== start) {
  646. value.remove(start, end);
  647. }
  648. if (inserted) {
  649. value.insert(start, inserted);
  650. }
  651. this._changeGuard = false;
  652. }
  653. /**
  654. * Handle the DOM events for the editor.
  655. *
  656. * @param event - The DOM event sent to the editor.
  657. *
  658. * #### Notes
  659. * This method implements the DOM `EventListener` interface and is
  660. * called in response to events on the editor's DOM node. It should
  661. * not be called directly by user code.
  662. */
  663. handleEvent(event: Event): void {
  664. switch (event.type) {
  665. case 'focus':
  666. this._evtFocus(event as FocusEvent);
  667. break;
  668. case 'scroll':
  669. this._evtScroll();
  670. break;
  671. default:
  672. break;
  673. }
  674. }
  675. /**
  676. * Handle `focus` events for the editor.
  677. */
  678. private _evtFocus(event: FocusEvent): void {
  679. if (this._needsRefresh) {
  680. this.refresh();
  681. }
  682. }
  683. /**
  684. * Handle `scroll` events for the editor.
  685. */
  686. private _evtScroll(): void {
  687. // Remove any active hover.
  688. this._clearHover();
  689. }
  690. /**
  691. * Clear the hover for a caret, due to things like
  692. * scrolling, resizing, deactivation, etc, where
  693. * the position is no longer valid.
  694. */
  695. private _clearHover(): void {
  696. if (this._caretHover) {
  697. window.clearTimeout(this._hoverTimeout);
  698. document.body.removeChild(this._caretHover);
  699. this._caretHover = null;
  700. }
  701. }
  702. /**
  703. * Construct a caret element representing the position
  704. * of a collaborator's cursor.
  705. */
  706. private _getCaret(collaborator: ICollaborator): HTMLElement {
  707. let name = collaborator ? collaborator.displayName : 'Anonymous';
  708. let color = collaborator ? collaborator.color : this._selectionStyle.color;
  709. let caret: HTMLElement = document.createElement('span');
  710. caret.className = COLLABORATOR_CURSOR_CLASS;
  711. caret.style.borderBottomColor = color;
  712. caret.onmouseenter = () => {
  713. this._clearHover();
  714. this._hoverId = collaborator.sessionId;
  715. let rect = caret.getBoundingClientRect();
  716. // Construct and place the hover box.
  717. let hover = document.createElement('div');
  718. hover.className = COLLABORATOR_HOVER_CLASS;
  719. hover.style.left = String(rect.left)+'px';
  720. hover.style.top = String(rect.bottom)+'px';
  721. hover.textContent = name;
  722. hover.style.backgroundColor = color;
  723. // If the user mouses over the hover, take over the timer.
  724. hover.onmouseenter = () => {
  725. window.clearTimeout(this._hoverTimeout);
  726. }
  727. hover.onmouseleave = () => {
  728. this._hoverTimeout = window.setTimeout(() => {
  729. this._clearHover();
  730. }, HOVER_TIMEOUT);
  731. }
  732. this._caretHover = hover;
  733. document.body.appendChild(hover);
  734. };
  735. caret.onmouseleave = () => {
  736. this._hoverTimeout = window.setTimeout(() => {
  737. this._clearHover();
  738. }, HOVER_TIMEOUT);
  739. };
  740. return caret;
  741. }
  742. private _model: CodeEditor.IModel;
  743. private _editor: CodeMirror.Editor;
  744. protected selectionMarkers: { [key: string]: CodeMirror.TextMarker[] | undefined } = {};
  745. private _caretHover: HTMLElement = null;
  746. private _hoverTimeout: number = null;
  747. private _hoverId: string = null;
  748. private _keydownHandlers = new Array<CodeEditor.KeydownHandler>();
  749. private _changeGuard = false;
  750. private _selectionStyle: CodeEditor.ISelectionStyle;
  751. private _uuid = '';
  752. private _needsRefresh = false;
  753. }
  754. /**
  755. * The namespace for `CodeMirrorEditor` statics.
  756. */
  757. export
  758. namespace CodeMirrorEditor {
  759. /**
  760. * The options used to initialize a code mirror editor.
  761. */
  762. export
  763. interface IOptions extends CodeEditor.IOptions {
  764. /**
  765. * The configuration options for the editor.
  766. */
  767. config?: Partial<IConfig>;
  768. }
  769. /**
  770. * The configuration options for a codemirror editor.
  771. */
  772. export
  773. interface IConfig extends CodeEditor.IConfig {
  774. /**
  775. * The mode to use.
  776. */
  777. mode?: string | Mode.ISpec;
  778. /**
  779. * The theme to style the editor with.
  780. * You must make sure the CSS file defining the corresponding
  781. * .cm-s-[name] styles is loaded.
  782. */
  783. theme?: string;
  784. /**
  785. * Whether to use the context-sensitive indentation that the mode provides
  786. * (or just indent the same as the line before).
  787. */
  788. smartIndent?: boolean;
  789. /**
  790. * Configures whether the editor should re-indent the current line when a
  791. * character is typed that might change its proper indentation
  792. * (only works if the mode supports indentation).
  793. */
  794. electricChars?: boolean;
  795. /**
  796. * Configures the keymap to use. The default is "default", which is the
  797. * only keymap defined in codemirror.js itself.
  798. * Extra keymaps are found in the CodeMirror keymap directory.
  799. */
  800. keyMap?: string;
  801. /**
  802. * Can be used to specify extra keybindings for the editor, alongside the
  803. * ones defined by keyMap. Should be either null, or a valid keymap value.
  804. */
  805. extraKeys?: any;
  806. /**
  807. * Can be used to add extra gutters (beyond or instead of the line number
  808. * gutter).
  809. * Should be an array of CSS class names, each of which defines a width
  810. * (and optionally a background),
  811. * and which will be used to draw the background of the gutters.
  812. * May include the CodeMirror-linenumbers class, in order to explicitly
  813. * set the position of the line number gutter
  814. * (it will default to be to the right of all other gutters).
  815. * These class names are the keys passed to setGutterMarker.
  816. */
  817. gutters?: ReadonlyArray<string>;
  818. /**
  819. * Determines whether the gutter scrolls along with the content
  820. * horizontally (false)
  821. * or whether it stays fixed during horizontal scrolling (true,
  822. * the default).
  823. */
  824. fixedGutter?: boolean;
  825. /**
  826. * Whether the cursor should be drawn when a selection is active.
  827. */
  828. showCursorWhenSelecting?: boolean;
  829. /**
  830. * When fixedGutter is on, and there is a horizontal scrollbar, by default
  831. * the gutter will be visible to the left of this scrollbar. If this
  832. * option is set to true, it will be covered by an element with class
  833. * CodeMirror-gutter-filler.
  834. */
  835. coverGutterNextToScrollbar: boolean;
  836. /**
  837. * Explicitly set the line separator for the editor.
  838. * By default (value null), the document will be split on CRLFs as well as
  839. * lone CRs and LFs, and a single LF will be used as line separator in all
  840. * output (such as getValue). When a specific string is given, lines will
  841. * only be split on that string, and output will, by default, use that
  842. * same separator.
  843. */
  844. lineSeparator: string;
  845. /**
  846. * Chooses a scrollbar implementation. The default is "native", showing
  847. * native scrollbars. The core library also provides the "null" style,
  848. * which completely hides the scrollbars. Addons can implement additional
  849. * scrollbar models.
  850. */
  851. scrollbarStyle: string;
  852. /**
  853. * When enabled, which is the default, doing copy or cut when there is no
  854. * selection will copy or cut the whole lines that have cursors on them.
  855. */
  856. lineWiseCopyCut: boolean;
  857. }
  858. /**
  859. * The default configuration options for an editor.
  860. */
  861. export
  862. let defaultConfig: IConfig = {
  863. ...CodeEditor.defaultConfig,
  864. mode: 'null',
  865. theme: 'jupyter',
  866. smartIndent: true,
  867. electricChars: true,
  868. keyMap: 'default',
  869. extraKeys: null,
  870. gutters: Object.freeze([]),
  871. fixedGutter: true,
  872. showCursorWhenSelecting: false,
  873. coverGutterNextToScrollbar: false,
  874. dragDrop: true,
  875. lineSeparator: null,
  876. scrollbarStyle: 'native',
  877. lineWiseCopyCut: true,
  878. };
  879. /**
  880. * Add a command to CodeMirror.
  881. *
  882. * @param name - The name of the command to add.
  883. *
  884. * @param command - The command function.
  885. */
  886. export
  887. function addCommand(name: string, command: (cm: CodeMirror.Editor) => void) {
  888. CodeMirror.commands[name] = command;
  889. }
  890. }
  891. /**
  892. * The namespace for module private data.
  893. */
  894. namespace Private {
  895. /**
  896. * Handle the codemirror configuration options.
  897. */
  898. export
  899. function handleConfig(editor: CodeMirror.Editor, config: Partial<CodeMirrorEditor.IConfig>): void {
  900. config = {
  901. ...CodeMirrorEditor.defaultConfig,
  902. ...config
  903. };
  904. for (let key of Object.keys(config)) {
  905. let option = key as keyof CodeMirrorEditor.IConfig;
  906. Private.setOption(editor, option, config[option]);
  907. }
  908. }
  909. /**
  910. * Delete spaces to the previous tab stob in a codemirror editor.
  911. */
  912. export
  913. function delSpaceToPrevTabStop(cm: CodeMirror.Editor): void {
  914. let doc = cm.getDoc();
  915. let from = doc.getCursor('from');
  916. let to = doc.getCursor('to');
  917. let sel = !posEq(from, to);
  918. if (sel) {
  919. let ranges = doc.listSelections();
  920. for (let i = ranges.length - 1; i >= 0; i--) {
  921. let head = ranges[i].head;
  922. let anchor = ranges[i].anchor;
  923. doc.replaceRange('', CodeMirror.Pos(head.line, head.ch), CodeMirror.Pos(anchor.line, anchor.ch));
  924. }
  925. return;
  926. }
  927. let cur = doc.getCursor();
  928. let tabsize = cm.getOption('tabSize');
  929. let chToPrevTabStop = cur.ch - (Math.ceil(cur.ch / tabsize) - 1) * tabsize;
  930. from = {ch: cur.ch - chToPrevTabStop, line: cur.line};
  931. let select = doc.getRange(from, cur);
  932. if (select.match(/^\ +$/) !== null) {
  933. doc.replaceRange('', from, cur);
  934. } else {
  935. CodeMirror.commands['delCharBefore'](cm);
  936. }
  937. };
  938. /**
  939. * Test whether two CodeMirror positions are equal.
  940. */
  941. export
  942. function posEq(a: CodeMirror.Position, b: CodeMirror.Position): boolean {
  943. return a.line === b.line && a.ch === b.ch;
  944. };
  945. /**
  946. * Get a config option for the editor.
  947. */
  948. export
  949. function getOption<K extends keyof CodeMirrorEditor.IConfig>(editor: CodeMirror.Editor, option: K): CodeMirrorEditor.IConfig[K] {
  950. switch (option) {
  951. case 'wordWrap':
  952. return editor.getOption('lineWrapping');
  953. case 'insertSpaces':
  954. return !editor.getOption('indentWithTabs');
  955. case 'autoClosingBrackets':
  956. return editor.getOption('autoCloseBrackets');
  957. default:
  958. return editor.getOption(option);
  959. }
  960. }
  961. /**
  962. * Set a config option for the editor.
  963. */
  964. export
  965. function setOption<K extends keyof CodeMirrorEditor.IConfig>(editor: CodeMirror.Editor, option: K, value: CodeMirrorEditor.IConfig[K]): void {
  966. switch (option) {
  967. case 'wordWrap':
  968. editor.setOption('lineWrapping', value);
  969. break;
  970. case 'tabSize':
  971. editor.setOption('indentSize', value);
  972. editor.setOption('tabSize', value);
  973. break;
  974. case 'insertSpaces':
  975. editor.setOption('indentWithTabs', !value);
  976. break;
  977. case 'autoClosingBrackets':
  978. editor.setOption('autoCloseBrackets', !value);
  979. break;
  980. case 'readOnly':
  981. let el = editor.getWrapperElement();
  982. if (value) {
  983. el.classList.add(READ_ONLY_CLASS);
  984. } else {
  985. el.classList.remove(READ_ONLY_CLASS);
  986. editor.getInputField().blur();
  987. }
  988. editor.setOption(option, value);
  989. break;
  990. default:
  991. editor.setOption(option, value);
  992. break;
  993. }
  994. }
  995. }
  996. /**
  997. * Add a CodeMirror command to delete until previous non blanking space
  998. * character or first multiple of tabsize tabstop.
  999. */
  1000. CodeMirrorEditor.addCommand(
  1001. 'delSpaceToPrevTabStop', Private.delSpaceToPrevTabStop
  1002. );