editor.ts 33 KB

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