plugin.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. AttachedProperty
  5. } from 'phosphor/lib/core/properties';
  6. import {
  7. Menu
  8. } from 'phosphor/lib/ui/menu';
  9. import {
  10. JupyterLab, JupyterLabPlugin
  11. } from '../application';
  12. import {
  13. InstanceTracker
  14. } from '../common/instancetracker';
  15. import {
  16. IDocumentRegistry
  17. } from '../docregistry';
  18. import {
  19. EditorWidgetFactory, EditorWidget
  20. } from './widget';
  21. import {
  22. ICommandPalette
  23. } from '../commandpalette';
  24. import {
  25. IMainMenu
  26. } from '../mainmenu';
  27. import {
  28. IStateDB
  29. } from '../statedb';
  30. import {
  31. IEditorTracker
  32. } from './index';
  33. import {
  34. DEFAULT_CODEMIRROR_THEME
  35. } from '../codemirror/widget';
  36. import 'codemirror/addon/edit/matchbrackets.js';
  37. import 'codemirror/addon/edit/closebrackets.js';
  38. import 'codemirror/addon/comment/comment.js';
  39. import 'codemirror/keymap/vim.js';
  40. /**
  41. * The class name for all main area portrait tab icons.
  42. */
  43. const PORTRAIT_ICON_CLASS = 'jp-MainAreaPortraitIcon';
  44. /**
  45. * The class name for the text editor icon from the default theme.
  46. */
  47. const EDITOR_ICON_CLASS = 'jp-ImageTextEditor';
  48. /**
  49. * The state database namespace for editor widgets.
  50. */
  51. const NAMESPACE = 'editorwidgets';
  52. /**
  53. * The map of command ids used by the editor.
  54. */
  55. const cmdIds = {
  56. lineNumbers: 'editor:line-numbers',
  57. lineWrap: 'editor:line-wrap',
  58. matchBrackets: 'editor:match-brackets',
  59. vimMode: 'editor:vim-mode',
  60. closeAll: 'editor:close-all',
  61. changeTheme: 'editor:change-theme',
  62. createConsole: 'editor:create-console',
  63. runCode: 'editor:run-code'
  64. };
  65. /**
  66. * The editor widget instance tracker.
  67. */
  68. const tracker = new InstanceTracker<EditorWidget>();
  69. /**
  70. * The editor handler extension.
  71. */
  72. export
  73. const editorHandlerProvider: JupyterLabPlugin<IEditorTracker> = {
  74. id: 'jupyter.services.editor-handler',
  75. requires: [IDocumentRegistry, IMainMenu, ICommandPalette, IStateDB],
  76. provides: IEditorTracker,
  77. activate: activateEditorHandler,
  78. autoStart: true
  79. };
  80. /**
  81. * Sets up the editor widget
  82. */
  83. function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, mainMenu: IMainMenu, palette: ICommandPalette, state: IStateDB): IEditorTracker {
  84. let widgetFactory = new EditorWidgetFactory({
  85. name: 'Editor',
  86. fileExtensions: ['*'],
  87. defaultFor: ['*']
  88. });
  89. // Sync tracker with currently focused widget.
  90. app.shell.currentChanged.connect((sender, args) => {
  91. tracker.sync(args.newValue);
  92. });
  93. widgetFactory.widgetCreated.connect((sender, widget) => {
  94. widget.title.icon = `${PORTRAIT_ICON_CLASS} ${EDITOR_ICON_CLASS}`;
  95. // Add the file path to the state database.
  96. let key = `${NAMESPACE}:${widget.context.path}`;
  97. state.save(key, { path: widget.context.path });
  98. // Remove the file path from the state database on disposal.
  99. widget.disposed.connect(() => { state.remove(key); });
  100. // Keep track of path changes in the state database.
  101. widget.context.pathChanged.connect((sender, path) => {
  102. state.remove(key);
  103. key = `${NAMESPACE}:${path}`;
  104. state.save(key, { path });
  105. });
  106. tracker.add(widget);
  107. });
  108. registry.addWidgetFactory(widgetFactory);
  109. mainMenu.addMenu(createMenu(app), {rank: 30});
  110. let commands = app.commands;
  111. commands.addCommand(cmdIds.lineNumbers, {
  112. execute: () => { toggleLineNums(); },
  113. label: 'Toggle Line Numbers',
  114. });
  115. commands.addCommand(cmdIds.lineWrap, {
  116. execute: () => { toggleLineWrap(); },
  117. label: 'Toggle Line Wrap',
  118. });
  119. commands.addCommand(cmdIds.matchBrackets, {
  120. execute: () => { toggleMatchBrackets(); },
  121. label: 'Toggle Match Brackets',
  122. });
  123. commands.addCommand(cmdIds.vimMode, {
  124. execute: () => { toggleVim(); },
  125. label: 'Toggle Vim Mode'
  126. });
  127. commands.addCommand(cmdIds.closeAll, {
  128. execute: () => { closeAllFiles(); },
  129. label: 'Close all files'
  130. });
  131. commands.addCommand(cmdIds.createConsole, {
  132. execute: () => {
  133. let widget = tracker.currentWidget;
  134. if (!widget) {
  135. return;
  136. }
  137. let options: any = {
  138. path: widget.context.path,
  139. preferredLanguage: widget.context.model.defaultKernelLanguage
  140. };
  141. commands.execute('console:create', options).then(id => {
  142. sessionIdProperty.set(widget, id);
  143. });
  144. },
  145. label: 'Create Console for Editor'
  146. });
  147. commands.addCommand(cmdIds.runCode, {
  148. execute: () => {
  149. let widget = tracker.currentWidget;
  150. if (!widget) {
  151. return;
  152. }
  153. // Get the session id.
  154. let id = sessionIdProperty.get(widget);
  155. if (!id) {
  156. return;
  157. }
  158. // Get the selected code from the editor.
  159. let doc = widget.editor.getDoc();
  160. let code = doc.getSelection();
  161. if (!code) {
  162. let { line } = doc.getCursor();
  163. code = doc.getLine(line);
  164. }
  165. commands.execute('console:inject', { id, code });
  166. },
  167. label: 'Run Code',
  168. });
  169. [
  170. cmdIds.lineNumbers,
  171. cmdIds.lineWrap,
  172. cmdIds.matchBrackets,
  173. cmdIds.vimMode,
  174. cmdIds.closeAll,
  175. cmdIds.createConsole,
  176. cmdIds.runCode,
  177. ].forEach(command => palette.addItem({ command, category: 'Editor' }));
  178. // Reload any editor widgets whose state has been stored.
  179. Promise.all([state.fetchNamespace(NAMESPACE), app.started]).then(([coll]) => {
  180. let { values } = coll;
  181. let open = 'file-operations:open';
  182. values.forEach(args => { app.commands.execute(open, args); });
  183. });
  184. return tracker;
  185. }
  186. /**
  187. * An attached property for the session id associated with an editor widget.
  188. */
  189. const sessionIdProperty = new AttachedProperty<EditorWidget, string>({ name: 'sessionId' });
  190. /**
  191. * Toggle editor line numbers
  192. */
  193. function toggleLineNums() {
  194. if (tracker.currentWidget) {
  195. let editor = tracker.currentWidget.editor;
  196. editor.setOption('lineNumbers', !editor.getOption('lineNumbers'));
  197. }
  198. }
  199. /**
  200. * Toggle editor line wrap
  201. */
  202. function toggleLineWrap() {
  203. if (tracker.currentWidget) {
  204. let editor = tracker.currentWidget.editor;
  205. editor.setOption('lineWrapping', !editor.getOption('lineWrapping'));
  206. }
  207. }
  208. /**
  209. * Toggle editor matching brackets
  210. */
  211. function toggleMatchBrackets() {
  212. if (tracker.currentWidget) {
  213. let editor = tracker.currentWidget.editor;
  214. editor.setOption('matchBrackets', !editor.getOption('matchBrackets'));
  215. }
  216. }
  217. /**
  218. * Toggle the editor's vim mode
  219. */
  220. function toggleVim() {
  221. tracker.forEach(widget => {
  222. let keymap = widget.editor.getOption('keyMap') === 'vim' ? 'default'
  223. : 'vim';
  224. widget.editor.setOption('keyMap', keymap);
  225. });
  226. }
  227. /**
  228. * Close all currently open text editor files
  229. */
  230. function closeAllFiles() {
  231. tracker.forEach(widget => { widget.close(); });
  232. }
  233. /**
  234. * Create a menu for the editor.
  235. */
  236. function createMenu(app: JupyterLab): Menu {
  237. let { commands, keymap } = app;
  238. let settings = new Menu({ commands, keymap });
  239. let theme = new Menu({ commands, keymap });
  240. let menu = new Menu({ commands, keymap });
  241. menu.title.label = 'Editor';
  242. settings.title.label = 'Settings';
  243. theme.title.label = 'Theme';
  244. settings.addItem({ command: cmdIds.lineNumbers });
  245. settings.addItem({ command: cmdIds.lineWrap });
  246. settings.addItem({ command: cmdIds.matchBrackets });
  247. settings.addItem({ command: cmdIds.vimMode });
  248. commands.addCommand(cmdIds.changeTheme, {
  249. label: args => {
  250. return args['theme'] as string;
  251. },
  252. execute: args => {
  253. let name: string = args['theme'] as string || DEFAULT_CODEMIRROR_THEME;
  254. tracker.forEach(widget => { widget.editor.setOption('theme', name); });
  255. }
  256. });
  257. [
  258. 'jupyter', 'default', 'abcdef', 'base16-dark', 'base16-light',
  259. 'hopscotch', 'material', 'mbo', 'mdn-like', 'seti', 'the-matrix',
  260. 'xq-light', 'zenburn'
  261. ].forEach(name => theme.addItem({
  262. command: 'editor:change-theme',
  263. args: { theme: name }
  264. }));
  265. menu.addItem({ command: cmdIds.closeAll });
  266. menu.addItem({ type: 'separator' });
  267. menu.addItem({ type: 'submenu', menu: settings });
  268. menu.addItem({ type: 'submenu', menu: theme });
  269. return menu;
  270. }