editor.ts 41 KB

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