index.ts 8.7 KB

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