editor.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  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. ArrayExt
  7. } from '@phosphor/algorithm';
  8. import {
  9. IDisposable, DisposableDelegate
  10. } from '@phosphor/disposable';
  11. import {
  12. Signal
  13. } from '@phosphor/signaling';
  14. import {
  15. CodeEditor
  16. } from '@jupyterlab/codeeditor';
  17. import {
  18. uuid
  19. } from '@jupyterlab/coreutils';
  20. import {
  21. IObservableString, ObservableString
  22. } from '@jupyterlab/coreutils';
  23. import {
  24. IObservableMap, ObservableMap
  25. } from '@jupyterlab/coreutils';
  26. import {
  27. loadModeByMIME
  28. } from './mode';
  29. import 'codemirror/addon/edit/matchbrackets.js';
  30. import 'codemirror/addon/edit/closebrackets.js';
  31. import 'codemirror/addon/comment/comment.js';
  32. import 'codemirror/keymap/vim.js';
  33. /**
  34. * The class name added to CodeMirrorWidget instances.
  35. */
  36. const EDITOR_CLASS = 'jp-CodeMirrorEditor';
  37. /**
  38. * The class name added to read only cell editor widgets.
  39. */
  40. const READ_ONLY_CLASS = 'jp-mod-readOnly';
  41. /**
  42. * The key code for the up arrow key.
  43. */
  44. const UP_ARROW = 38;
  45. /**
  46. * The key code for the down arrow key.
  47. */
  48. const DOWN_ARROW = 40;
  49. /**
  50. * CodeMirror editor.
  51. */
  52. export
  53. class CodeMirrorEditor implements CodeEditor.IEditor {
  54. /**
  55. * Construct a CodeMirror editor.
  56. */
  57. constructor(options: CodeEditor.IOptions, config: CodeMirror.EditorConfiguration={}) {
  58. let host = this.host = options.host;
  59. host.classList.add(EDITOR_CLASS);
  60. this._uuid = options.uuid || uuid();
  61. this._selectionStyle = options.selectionStyle || {};
  62. Private.updateConfig(options, config);
  63. let model = this._model = options.model;
  64. let editor = this._editor = CodeMirror(host, config);
  65. let doc = editor.getDoc();
  66. // Handle initial values for text, mimetype, and selections.
  67. doc.setValue(model.value.text);
  68. this._onMimeTypeChanged();
  69. this._onCursorActivity();
  70. // Connect to changes.
  71. model.value.changed.connect(this._onValueChanged, this);
  72. model.mimeTypeChanged.connect(this._onMimeTypeChanged, this);
  73. model.selections.changed.connect(this._onSelectionsChanged, this);
  74. CodeMirror.on(editor, 'keydown', (editor, event) => {
  75. let index = ArrayExt.findFirstIndex(this._keydownHandlers, handler => {
  76. if (handler(this, event) === true) {
  77. event.preventDefault();
  78. return true;
  79. }
  80. });
  81. if (index === -1) {
  82. this.onKeydown(event);
  83. }
  84. });
  85. CodeMirror.on(editor, 'cursorActivity', () => this._onCursorActivity());
  86. CodeMirror.on(editor.getDoc(), 'beforeChange', (instance, change) => {
  87. this._onDocChanged(instance, change);
  88. });
  89. }
  90. /**
  91. * A signal emitted when either the top or bottom edge is requested.
  92. */
  93. readonly edgeRequested = new Signal<this, CodeEditor.EdgeLocation>(this);
  94. /**
  95. * The DOM node that hosts the editor.
  96. */
  97. readonly host: HTMLElement;
  98. /**
  99. * The uuid of this editor;
  100. */
  101. get uuid(): string {
  102. return this._uuid;
  103. }
  104. set uuid(value: string) {
  105. this._uuid = value;
  106. }
  107. /**
  108. * The selection style of this editor.
  109. */
  110. get selectionStyle(): CodeEditor.ISelectionStyle {
  111. return this._selectionStyle;
  112. }
  113. set selectionStyle(value: CodeEditor.ISelectionStyle) {
  114. this._selectionStyle = value;
  115. }
  116. /**
  117. * Get the codemirror editor wrapped by the editor.
  118. */
  119. get editor(): CodeMirror.Editor {
  120. return this._editor;
  121. }
  122. /**
  123. * Get the codemirror doc wrapped by the widget.
  124. */
  125. get doc(): CodeMirror.Doc {
  126. return this._editor.getDoc();
  127. }
  128. /**
  129. * Get the number of lines in the editor.
  130. */
  131. get lineCount(): number {
  132. return this.doc.lineCount();
  133. }
  134. /**
  135. * Control the rendering of line numbers.
  136. */
  137. get lineNumbers(): boolean {
  138. return this._editor.getOption('lineNumbers');
  139. }
  140. set lineNumbers(value: boolean) {
  141. this._editor.setOption('lineNumbers', value);
  142. }
  143. /**
  144. * Set to false for horizontal scrolling. Defaults to true.
  145. */
  146. get wordWrap(): boolean {
  147. return this._editor.getOption('lineWrapping');
  148. }
  149. set wordWrap(value: boolean) {
  150. this._editor.setOption('lineWrapping', value);
  151. }
  152. /**
  153. * Should the editor be read only.
  154. */
  155. get readOnly(): boolean {
  156. return this._editor.getOption('readOnly') !== false;
  157. }
  158. set readOnly(readOnly: boolean) {
  159. this._editor.setOption('readOnly', readOnly);
  160. if (readOnly) {
  161. this.host.classList.add(READ_ONLY_CLASS);
  162. } else {
  163. this.host.classList.remove(READ_ONLY_CLASS);
  164. this.blur();
  165. }
  166. }
  167. /**
  168. * Returns a model for this editor.
  169. */
  170. get model(): CodeEditor.IModel {
  171. return this._model;
  172. }
  173. /**
  174. * The height of a line in the editor in pixels.
  175. */
  176. get lineHeight(): number {
  177. return this._editor.defaultTextHeight();
  178. }
  179. /**
  180. * The widget of a character in the editor in pixels.
  181. */
  182. get charWidth(): number {
  183. return this._editor.defaultCharWidth();
  184. }
  185. /**
  186. * Tests whether the editor is disposed.
  187. */
  188. get isDisposed(): boolean {
  189. return this._editor === null;
  190. }
  191. /**
  192. * Dispose of the resources held by the widget.
  193. */
  194. dispose(): void {
  195. if (this._editor === null) {
  196. return;
  197. }
  198. this._editor = null;
  199. this._model = null;
  200. this._keydownHandlers.length = 0;
  201. Signal.clearData(this);
  202. }
  203. /**
  204. * Returns the content for the given line number.
  205. */
  206. getLine(line: number): string | undefined {
  207. return this.doc.getLine(line);
  208. }
  209. /**
  210. * Find an offset for the given position.
  211. */
  212. getOffsetAt(position: CodeEditor.IPosition): number {
  213. return this.doc.indexFromPos({
  214. ch: position.column,
  215. line: position.line
  216. });
  217. }
  218. /**
  219. * Find a position fot the given offset.
  220. */
  221. getPositionAt(offset: number): CodeEditor.IPosition {
  222. const { ch, line } = this.doc.posFromIndex(offset);
  223. return { line, column: ch };
  224. }
  225. /**
  226. * Undo one edit (if any undo events are stored).
  227. */
  228. undo(): void {
  229. this.doc.undo();
  230. }
  231. /**
  232. * Redo one undone edit.
  233. */
  234. redo(): void {
  235. this.doc.redo();
  236. }
  237. /**
  238. * Clear the undo history.
  239. */
  240. clearHistory(): void {
  241. this.doc.clearHistory();
  242. }
  243. /**
  244. * Brings browser focus to this editor text.
  245. */
  246. focus(): void {
  247. this._editor.focus();
  248. }
  249. /**
  250. * Test whether the editor has keyboard focus.
  251. */
  252. hasFocus(): boolean {
  253. return this._editor.hasFocus();
  254. }
  255. /**
  256. * Explicitly blur the editor.
  257. */
  258. blur(): void {
  259. this._editor.getInputField().blur();
  260. }
  261. /**
  262. * Repaint editor.
  263. */
  264. refresh(): void {
  265. this._editor.refresh();
  266. }
  267. /**
  268. * Add a keydown handler to the editor.
  269. *
  270. * @param handler - A keydown handler.
  271. *
  272. * @returns A disposable that can be used to remove the handler.
  273. */
  274. addKeydownHandler(handler: CodeEditor.KeydownHandler): IDisposable {
  275. this._keydownHandlers.push(handler);
  276. return new DisposableDelegate(() => {
  277. ArrayExt.removeAllWhere(this._keydownHandlers, val => val === handler);
  278. });
  279. }
  280. /**
  281. * Set the size of the editor in pixels.
  282. */
  283. setSize(dimension: CodeEditor.IDimension | null): void {
  284. if (dimension) {
  285. this._editor.setSize(dimension.width, dimension.height);
  286. } else {
  287. this._editor.setSize(null, null);
  288. }
  289. }
  290. /**
  291. * Reveal the given position in the editor.
  292. */
  293. revealPosition(position: CodeEditor.IPosition): void {
  294. const cmPosition = this._toCodeMirrorPosition(position);
  295. this._editor.scrollIntoView(cmPosition);
  296. }
  297. /**
  298. * Reveal the given selection in the editor.
  299. */
  300. revealSelection(selection: CodeEditor.IRange): void {
  301. const range = this._toCodeMirrorRange(selection);
  302. this._editor.scrollIntoView(range);
  303. }
  304. /**
  305. * Get the window coordinates given a cursor position.
  306. */
  307. getCoordinateForPosition(position: CodeEditor.IPosition): CodeEditor.ICoordinate {
  308. const pos = this._toCodeMirrorPosition(position);
  309. const rect = this.editor.charCoords(pos, 'page');
  310. return rect as CodeEditor.ICoordinate;
  311. }
  312. /**
  313. * Get the cursor position given window coordinates.
  314. *
  315. * @param coordinate - The desired coordinate.
  316. *
  317. * @returns The position of the coordinates, or null if not
  318. * contained in the editor.
  319. */
  320. getPositionForCoordinate(coordinate: CodeEditor.ICoordinate): CodeEditor.IPosition | null {
  321. return this._toPosition(this.editor.coordsChar(coordinate)) || null;
  322. }
  323. /**
  324. * Returns the primary position of the cursor, never `null`.
  325. */
  326. getCursorPosition(): CodeEditor.IPosition {
  327. const cursor = this.doc.getCursor();
  328. return this._toPosition(cursor);
  329. }
  330. /**
  331. * Set the primary position of the cursor.
  332. *
  333. * #### Notes
  334. * This will remove any secondary cursors.
  335. */
  336. setCursorPosition(position: CodeEditor.IPosition): void {
  337. const cursor = this._toCodeMirrorPosition(position);
  338. this.doc.setCursor(cursor);
  339. }
  340. /**
  341. * Returns the primary selection, never `null`.
  342. */
  343. getSelection(): CodeEditor.ITextSelection {
  344. return this.getSelections()[0];
  345. }
  346. /**
  347. * Set the primary selection. This will remove any secondary cursors.
  348. */
  349. setSelection(selection: CodeEditor.IRange): void {
  350. this.setSelections([selection]);
  351. }
  352. /**
  353. * Gets the selections for all the cursors, never `null` or empty.
  354. */
  355. getSelections(): CodeEditor.ITextSelection[] {
  356. const selections = this.doc.listSelections();
  357. if (selections.length > 0) {
  358. return selections.map(selection => this._toSelection(selection));
  359. }
  360. const cursor = this.doc.getCursor();
  361. const selection = this._toSelection({ anchor: cursor, head: cursor });
  362. return [selection];
  363. }
  364. /**
  365. * Sets the selections for all the cursors, should not be empty.
  366. * Cursors will be removed or added, as necessary.
  367. * Passing an empty array resets a cursor position to the start of a document.
  368. */
  369. setSelections(selections: CodeEditor.IRange[]): void {
  370. const cmSelections = this._toCodeMirrorSelections(selections);
  371. this.doc.setSelections(cmSelections, 0);
  372. }
  373. /**
  374. * Handle keydown events from the editor.
  375. */
  376. protected onKeydown(event: KeyboardEvent): boolean {
  377. let position = this.getCursorPosition();
  378. let { line, column } = position;
  379. if (line === 0 && column === 0 && event.keyCode === UP_ARROW) {
  380. if (!event.shiftKey) {
  381. this.edgeRequested.emit('top');
  382. }
  383. return false;
  384. }
  385. let lastLine = this.lineCount - 1;
  386. let lastCh = this.getLine(lastLine).length;
  387. if (line === lastLine && column === lastCh
  388. && event.keyCode === DOWN_ARROW) {
  389. if (!event.shiftKey) {
  390. this.edgeRequested.emit('bottom');
  391. }
  392. return false;
  393. }
  394. return false;
  395. }
  396. /**
  397. * Converts selections to code mirror selections.
  398. */
  399. private _toCodeMirrorSelections(selections: CodeEditor.IRange[]): CodeMirror.Selection[] {
  400. if (selections.length > 0) {
  401. return selections.map(selection => this._toCodeMirrorSelection(selection));
  402. }
  403. const position = { line: 0, ch: 0 };
  404. return [{ anchor: position, head: position }];
  405. }
  406. /**
  407. * Handles a mime type change.
  408. */
  409. private _onMimeTypeChanged(): void {
  410. const mime = this._model.mimeType;
  411. let editor = this._editor;
  412. loadModeByMIME(editor, mime);
  413. let isCode = (mime !== 'text/plain') && (mime !== 'text/x-ipythongfm');
  414. editor.setOption('matchBrackets', isCode);
  415. editor.setOption('autoCloseBrackets', isCode);
  416. let extraKeys = editor.getOption('extraKeys') || {};
  417. if (isCode) {
  418. extraKeys['Backspace'] = 'delSpaceToPrevTabStop';
  419. } else {
  420. delete extraKeys['Backspace'];
  421. }
  422. editor.setOption('extraKeys', extraKeys);
  423. }
  424. /**
  425. * Handles a selections change.
  426. */
  427. private _onSelectionsChanged(selections: IObservableMap<CodeEditor.ITextSelection[]>, args: ObservableMap.IChangedArgs<CodeEditor.ITextSelection[]>): void {
  428. const uuid = args.key;
  429. if (uuid !== this.uuid) {
  430. this._cleanSelections(uuid);
  431. this._markSelections(uuid, args.newValue);
  432. }
  433. }
  434. /**
  435. * Clean selections for the given uuid.
  436. */
  437. private _cleanSelections(uuid: string) {
  438. const markers = this.selectionMarkers[uuid];
  439. if (markers) {
  440. markers.forEach(marker => { marker.clear(); });
  441. }
  442. delete this.selectionMarkers[uuid];
  443. }
  444. /**
  445. * Marks selections.
  446. */
  447. private _markSelections(uuid: string, selections: CodeEditor.ITextSelection[]) {
  448. const markers: CodeMirror.TextMarker[] = [];
  449. selections.forEach(selection => {
  450. const { anchor, head } = this._toCodeMirrorSelection(selection);
  451. const markerOptions = this._toTextMarkerOptions(selection);
  452. this.doc.markText(anchor, head, markerOptions);
  453. });
  454. this.selectionMarkers[uuid] = markers;
  455. }
  456. /**
  457. * Handles a cursor activity event.
  458. */
  459. private _onCursorActivity(): void {
  460. const selections = this.getSelections();
  461. this.model.selections.set(this.uuid, selections);
  462. }
  463. /**
  464. * Converts a code mirror selection to an editor selection.
  465. */
  466. private _toSelection(selection: CodeMirror.Selection): CodeEditor.ITextSelection {
  467. return {
  468. uuid: this.uuid,
  469. start: this._toPosition(selection.anchor),
  470. end: this._toPosition(selection.head),
  471. style: this.selectionStyle
  472. };
  473. }
  474. /**
  475. * Converts the selection style to a text marker options.
  476. */
  477. private _toTextMarkerOptions(style: CodeEditor.ISelectionStyle | undefined): CodeMirror.TextMarkerOptions | undefined {
  478. if (style) {
  479. return {
  480. className: style.className,
  481. title: style.displayName
  482. };
  483. }
  484. return undefined;
  485. }
  486. /**
  487. * Converts an editor selection to a code mirror selection.
  488. */
  489. private _toCodeMirrorSelection(selection: CodeEditor.IRange): CodeMirror.Selection {
  490. return {
  491. anchor: this._toCodeMirrorPosition(selection.start),
  492. head: this._toCodeMirrorPosition(selection.end)
  493. };
  494. }
  495. /**
  496. * Converts an editor selection to a code mirror selection.
  497. */
  498. private _toCodeMirrorRange(range: CodeEditor.IRange): CodeMirror.Range {
  499. return {
  500. from: this._toCodeMirrorPosition(range.start),
  501. to: this._toCodeMirrorPosition(range.end)
  502. };
  503. }
  504. /**
  505. * Convert a code mirror position to an editor position.
  506. */
  507. private _toPosition(position: CodeMirror.Position) {
  508. return {
  509. line: position.line,
  510. column: position.ch
  511. };
  512. }
  513. /**
  514. * Convert an editor position to a code mirror position.
  515. */
  516. private _toCodeMirrorPosition(position: CodeEditor.IPosition) {
  517. return {
  518. line: position.line,
  519. ch: position.column
  520. };
  521. }
  522. /**
  523. * Handle model value changes.
  524. */
  525. private _onValueChanged(value: IObservableString, args: ObservableString.IChangedArgs): void {
  526. if (this._changeGuard) {
  527. return;
  528. }
  529. this._changeGuard = true;
  530. let doc = this.doc;
  531. switch (args.type) {
  532. case 'insert':
  533. let pos = doc.posFromIndex(args.start);
  534. doc.replaceRange(args.value, pos, pos);
  535. break;
  536. case 'remove':
  537. let from = doc.posFromIndex(args.start);
  538. let to = doc.posFromIndex(args.end);
  539. doc.replaceRange('', from, to);
  540. break;
  541. case 'set':
  542. doc.setValue(args.value);
  543. break;
  544. default:
  545. break;
  546. }
  547. this._changeGuard = false;
  548. }
  549. /**
  550. * Handles document changes.
  551. */
  552. private _onDocChanged(doc: CodeMirror.Doc, change: CodeMirror.EditorChange) {
  553. if (this._changeGuard) {
  554. return;
  555. }
  556. this._changeGuard = true;
  557. let value = this._model.value;
  558. let start = doc.indexFromPos(change.from);
  559. let end = doc.indexFromPos(change.to);
  560. let inserted = change.text.join('\n');
  561. if (end !== start) {
  562. value.remove(start, end);
  563. }
  564. if (inserted) {
  565. value.insert(start, inserted);
  566. }
  567. this._changeGuard = false;
  568. }
  569. private _model: CodeEditor.IModel;
  570. private _editor: CodeMirror.Editor;
  571. protected selectionMarkers: { [key: string]: CodeMirror.TextMarker[] | undefined } = {};
  572. private _keydownHandlers = new Array<CodeEditor.KeydownHandler>();
  573. private _changeGuard = false;
  574. private _selectionStyle: CodeEditor.ISelectionStyle;
  575. private _uuid = '';
  576. }
  577. /**
  578. * The namespace for `CodeMirrorEditor` statics.
  579. */
  580. export
  581. namespace CodeMirrorEditor {
  582. /**
  583. * The name of the default CodeMirror theme
  584. */
  585. export
  586. const DEFAULT_THEME: string = 'jupyter';
  587. }
  588. /**
  589. * The namespace for module private data.
  590. */
  591. namespace Private {
  592. /**
  593. * Handle extra codemirror config from codeeditor options.
  594. */
  595. export
  596. function updateConfig(options: CodeEditor.IOptions, config: CodeMirror.EditorConfiguration): void {
  597. if (options.readOnly !== undefined) {
  598. config.readOnly = options.readOnly;
  599. } else {
  600. config.readOnly = false;
  601. }
  602. if (options.lineNumbers !== undefined) {
  603. config.lineNumbers = options.lineNumbers;
  604. } else {
  605. config.lineNumbers = false;
  606. }
  607. if (options.wordWrap !== undefined) {
  608. config.lineWrapping = options.wordWrap;
  609. } else {
  610. config.lineWrapping = true;
  611. }
  612. config.theme = (config.theme || CodeMirrorEditor.DEFAULT_THEME);
  613. config.indentUnit = config.indentUnit || 4;
  614. }
  615. /**
  616. * Delete spaces to the previous tab stob in a codemirror editor.
  617. */
  618. export
  619. function delSpaceToPrevTabStop(cm: CodeMirror.Editor): void {
  620. let doc = cm.getDoc();
  621. let from = doc.getCursor('from');
  622. let to = doc.getCursor('to');
  623. let sel = !posEq(from, to);
  624. if (sel) {
  625. let ranges = doc.listSelections();
  626. for (let i = ranges.length - 1; i >= 0; i--) {
  627. let head = ranges[i].head;
  628. let anchor = ranges[i].anchor;
  629. doc.replaceRange('', CodeMirror.Pos(head.line, head.ch), CodeMirror.Pos(anchor.line, anchor.ch));
  630. }
  631. return;
  632. }
  633. let cur = doc.getCursor();
  634. let tabsize = cm.getOption('tabSize');
  635. let chToPrevTabStop = cur.ch - (Math.ceil(cur.ch / tabsize) - 1) * tabsize;
  636. from = {ch: cur.ch - chToPrevTabStop, line: cur.line};
  637. let select = doc.getRange(from, cur);
  638. if (select.match(/^\ +$/) !== null) {
  639. doc.replaceRange('', from, cur);
  640. } else {
  641. CodeMirror.commands['delCharBefore'](cm);
  642. }
  643. };
  644. /**
  645. * Test whether two CodeMirror positions are equal.
  646. */
  647. export
  648. function posEq(a: CodeMirror.Position, b: CodeMirror.Position): boolean {
  649. return a.line === b.line && a.ch === b.ch;
  650. };
  651. }
  652. /**
  653. * Add a CodeMirror command to delete until previous non blanking space
  654. * character or first multiple of 4 tabstop.
  655. */
  656. CodeMirror.commands['delSpaceToPrevTabStop'] = Private.delSpaceToPrevTabStop;