editor.ts 33 KB

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