index.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. JSONObject
  5. } from '@phosphor/coreutils';
  6. import {
  7. Menu
  8. } from '@phosphor/widgets';
  9. import {
  10. JupyterLab, JupyterLabPlugin
  11. } from '@jupyterlab/application';
  12. import {
  13. ICommandPalette
  14. } from '@jupyterlab/apputils';
  15. import {
  16. IMainMenu, IEditMenu
  17. } from '@jupyterlab/mainmenu';
  18. import {
  19. IEditorServices
  20. } from '@jupyterlab/codeeditor';
  21. import {
  22. editorServices, CodeMirrorEditor, Mode
  23. } from '@jupyterlab/codemirror';
  24. import {
  25. ISettingRegistry, IStateDB
  26. } from '@jupyterlab/coreutils';
  27. import {
  28. IEditorTracker, FileEditor
  29. } from '@jupyterlab/fileeditor';
  30. /**
  31. * The command IDs used by the codemirror plugin.
  32. */
  33. namespace CommandIDs {
  34. export
  35. const changeKeyMap = 'codemirror:change-keymap';
  36. export
  37. const changeTheme = 'codemirror:change-theme';
  38. export
  39. const changeMode = 'codemirror:change-mode';
  40. export
  41. const find = 'codemirror:find';
  42. export
  43. const findAndReplace = 'codemirror:find-and-replace';
  44. }
  45. /**
  46. * The editor services.
  47. */
  48. const services: JupyterLabPlugin<IEditorServices> = {
  49. id: '@jupyterlab/codemirror-extension:services',
  50. provides: IEditorServices,
  51. activate: (): IEditorServices => editorServices
  52. };
  53. /**
  54. * The editor commands.
  55. */
  56. const commands: JupyterLabPlugin<void> = {
  57. id: '@jupyterlab/codemirror-extension:commands',
  58. requires: [
  59. IEditorTracker,
  60. IMainMenu,
  61. ICommandPalette,
  62. IStateDB,
  63. ISettingRegistry
  64. ],
  65. activate: activateEditorCommands,
  66. autoStart: true
  67. };
  68. /**
  69. * Export the plugins as default.
  70. */
  71. const plugins: JupyterLabPlugin<any>[] = [commands, services];
  72. export default plugins;
  73. /**
  74. * The plugin ID used as the key in the setting registry.
  75. */
  76. const id = commands.id;
  77. /**
  78. * Set up the editor widget menu and commands.
  79. */
  80. function activateEditorCommands(app: JupyterLab, tracker: IEditorTracker, mainMenu: IMainMenu, palette: ICommandPalette, state: IStateDB, settingRegistry: ISettingRegistry): void {
  81. const { commands, restored } = app;
  82. let { theme, keyMap } = CodeMirrorEditor.defaultConfig;
  83. /**
  84. * Update the setting values.
  85. */
  86. function updateSettings(settings: ISettingRegistry.ISettings): void {
  87. keyMap = settings.get('keyMap').composite as string | null || keyMap;
  88. theme = settings.get('theme').composite as string | null || theme;
  89. }
  90. /**
  91. * Update the settings of the current tracker instances.
  92. */
  93. function updateTracker(): void {
  94. tracker.forEach(widget => {
  95. if (widget.editor instanceof CodeMirrorEditor) {
  96. let cm = widget.editor.editor;
  97. cm.setOption('keyMap', keyMap);
  98. cm.setOption('theme', theme);
  99. }
  100. });
  101. }
  102. // Fetch the initial state of the settings.
  103. Promise.all([settingRegistry.load(id), restored]).then(([settings]) => {
  104. updateSettings(settings);
  105. updateTracker();
  106. settings.changed.connect(() => {
  107. updateSettings(settings);
  108. updateTracker();
  109. });
  110. }).catch((reason: Error) => {
  111. console.error(reason.message);
  112. updateTracker();
  113. });
  114. /**
  115. * Handle the settings of new widgets.
  116. */
  117. tracker.widgetAdded.connect((sender, widget) => {
  118. if (widget.editor instanceof CodeMirrorEditor) {
  119. let cm = widget.editor.editor;
  120. cm.setOption('keyMap', keyMap);
  121. cm.setOption('theme', theme);
  122. }
  123. });
  124. /**
  125. * A test for whether the tracker has an active widget.
  126. */
  127. function isEnabled(): boolean {
  128. return tracker.currentWidget !== null &&
  129. tracker.currentWidget === app.shell.currentWidget;
  130. }
  131. /**
  132. * Create a menu for the editor.
  133. */
  134. function createMenu(): Menu {
  135. const menu = new Menu({ commands });
  136. const themeMenu = new Menu({ commands });
  137. const keyMapMenu = new Menu({ commands });
  138. const modeMenu = new Menu({ commands });
  139. const tabMenu = new Menu({ commands });
  140. menu.title.label = 'Editor';
  141. themeMenu.title.label = 'Theme';
  142. keyMapMenu.title.label = 'Key Map';
  143. modeMenu.title.label = 'Language';
  144. tabMenu.title.label = 'Tabs';
  145. commands.addCommand(CommandIDs.changeTheme, {
  146. label: args => args['theme'] as string,
  147. execute: args => {
  148. const key = 'theme';
  149. const value = theme = args['theme'] as string || theme;
  150. updateTracker();
  151. return settingRegistry.set(id, key, value).catch((reason: Error) => {
  152. console.error(`Failed to set ${id}:${key} - ${reason.message}`);
  153. });
  154. },
  155. isEnabled,
  156. isToggled: args => args['theme'] === theme
  157. });
  158. commands.addCommand(CommandIDs.changeKeyMap, {
  159. label: args => {
  160. let title = args['keyMap'] as string;
  161. return title === 'sublime' ? 'Sublime Text' : title;
  162. },
  163. execute: args => {
  164. const key = 'keyMap';
  165. const value = keyMap = args['keyMap'] as string || keyMap;
  166. updateTracker();
  167. return settingRegistry.set(id, key, value).catch((reason: Error) => {
  168. console.error(`Failed to set ${id}:${key} - ${reason.message}`);
  169. });
  170. },
  171. isEnabled,
  172. isToggled: args => args['keyMap'] === keyMap
  173. });
  174. commands.addCommand(CommandIDs.find, {
  175. label: 'Find...',
  176. execute: () => {
  177. let widget = tracker.currentWidget;
  178. if (!widget) {
  179. return;
  180. }
  181. let editor = widget.editor as CodeMirrorEditor;
  182. editor.execCommand('find');
  183. },
  184. isEnabled
  185. });
  186. commands.addCommand(CommandIDs.findAndReplace, {
  187. label: 'Find & Replace...',
  188. execute: () => {
  189. let widget = tracker.currentWidget;
  190. if (!widget) {
  191. return;
  192. }
  193. let editor = widget.editor as CodeMirrorEditor;
  194. editor.execCommand('replace');
  195. },
  196. isEnabled
  197. });
  198. commands.addCommand(CommandIDs.changeMode, {
  199. label: args => args['name'] as string,
  200. execute: args => {
  201. let name = args['name'] as string;
  202. let widget = tracker.currentWidget;
  203. if (name && widget) {
  204. let spec = Mode.findByName(name);
  205. if (spec) {
  206. widget.model.mimeType = spec.mime;
  207. }
  208. }
  209. },
  210. isEnabled,
  211. isToggled: args => {
  212. let widget = tracker.currentWidget;
  213. if (!widget) {
  214. return false;
  215. }
  216. let mime = widget.model.mimeType;
  217. let spec = Mode.findByMIME(mime);
  218. let name = spec && spec.name;
  219. return args['name'] === name;
  220. }
  221. });
  222. Mode.getModeInfo().sort((a, b) => {
  223. let aName = a.name || '';
  224. let bName = b.name || '';
  225. return aName.localeCompare(bName);
  226. }).forEach(spec => {
  227. // Avoid mode name with a curse word.
  228. if (spec.mode.indexOf('brainf') === 0) {
  229. return;
  230. }
  231. modeMenu.addItem({
  232. command: CommandIDs.changeMode,
  233. args: {...spec}
  234. });
  235. });
  236. [
  237. 'jupyter', 'default', 'abcdef', 'base16-dark', 'base16-light',
  238. 'hopscotch', 'material', 'mbo', 'mdn-like', 'seti', 'the-matrix',
  239. 'xq-light', 'zenburn'
  240. ].forEach(name => themeMenu.addItem({
  241. command: CommandIDs.changeTheme,
  242. args: { theme: name }
  243. }));
  244. ['default', 'sublime', 'vim', 'emacs'].forEach(name => {
  245. keyMapMenu.addItem({
  246. command: CommandIDs.changeKeyMap,
  247. args: { keyMap: name }
  248. });
  249. });
  250. let args: JSONObject = {
  251. insertSpaces: false, size: 4, name: 'Indent with Tab'
  252. };
  253. let command = 'fileeditor:change-tabs';
  254. tabMenu.addItem({ command, args });
  255. palette.addItem({ command, args, category: 'Editor' });
  256. for (let size of [1, 2, 4, 8]) {
  257. let args: JSONObject = {
  258. insertSpaces: true, size, name: `Spaces: ${size} `
  259. };
  260. tabMenu.addItem({ command, args });
  261. palette.addItem({ command, args, category: 'Editor' });
  262. }
  263. menu.addItem({ command: 'fileeditor:toggle-autoclosing-brackets' });
  264. menu.addItem({ type: 'submenu', submenu: tabMenu });
  265. menu.addItem({ type: 'separator' });
  266. menu.addItem({ type: 'submenu', submenu: modeMenu });
  267. menu.addItem({ type: 'submenu', submenu: keyMapMenu });
  268. menu.addItem({ type: 'submenu', submenu: themeMenu });
  269. return menu;
  270. }
  271. mainMenu.addMenu(createMenu(), { rank: 30 });
  272. // Add find-replace capabilities to the edit menu.
  273. mainMenu.editMenu.findReplacers.set('Editor', {
  274. tracker,
  275. find: (widget: FileEditor) => {
  276. let editor = widget.editor as CodeMirrorEditor;
  277. editor.execCommand('find');
  278. },
  279. findAndReplace: (widget: FileEditor) => {
  280. let editor = widget.editor as CodeMirrorEditor;
  281. editor.execCommand('find');
  282. }
  283. } as IEditMenu.IFindReplacer<FileEditor>)
  284. }