editor.ts 33 KB

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