index.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. JupyterLab, JupyterLabPlugin
  5. } from '@jupyterlab/application';
  6. import {
  7. ILayoutRestorer, InstanceTracker
  8. } from '@jupyterlab/apputils';
  9. import {
  10. ISettingRegistry
  11. } from '@jupyterlab/coreutils';
  12. import {
  13. IEditorServices
  14. } from '@jupyterlab/codeeditor';
  15. import {
  16.  MarkdownCodeBlocks, PathExt
  17. } from '@jupyterlab/coreutils';
  18. import {
  19. IDocumentRegistry
  20. } from '@jupyterlab/docregistry';
  21. import {
  22. FileEditor, FileEditorFactory, IEditorTracker
  23. } from '@jupyterlab/fileeditor';
  24. import {
  25. ILauncher
  26. } from '@jupyterlab/launcher';
  27. /**
  28. * The class name for the text editor icon from the default theme.
  29. */
  30. const EDITOR_ICON_CLASS = 'jp-ImageTextEditor';
  31. /**
  32. * The name of the factory that creates editor widgets.
  33. */
  34. const FACTORY = 'Editor';
  35. /**
  36. * The command IDs used by the fileeditor plugin.
  37. */
  38. namespace CommandIDs {
  39. export
  40. const lineNumbers = 'editor:line-numbers';
  41. export
  42. const wordWrap = 'editor:word-wrap';
  43. export
  44. const createConsole = 'editor:create-console';
  45. export
  46. const runCode = 'editor:run-code';
  47. export
  48. const markdownPreview = 'editor:markdown-preview';
  49. };
  50. /**
  51. * The editor tracker extension.
  52. */
  53. const plugin: JupyterLabPlugin<IEditorTracker> = {
  54. activate,
  55. id: 'jupyter.services.editor-tracker',
  56. requires: [IDocumentRegistry, ILayoutRestorer, IEditorServices, ISettingRegistry],
  57. optional: [ILauncher],
  58. provides: IEditorTracker,
  59. autoStart: true
  60. };
  61. /**
  62. * Export the plugins as default.
  63. */
  64. export default plugin;
  65. /**
  66. * Activate the editor tracker plugin.
  67. */
  68. function activate(app: JupyterLab, registry: IDocumentRegistry, restorer: ILayoutRestorer, editorServices: IEditorServices, settingRegistry: ISettingRegistry, launcher: ILauncher | null): IEditorTracker {
  69. const id = plugin.id;
  70. const namespace = 'editor';
  71. const factory = new FileEditorFactory({
  72. editorServices,
  73. factoryOptions: { name: FACTORY, fileExtensions: ['*'], defaultFor: ['*'] }
  74. });
  75. const { commands, restored } = app;
  76. const tracker = new InstanceTracker<FileEditor>({ namespace });
  77. const hasWidget = () => tracker.currentWidget !== null;
  78. let lineNumbers = true;
  79. let wordWrap = true;
  80. // Handle state restoration.
  81. restorer.restore(tracker, {
  82. command: 'file-operations:open',
  83. args: widget => ({ path: widget.context.path, factory: FACTORY }),
  84. name: widget => widget.context.path
  85. });
  86. /**
  87. * Update the setting values.
  88. */
  89. function updateSettings(settings: ISettingRegistry.ISettings): void {
  90. let cached = settings.get('lineNumbers') as boolean | null;
  91. lineNumbers = cached === null ? false : !!cached;
  92. cached = settings.get('wordWrap') as boolean | null;
  93. wordWrap = cached === null ? false : !!cached;
  94. }
  95. /**
  96. * Update the settings of the current tracker instances.
  97. */
  98. function updateTracker(): void {
  99. tracker.forEach(widget => {
  100. widget.editor.lineNumbers = lineNumbers;
  101. widget.editor.wordWrap = wordWrap;
  102. });
  103. }
  104. // Fetch the initial state of the settings.
  105. Promise.all([settingRegistry.load(id), restored]).then(([settings]) => {
  106. updateSettings(settings);
  107. updateTracker();
  108. settings.changed.connect(() => {
  109. updateSettings(settings);
  110. updateTracker();
  111. });
  112. });
  113. factory.widgetCreated.connect((sender, widget) => {
  114. widget.title.icon = EDITOR_ICON_CLASS;
  115. // Notify the instance tracker if restore data needs to update.
  116. widget.context.pathChanged.connect(() => { tracker.save(widget); });
  117. tracker.add(widget);
  118. widget.editor.lineNumbers = lineNumbers;
  119. widget.editor.wordWrap = wordWrap;
  120. });
  121. registry.addWidgetFactory(factory);
  122. // Handle the settings of new widgets.
  123. tracker.widgetAdded.connect((sender, widget) => {
  124. const editor = widget.editor;
  125. editor.lineNumbers = lineNumbers;
  126. editor.wordWrap = wordWrap;
  127. });
  128. commands.addCommand(CommandIDs.lineNumbers, {
  129. execute: () => {
  130. lineNumbers = !lineNumbers;
  131. tracker.forEach(widget => { widget.editor.lineNumbers = lineNumbers; });
  132. return settingRegistry.set(id, 'lineNumbers', lineNumbers);
  133. },
  134. isEnabled: hasWidget,
  135. isToggled: () => lineNumbers,
  136. label: 'Line Numbers'
  137. });
  138. commands.addCommand(CommandIDs.wordWrap, {
  139. execute: () => {
  140. wordWrap = !wordWrap;
  141. tracker.forEach(widget => { widget.editor.wordWrap = wordWrap; });
  142. return settingRegistry.set(id, 'wordWrap', wordWrap);
  143. },
  144. isEnabled: hasWidget,
  145. isToggled: () => wordWrap,
  146. label: 'Word Wrap'
  147. });
  148. commands.addCommand(CommandIDs.createConsole, {
  149. execute: args => {
  150. const widget = tracker.currentWidget;
  151. if (!widget) {
  152. return;
  153. }
  154. return commands.execute('console:create', {
  155. activate: args['activate'],
  156. path: widget.context.path,
  157. preferredLanguage: widget.context.model.defaultKernelLanguage
  158. });
  159. },
  160. isEnabled: hasWidget,
  161. label: 'Create Console for Editor'
  162. });
  163. commands.addCommand(CommandIDs.runCode, {
  164. execute: () => {
  165. const widget = tracker.currentWidget;
  166. if (!widget) {
  167. return;
  168. }
  169. let code = '';
  170. const editor = widget.editor;
  171. const path = widget.context.path;
  172. const extension = PathExt.extname(path);
  173. const selection = editor.getSelection();
  174. const { start, end } = selection;
  175. const selected = start.column !== end.column || start.line !== end.line;
  176. if (selected) {
  177. // Get the selected code from the editor.
  178. const start = editor.getOffsetAt(selection.start);
  179. const end = editor.getOffsetAt(selection.end);
  180. code = editor.model.value.text.substring(start, end);
  181. if (start === end) {
  182. code = editor.getLine(selection.start.line);
  183. }
  184. } else if (MarkdownCodeBlocks.isMarkdown(extension)) {
  185. const { text } = editor.model.value;
  186. const blocks = MarkdownCodeBlocks.findMarkdownCodeBlocks(text);
  187. for (let block of blocks) {
  188. if (block.startLine <= start.line && start.line <= block.endLine) {
  189. code = block.code;
  190. break;
  191. }
  192. }
  193. }
  194. const { column, line } = editor.getCursorPosition();
  195. const activate = false;
  196. // Advance cursor to the next line.
  197. if (line + 1 === editor.lineCount) {
  198. const text = editor.model.value.text;
  199. editor.model.value.text = text + '\n';
  200. }
  201. editor.setCursorPosition({ column, line: line + 1 });
  202. return commands.execute('console:inject', { activate, code, path });
  203. },
  204. isEnabled: hasWidget,
  205. label: 'Run Code'
  206. });
  207. commands.addCommand(CommandIDs.markdownPreview, {
  208. execute: () => {
  209. let path = tracker.currentWidget.context.path;
  210. return commands.execute('markdown-preview:open', { path });
  211. },
  212. isVisible: () => {
  213. let widget = tracker.currentWidget;
  214. return widget && PathExt.extname(widget.context.path) === '.md';
  215. },
  216. label: 'Show Markdown Preview'
  217. });
  218. // Add a launcher item if the launcher is available.
  219. if (launcher) {
  220. launcher.add({
  221. args: { creatorName: 'Text File' },
  222. command: 'file-operations:create-from',
  223. name: 'Text Editor'
  224. });
  225. }
  226. app.contextMenu.addItem({
  227. command: CommandIDs.createConsole, selector: '.jp-FileEditor'
  228. });
  229. app.contextMenu.addItem({
  230. command: CommandIDs.markdownPreview, selector: '.jp-FileEditor'
  231. });
  232. return tracker;
  233. }