index.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. ILayoutRestorer,
  5. JupyterLab,
  6. JupyterLabPlugin
  7. } from '@jupyterlab/application';
  8. import { ICommandPalette, InstanceTracker } from '@jupyterlab/apputils';
  9. import { CodeEditor, IEditorServices } from '@jupyterlab/codeeditor';
  10. import { IConsoleTracker } from '@jupyterlab/console';
  11. import {
  12. ISettingRegistry,
  13. MarkdownCodeBlocks,
  14. PathExt
  15. } from '@jupyterlab/coreutils';
  16. import { IDocumentWidget } from '@jupyterlab/docregistry';
  17. import { IFileBrowserFactory } from '@jupyterlab/filebrowser';
  18. import {
  19. FileEditor,
  20. FileEditorFactory,
  21. IEditorTracker,
  22. TabSpaceStatus
  23. } from '@jupyterlab/fileeditor';
  24. import { ILauncher } from '@jupyterlab/launcher';
  25. import {
  26. IEditMenu,
  27. IFileMenu,
  28. IMainMenu,
  29. IRunMenu,
  30. IViewMenu
  31. } from '@jupyterlab/mainmenu';
  32. import { IStatusBar } from '@jupyterlab/statusbar';
  33. import { JSONObject } from '@phosphor/coreutils';
  34. import { Menu } from '@phosphor/widgets';
  35. /**
  36. * The class name for the text editor icon from the default theme.
  37. */
  38. const EDITOR_ICON_CLASS = 'jp-TextEditorIcon';
  39. /**
  40. * The class name for the text editor icon from the default theme.
  41. */
  42. const MARKDOWN_ICON_CLASS = 'jp-MarkdownIcon';
  43. /**
  44. * The name of the factory that creates editor widgets.
  45. */
  46. const FACTORY = 'Editor';
  47. /**
  48. * The command IDs used by the fileeditor plugin.
  49. */
  50. namespace CommandIDs {
  51. export const createNew = 'fileeditor:create-new';
  52. export const createNewMarkdown = 'fileeditor:create-new-markdown-file';
  53. export const changeFontSize = 'fileeditor:change-font-size';
  54. export const lineNumbers = 'fileeditor:toggle-line-numbers';
  55. export const lineWrap = 'fileeditor:toggle-line-wrap';
  56. export const changeTabs = 'fileeditor:change-tabs';
  57. export const matchBrackets = 'fileeditor:toggle-match-brackets';
  58. export const autoClosingBrackets = 'fileeditor:toggle-autoclosing-brackets';
  59. export const createConsole = 'fileeditor:create-console';
  60. export const runCode = 'fileeditor:run-code';
  61. export const runAllCode = 'fileeditor:run-all';
  62. export const markdownPreview = 'fileeditor:markdown-preview';
  63. }
  64. /**
  65. * The editor tracker extension.
  66. */
  67. const plugin: JupyterLabPlugin<IEditorTracker> = {
  68. activate,
  69. id: '@jupyterlab/fileeditor-extension:plugin',
  70. requires: [
  71. IConsoleTracker,
  72. IEditorServices,
  73. IFileBrowserFactory,
  74. ILayoutRestorer,
  75. ISettingRegistry
  76. ],
  77. optional: [ICommandPalette, ILauncher, IMainMenu],
  78. provides: IEditorTracker,
  79. autoStart: true
  80. };
  81. /**
  82. * A plugin that provides a status item allowing the user to
  83. * switch tabs vs spaces and tab widths for text editors.
  84. */
  85. export const tabSpaceStatus: JupyterLabPlugin<void> = {
  86. id: '@jupyterlab/fileeditor-extension:tab-space-status',
  87. autoStart: true,
  88. requires: [IStatusBar, IEditorTracker, ISettingRegistry],
  89. activate: (
  90. app: JupyterLab,
  91. statusBar: IStatusBar,
  92. editorTracker: IEditorTracker,
  93. settingRegistry: ISettingRegistry
  94. ) => {
  95. // Create a menu for switching tabs vs spaces.
  96. const menu = new Menu({ commands: app.commands });
  97. const command = 'fileeditor:change-tabs';
  98. const args: JSONObject = {
  99. insertSpaces: false,
  100. size: 4,
  101. name: 'Indent with Tab'
  102. };
  103. menu.addItem({ command, args });
  104. for (let size of [1, 2, 4, 8]) {
  105. let args: JSONObject = {
  106. insertSpaces: true,
  107. size,
  108. name: `Spaces: ${size} `
  109. };
  110. menu.addItem({ command, args });
  111. }
  112. // Create the status item.
  113. const item = new TabSpaceStatus({ menu });
  114. // Keep a reference to the code editor config from the settings system.
  115. const updateSettings = (settings: ISettingRegistry.ISettings): void => {
  116. const cached = settings.get('editorConfig').composite as Partial<
  117. CodeEditor.IConfig
  118. >;
  119. const config: CodeEditor.IConfig = {
  120. ...CodeEditor.defaultConfig,
  121. ...cached
  122. };
  123. item.model!.config = config;
  124. };
  125. Promise.all([
  126. settingRegistry.load('@jupyterlab/fileeditor-extension:plugin'),
  127. app.restored
  128. ]).then(([settings]) => {
  129. updateSettings(settings);
  130. settings.changed.connect(updateSettings);
  131. });
  132. // Add the status item.
  133. statusBar.registerStatusItem(
  134. '@jupyterlab/fileeditor-extension:tab-space-status',
  135. {
  136. item,
  137. align: 'right',
  138. rank: 1,
  139. isActive: () => {
  140. return (
  141. app.shell.currentWidget &&
  142. editorTracker.has(app.shell.currentWidget)
  143. );
  144. }
  145. }
  146. );
  147. }
  148. };
  149. /**
  150. * Export the plugins as default.
  151. */
  152. const plugins: JupyterLabPlugin<any>[] = [plugin, tabSpaceStatus];
  153. export default plugins;
  154. /**
  155. * Activate the editor tracker plugin.
  156. */
  157. function activate(
  158. app: JupyterLab,
  159. consoleTracker: IConsoleTracker,
  160. editorServices: IEditorServices,
  161. browserFactory: IFileBrowserFactory,
  162. restorer: ILayoutRestorer,
  163. settingRegistry: ISettingRegistry,
  164. palette: ICommandPalette,
  165. launcher: ILauncher | null,
  166. menu: IMainMenu | null
  167. ): IEditorTracker {
  168. const id = plugin.id;
  169. const namespace = 'editor';
  170. const factory = new FileEditorFactory({
  171. editorServices,
  172. factoryOptions: {
  173. name: FACTORY,
  174. fileTypes: ['markdown', '*'], // Explicitly add the markdown fileType so
  175. defaultFor: ['markdown', '*'] // it outranks the defaultRendered viewer.
  176. }
  177. });
  178. const { commands, restored } = app;
  179. const tracker = new InstanceTracker<IDocumentWidget<FileEditor>>({
  180. namespace
  181. });
  182. const isEnabled = () =>
  183. tracker.currentWidget !== null &&
  184. tracker.currentWidget === app.shell.currentWidget;
  185. let config = { ...CodeEditor.defaultConfig };
  186. // Handle state restoration.
  187. restorer.restore(tracker, {
  188. command: 'docmanager:open',
  189. args: widget => ({ path: widget.context.path, factory: FACTORY }),
  190. name: widget => widget.context.path
  191. });
  192. /**
  193. * Update the setting values.
  194. */
  195. function updateSettings(settings: ISettingRegistry.ISettings): void {
  196. let cached = settings.get('editorConfig').composite as Partial<
  197. CodeEditor.IConfig
  198. >;
  199. Object.keys(config).forEach((key: keyof CodeEditor.IConfig) => {
  200. config[key] =
  201. cached[key] === null || cached[key] === undefined
  202. ? CodeEditor.defaultConfig[key]
  203. : cached[key];
  204. });
  205. // Trigger a refresh of the rendered commands
  206. app.commands.notifyCommandChanged();
  207. }
  208. /**
  209. * Update the settings of the current tracker instances.
  210. */
  211. function updateTracker(): void {
  212. tracker.forEach(widget => {
  213. updateWidget(widget.content);
  214. });
  215. }
  216. /**
  217. * Update the settings of a widget.
  218. */
  219. function updateWidget(widget: FileEditor): void {
  220. const editor = widget.editor;
  221. Object.keys(config).forEach((key: keyof CodeEditor.IConfig) => {
  222. editor.setOption(key, config[key]);
  223. });
  224. }
  225. // Add a console creator to the File menu
  226. // Fetch the initial state of the settings.
  227. Promise.all([settingRegistry.load(id), restored])
  228. .then(([settings]) => {
  229. updateSettings(settings);
  230. updateTracker();
  231. settings.changed.connect(() => {
  232. updateSettings(settings);
  233. updateTracker();
  234. });
  235. })
  236. .catch((reason: Error) => {
  237. console.error(reason.message);
  238. updateTracker();
  239. });
  240. factory.widgetCreated.connect((sender, widget) => {
  241. widget.title.icon = EDITOR_ICON_CLASS;
  242. // Notify the instance tracker if restore data needs to update.
  243. widget.context.pathChanged.connect(() => {
  244. tracker.save(widget);
  245. });
  246. tracker.add(widget);
  247. updateWidget(widget.content);
  248. });
  249. app.docRegistry.addWidgetFactory(factory);
  250. // Handle the settings of new widgets.
  251. tracker.widgetAdded.connect((sender, widget) => {
  252. updateWidget(widget.content);
  253. });
  254. // Add a command to change font size.
  255. commands.addCommand(CommandIDs.changeFontSize, {
  256. execute: args => {
  257. const delta = Number(args['delta']);
  258. if (Number.isNaN(delta)) {
  259. console.error(
  260. `${CommandIDs.changeFontSize}: delta arg must be a number`
  261. );
  262. return;
  263. }
  264. const style = window.getComputedStyle(document.documentElement);
  265. const cssSize = parseInt(
  266. style.getPropertyValue('--jp-code-font-size'),
  267. 10
  268. );
  269. const currentSize = config.fontSize || cssSize;
  270. config.fontSize = currentSize + delta;
  271. return settingRegistry
  272. .set(id, 'editorConfig', config)
  273. .catch((reason: Error) => {
  274. console.error(`Failed to set ${id}: ${reason.message}`);
  275. });
  276. },
  277. label: args => args['name'] as string
  278. });
  279. commands.addCommand(CommandIDs.lineNumbers, {
  280. execute: () => {
  281. config.lineNumbers = !config.lineNumbers;
  282. return settingRegistry
  283. .set(id, 'editorConfig', config)
  284. .catch((reason: Error) => {
  285. console.error(`Failed to set ${id}: ${reason.message}`);
  286. });
  287. },
  288. isEnabled,
  289. isToggled: () => config.lineNumbers,
  290. label: 'Line Numbers'
  291. });
  292. type wrappingMode = 'on' | 'off' | 'wordWrapColumn' | 'bounded';
  293. commands.addCommand(CommandIDs.lineWrap, {
  294. execute: args => {
  295. const lineWrap = (args['mode'] as wrappingMode) || 'off';
  296. config.lineWrap = lineWrap;
  297. return settingRegistry
  298. .set(id, 'editorConfig', config)
  299. .catch((reason: Error) => {
  300. console.error(`Failed to set ${id}: ${reason.message}`);
  301. });
  302. },
  303. isEnabled,
  304. isToggled: args => {
  305. const lineWrap = (args['mode'] as wrappingMode) || 'off';
  306. return config.lineWrap === lineWrap;
  307. },
  308. label: 'Word Wrap'
  309. });
  310. commands.addCommand(CommandIDs.changeTabs, {
  311. label: args => args['name'] as string,
  312. execute: args => {
  313. config.tabSize = (args['size'] as number) || 4;
  314. config.insertSpaces = !!args['insertSpaces'];
  315. return settingRegistry
  316. .set(id, 'editorConfig', config)
  317. .catch((reason: Error) => {
  318. console.error(`Failed to set ${id}: ${reason.message}`);
  319. });
  320. },
  321. isToggled: args => {
  322. const insertSpaces = !!args['insertSpaces'];
  323. const size = (args['size'] as number) || 4;
  324. return config.insertSpaces === insertSpaces && config.tabSize === size;
  325. }
  326. });
  327. commands.addCommand(CommandIDs.matchBrackets, {
  328. execute: () => {
  329. config.matchBrackets = !config.matchBrackets;
  330. return settingRegistry
  331. .set(id, 'editorConfig', config)
  332. .catch((reason: Error) => {
  333. console.error(`Failed to set ${id}: ${reason.message}`);
  334. });
  335. },
  336. label: 'Match Brackets',
  337. isEnabled,
  338. isToggled: () => config.matchBrackets
  339. });
  340. commands.addCommand(CommandIDs.autoClosingBrackets, {
  341. execute: () => {
  342. config.autoClosingBrackets = !config.autoClosingBrackets;
  343. return settingRegistry
  344. .set(id, 'editorConfig', config)
  345. .catch((reason: Error) => {
  346. console.error(`Failed to set ${id}: ${reason.message}`);
  347. });
  348. },
  349. label: 'Auto Close Brackets for Text Editor',
  350. isToggled: () => config.autoClosingBrackets
  351. });
  352. commands.addCommand(CommandIDs.createConsole, {
  353. execute: args => {
  354. const widget = tracker.currentWidget;
  355. if (!widget) {
  356. return;
  357. }
  358. return commands
  359. .execute('console:create', {
  360. activate: args['activate'],
  361. name: widget.context.contentsModel.name,
  362. path: widget.context.path,
  363. preferredLanguage: widget.context.model.defaultKernelLanguage,
  364. ref: widget.id,
  365. insertMode: 'split-bottom'
  366. })
  367. .then(console => {
  368. widget.context.pathChanged.connect((sender, value) => {
  369. console.session.setPath(value);
  370. console.session.setName(widget.context.contentsModel.name);
  371. });
  372. });
  373. },
  374. isEnabled,
  375. label: 'Create Console for Editor'
  376. });
  377. commands.addCommand(CommandIDs.runCode, {
  378. execute: () => {
  379. // Run the appropriate code, taking into account a ```fenced``` code block.
  380. const widget = tracker.currentWidget.content;
  381. if (!widget) {
  382. return;
  383. }
  384. let code = '';
  385. const editor = widget.editor;
  386. const path = widget.context.path;
  387. const extension = PathExt.extname(path);
  388. const selection = editor.getSelection();
  389. const { start, end } = selection;
  390. let selected = start.column !== end.column || start.line !== end.line;
  391. if (selected) {
  392. // Get the selected code from the editor.
  393. const start = editor.getOffsetAt(selection.start);
  394. const end = editor.getOffsetAt(selection.end);
  395. code = editor.model.value.text.substring(start, end);
  396. } else if (MarkdownCodeBlocks.isMarkdown(extension)) {
  397. const { text } = editor.model.value;
  398. const blocks = MarkdownCodeBlocks.findMarkdownCodeBlocks(text);
  399. for (let block of blocks) {
  400. if (block.startLine <= start.line && start.line <= block.endLine) {
  401. code = block.code;
  402. selected = true;
  403. break;
  404. }
  405. }
  406. }
  407. if (!selected) {
  408. // no selection, submit whole line and advance
  409. code = editor.getLine(selection.start.line);
  410. const cursor = editor.getCursorPosition();
  411. if (cursor.line + 1 === editor.lineCount) {
  412. let text = editor.model.value.text;
  413. editor.model.value.text = text + '\n';
  414. }
  415. editor.setCursorPosition({
  416. line: cursor.line + 1,
  417. column: cursor.column
  418. });
  419. }
  420. const activate = false;
  421. if (code) {
  422. return commands.execute('console:inject', { activate, code, path });
  423. } else {
  424. return Promise.resolve(void 0);
  425. }
  426. },
  427. isEnabled,
  428. label: 'Run Code'
  429. });
  430. commands.addCommand(CommandIDs.runAllCode, {
  431. execute: () => {
  432. let widget = tracker.currentWidget.content;
  433. if (!widget) {
  434. return;
  435. }
  436. let code = '';
  437. let editor = widget.editor;
  438. let text = editor.model.value.text;
  439. let path = widget.context.path;
  440. let extension = PathExt.extname(path);
  441. if (MarkdownCodeBlocks.isMarkdown(extension)) {
  442. // For Markdown files, run only code blocks.
  443. const blocks = MarkdownCodeBlocks.findMarkdownCodeBlocks(text);
  444. for (let block of blocks) {
  445. code += block.code;
  446. }
  447. } else {
  448. code = text;
  449. }
  450. const activate = false;
  451. if (code) {
  452. return commands.execute('console:inject', { activate, code, path });
  453. } else {
  454. return Promise.resolve(void 0);
  455. }
  456. },
  457. isEnabled,
  458. label: 'Run All Code'
  459. });
  460. commands.addCommand(CommandIDs.markdownPreview, {
  461. execute: () => {
  462. let widget = tracker.currentWidget;
  463. if (!widget) {
  464. return;
  465. }
  466. let path = widget.context.path;
  467. return commands.execute('markdownviewer:open', {
  468. path,
  469. options: {
  470. mode: 'split-right'
  471. }
  472. });
  473. },
  474. isVisible: () => {
  475. let widget = tracker.currentWidget;
  476. return (
  477. (widget && PathExt.extname(widget.context.path) === '.md') || false
  478. );
  479. },
  480. label: 'Show Markdown Preview'
  481. });
  482. // Function to create a new untitled text file, given
  483. // the current working directory.
  484. const createNew = (cwd: string, ext: string = 'txt') => {
  485. return commands
  486. .execute('docmanager:new-untitled', {
  487. path: cwd,
  488. type: 'file',
  489. ext
  490. })
  491. .then(model => {
  492. return commands.execute('docmanager:open', {
  493. path: model.path,
  494. factory: FACTORY
  495. });
  496. });
  497. };
  498. // Add a command for creating a new text file.
  499. commands.addCommand(CommandIDs.createNew, {
  500. label: args => (args['isPalette'] ? 'New Text File' : 'Text File'),
  501. caption: 'Create a new text file',
  502. iconClass: args => (args['isPalette'] ? '' : EDITOR_ICON_CLASS),
  503. execute: args => {
  504. let cwd = args['cwd'] || browserFactory.defaultBrowser.model.path;
  505. return createNew(cwd as string);
  506. }
  507. });
  508. // Add a command for creating a new Markdown file.
  509. commands.addCommand(CommandIDs.createNewMarkdown, {
  510. label: args => (args['isPalette'] ? 'New Markdown File' : 'Markdown File'),
  511. caption: 'Create a new markdown file',
  512. iconClass: args => (args['isPalette'] ? '' : MARKDOWN_ICON_CLASS),
  513. execute: args => {
  514. let cwd = args['cwd'] || browserFactory.defaultBrowser.model.path;
  515. return createNew(cwd as string, 'md');
  516. }
  517. });
  518. // Add a launcher item if the launcher is available.
  519. if (launcher) {
  520. launcher.add({
  521. command: CommandIDs.createNew,
  522. category: 'Other',
  523. rank: 1
  524. });
  525. launcher.add({
  526. command: CommandIDs.createNewMarkdown,
  527. category: 'Other',
  528. rank: 2
  529. });
  530. }
  531. if (palette) {
  532. const category = 'Text Editor';
  533. let args: JSONObject = {
  534. insertSpaces: false,
  535. size: 4,
  536. name: 'Indent with Tab'
  537. };
  538. let command = 'fileeditor:change-tabs';
  539. palette.addItem({ command, args, category });
  540. for (let size of [1, 2, 4, 8]) {
  541. let args: JSONObject = {
  542. insertSpaces: true,
  543. size,
  544. name: `Spaces: ${size} `
  545. };
  546. palette.addItem({ command, args, category });
  547. }
  548. args = { isPalette: true };
  549. command = CommandIDs.createNew;
  550. palette.addItem({ command, args, category });
  551. args = { isPalette: true };
  552. command = CommandIDs.createNewMarkdown;
  553. palette.addItem({ command, args, category });
  554. args = { name: 'Increase Font Size', delta: 1 };
  555. command = CommandIDs.changeFontSize;
  556. palette.addItem({ command, args, category });
  557. args = { name: 'Decrease Font Size', delta: -1 };
  558. command = CommandIDs.changeFontSize;
  559. palette.addItem({ command, args, category });
  560. }
  561. if (menu) {
  562. // Add the editing commands to the settings menu.
  563. const tabMenu = new Menu({ commands });
  564. tabMenu.title.label = 'Text Editor Indentation';
  565. let args: JSONObject = {
  566. insertSpaces: false,
  567. size: 4,
  568. name: 'Indent with Tab'
  569. };
  570. let command = 'fileeditor:change-tabs';
  571. tabMenu.addItem({ command, args });
  572. for (let size of [1, 2, 4, 8]) {
  573. let args: JSONObject = {
  574. insertSpaces: true,
  575. size,
  576. name: `Spaces: ${size} `
  577. };
  578. tabMenu.addItem({ command, args });
  579. }
  580. menu.settingsMenu.addGroup(
  581. [
  582. {
  583. command: CommandIDs.changeFontSize,
  584. args: { name: 'Increase Text Editor Font Size', delta: +1 }
  585. },
  586. {
  587. command: CommandIDs.changeFontSize,
  588. args: { name: 'Decrease Text Editor Font Size', delta: -1 }
  589. },
  590. { type: 'submenu', submenu: tabMenu },
  591. { command: CommandIDs.autoClosingBrackets }
  592. ],
  593. 30
  594. );
  595. // Add new text file creation to the file menu.
  596. menu.fileMenu.newMenu.addGroup([{ command: CommandIDs.createNew }], 30);
  597. // Add new markdown file creation to the file menu.
  598. menu.fileMenu.newMenu.addGroup(
  599. [{ command: CommandIDs.createNewMarkdown }],
  600. 30
  601. );
  602. // Add undo/redo hooks to the edit menu.
  603. menu.editMenu.undoers.add({
  604. tracker,
  605. undo: widget => {
  606. widget.content.editor.undo();
  607. },
  608. redo: widget => {
  609. widget.content.editor.redo();
  610. }
  611. } as IEditMenu.IUndoer<IDocumentWidget<FileEditor>>);
  612. // Add editor view options.
  613. menu.viewMenu.editorViewers.add({
  614. tracker,
  615. toggleLineNumbers: widget => {
  616. const lineNumbers = !widget.content.editor.getOption('lineNumbers');
  617. widget.content.editor.setOption('lineNumbers', lineNumbers);
  618. },
  619. toggleWordWrap: widget => {
  620. const oldValue = widget.content.editor.getOption('lineWrap');
  621. const newValue = oldValue === 'off' ? 'on' : 'off';
  622. widget.content.editor.setOption('lineWrap', newValue);
  623. },
  624. toggleMatchBrackets: widget => {
  625. const matchBrackets = !widget.content.editor.getOption('matchBrackets');
  626. widget.content.editor.setOption('matchBrackets', matchBrackets);
  627. },
  628. lineNumbersToggled: widget =>
  629. widget.content.editor.getOption('lineNumbers'),
  630. wordWrapToggled: widget =>
  631. widget.content.editor.getOption('lineWrap') !== 'off',
  632. matchBracketsToggled: widget =>
  633. widget.content.editor.getOption('matchBrackets')
  634. } as IViewMenu.IEditorViewer<IDocumentWidget<FileEditor>>);
  635. // Add a console creator the the Kernel menu.
  636. menu.fileMenu.consoleCreators.add({
  637. tracker,
  638. name: 'Editor',
  639. createConsole: current => {
  640. const options = {
  641. path: current.context.path,
  642. preferredLanguage: current.context.model.defaultKernelLanguage
  643. };
  644. return commands.execute('console:create', options);
  645. }
  646. } as IFileMenu.IConsoleCreator<IDocumentWidget<FileEditor>>);
  647. // Add a code runner to the Run menu.
  648. menu.runMenu.codeRunners.add({
  649. tracker,
  650. noun: 'Code',
  651. isEnabled: current => {
  652. let found = false;
  653. consoleTracker.forEach(console => {
  654. if (console.console.session.path === current.context.path) {
  655. found = true;
  656. }
  657. });
  658. return found;
  659. },
  660. run: () => commands.execute(CommandIDs.runCode),
  661. runAll: () => commands.execute(CommandIDs.runAllCode),
  662. restartAndRunAll: current => {
  663. return current.context.session.restart().then(restarted => {
  664. if (restarted) {
  665. commands.execute(CommandIDs.runAllCode);
  666. }
  667. return restarted;
  668. });
  669. }
  670. } as IRunMenu.ICodeRunner<IDocumentWidget<FileEditor>>);
  671. }
  672. app.contextMenu.addItem({
  673. command: CommandIDs.createConsole,
  674. selector: '.jp-FileEditor'
  675. });
  676. app.contextMenu.addItem({
  677. command: CommandIDs.markdownPreview,
  678. selector: '.jp-FileEditor'
  679. });
  680. return tracker;
  681. }