index.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import CodeMirror from 'codemirror';
  4. import { Menu } from '@lumino/widgets';
  5. import {
  6. ILabShell,
  7. JupyterFrontEnd,
  8. JupyterFrontEndPlugin
  9. } from '@jupyterlab/application';
  10. import { IEditMenu, IMainMenu } from '@jupyterlab/mainmenu';
  11. import { IEditorServices } from '@jupyterlab/codeeditor';
  12. import {
  13. editorServices,
  14. EditorSyntaxStatus,
  15. CodeMirrorEditor,
  16. Mode,
  17. ICodeMirror
  18. } from '@jupyterlab/codemirror';
  19. import { IDocumentWidget } from '@jupyterlab/docregistry';
  20. import { IEditorTracker, FileEditor } from '@jupyterlab/fileeditor';
  21. import { ISettingRegistry } from '@jupyterlab/settingregistry';
  22. import { IStatusBar } from '@jupyterlab/statusbar';
  23. import { ITranslator } from '@jupyterlab/translation';
  24. /**
  25. * The command IDs used by the codemirror plugin.
  26. */
  27. namespace CommandIDs {
  28. export const changeKeyMap = 'codemirror:change-keymap';
  29. export const changeTheme = 'codemirror:change-theme';
  30. export const changeMode = 'codemirror:change-mode';
  31. export const find = 'codemirror:find';
  32. export const goToLine = 'codemirror:go-to-line';
  33. }
  34. /** The CodeMirror singleton. */
  35. const codemirrorSingleton: JupyterFrontEndPlugin<ICodeMirror> = {
  36. id: '@jupyterlab/codemirror-extension:codemirror',
  37. provides: ICodeMirror,
  38. activate: activateCodeMirror
  39. };
  40. /**
  41. * The editor services.
  42. */
  43. const services: JupyterFrontEndPlugin<IEditorServices> = {
  44. id: '@jupyterlab/codemirror-extension:services',
  45. provides: IEditorServices,
  46. activate: activateEditorServices
  47. };
  48. /**
  49. * The editor commands.
  50. */
  51. const commands: JupyterFrontEndPlugin<void> = {
  52. id: '@jupyterlab/codemirror-extension:commands',
  53. requires: [IEditorTracker, ISettingRegistry, ITranslator, ICodeMirror],
  54. optional: [IMainMenu],
  55. activate: activateEditorCommands,
  56. autoStart: true
  57. };
  58. /**
  59. * The JupyterLab plugin for the EditorSyntax status item.
  60. */
  61. export const editorSyntaxStatus: JupyterFrontEndPlugin<void> = {
  62. id: '@jupyterlab/codemirror-extension:editor-syntax-status',
  63. autoStart: true,
  64. requires: [IEditorTracker, ILabShell, ITranslator],
  65. optional: [IStatusBar],
  66. activate: (
  67. app: JupyterFrontEnd,
  68. tracker: IEditorTracker,
  69. labShell: ILabShell,
  70. translator: ITranslator,
  71. statusBar: IStatusBar | null
  72. ) => {
  73. if (!statusBar) {
  74. // Automatically disable if statusbar missing
  75. return;
  76. }
  77. const item = new EditorSyntaxStatus({ commands: app.commands, translator });
  78. labShell.currentChanged.connect(() => {
  79. const current = labShell.currentWidget;
  80. if (current && tracker.has(current) && item.model) {
  81. item.model.editor = (current as IDocumentWidget<
  82. FileEditor
  83. >).content.editor;
  84. }
  85. });
  86. statusBar.registerStatusItem(
  87. '@jupyterlab/codemirror-extension:editor-syntax-status',
  88. {
  89. item,
  90. align: 'left',
  91. rank: 0,
  92. isActive: () =>
  93. !!labShell.currentWidget &&
  94. !!tracker.currentWidget &&
  95. labShell.currentWidget === tracker.currentWidget
  96. }
  97. );
  98. }
  99. };
  100. /**
  101. * Export the plugins as default.
  102. */
  103. const plugins: JupyterFrontEndPlugin<any>[] = [
  104. commands,
  105. services,
  106. editorSyntaxStatus,
  107. codemirrorSingleton
  108. ];
  109. export default plugins;
  110. /**
  111. * The plugin ID used as the key in the setting registry.
  112. */
  113. const id = commands.id;
  114. /**
  115. * Set up the editor services.
  116. */
  117. function activateEditorServices(app: JupyterFrontEnd): IEditorServices {
  118. CodeMirror.prototype.save = () => {
  119. void app.commands.execute('docmanager:save');
  120. };
  121. return editorServices;
  122. }
  123. /**
  124. * Simplest implementation of the CodeMirror singleton provider.
  125. */
  126. class CodeMirrorSingleton implements ICodeMirror {
  127. get CodeMirror() {
  128. return CodeMirror;
  129. }
  130. async ensureVimKeymap() {
  131. if (!('Vim' in (CodeMirror as any))) {
  132. // @ts-expect-error
  133. await import('codemirror/keymap/vim.js');
  134. }
  135. }
  136. }
  137. /**
  138. * Set up the CodeMirror singleton.
  139. */
  140. function activateCodeMirror(app: JupyterFrontEnd): ICodeMirror {
  141. return new CodeMirrorSingleton();
  142. }
  143. /**
  144. * Set up the editor widget menu and commands.
  145. */
  146. function activateEditorCommands(
  147. app: JupyterFrontEnd,
  148. tracker: IEditorTracker,
  149. settingRegistry: ISettingRegistry,
  150. translator: ITranslator,
  151. codeMirror: ICodeMirror,
  152. mainMenu: IMainMenu | null
  153. ): void {
  154. const trans = translator.load('jupyterlab');
  155. const { commands, restored } = app;
  156. let {
  157. theme,
  158. keyMap,
  159. scrollPastEnd,
  160. styleActiveLine,
  161. styleSelectedText,
  162. selectionPointer,
  163. lineWiseCopyCut
  164. } = CodeMirrorEditor.defaultConfig;
  165. /**
  166. * Update the setting values.
  167. */
  168. async function updateSettings(
  169. settings: ISettingRegistry.ISettings
  170. ): Promise<void> {
  171. keyMap = (settings.get('keyMap').composite as string | null) || keyMap;
  172. // Lazy loading of vim mode
  173. if (keyMap === 'vim') {
  174. await codeMirror.ensureVimKeymap();
  175. }
  176. theme = (settings.get('theme').composite as string | null) || theme;
  177. // Lazy loading of theme stylesheets
  178. if (theme !== 'jupyter' && theme !== 'default') {
  179. const filename =
  180. theme === 'solarized light' || theme === 'solarized dark'
  181. ? 'solarized'
  182. : theme;
  183. await import(`codemirror/theme/${filename}.css`);
  184. }
  185. scrollPastEnd =
  186. (settings.get('scrollPastEnd').composite as boolean | null) ??
  187. scrollPastEnd;
  188. styleActiveLine =
  189. (settings.get('styleActiveLine').composite as
  190. | boolean
  191. | CodeMirror.StyleActiveLine) ?? styleActiveLine;
  192. styleSelectedText =
  193. (settings.get('styleSelectedText').composite as boolean) ??
  194. styleSelectedText;
  195. selectionPointer =
  196. (settings.get('selectionPointer').composite as boolean | string) ??
  197. selectionPointer;
  198. lineWiseCopyCut =
  199. (settings.get('lineWiseCopyCut').composite as boolean) ?? lineWiseCopyCut;
  200. }
  201. /**
  202. * Update the settings of the current tracker instances.
  203. */
  204. function updateTracker(): void {
  205. tracker.forEach(widget => {
  206. if (widget.content.editor instanceof CodeMirrorEditor) {
  207. const { editor } = widget.content;
  208. editor.setOption('keyMap', keyMap);
  209. editor.setOption('lineWiseCopyCut', lineWiseCopyCut);
  210. editor.setOption('scrollPastEnd', scrollPastEnd);
  211. editor.setOption('selectionPointer', selectionPointer);
  212. editor.setOption('styleActiveLine', styleActiveLine);
  213. editor.setOption('styleSelectedText', styleSelectedText);
  214. editor.setOption('theme', theme);
  215. }
  216. });
  217. }
  218. // Fetch the initial state of the settings.
  219. Promise.all([settingRegistry.load(id), restored])
  220. .then(async ([settings]) => {
  221. await updateSettings(settings);
  222. updateTracker();
  223. settings.changed.connect(async () => {
  224. await updateSettings(settings);
  225. updateTracker();
  226. });
  227. })
  228. .catch((reason: Error) => {
  229. console.error(reason.message);
  230. updateTracker();
  231. });
  232. /**
  233. * Handle the settings of new widgets.
  234. */
  235. tracker.widgetAdded.connect((sender, widget) => {
  236. if (widget.content.editor instanceof CodeMirrorEditor) {
  237. const { editor } = widget.content;
  238. editor.setOption('keyMap', keyMap);
  239. editor.setOption('lineWiseCopyCut', lineWiseCopyCut);
  240. editor.setOption('selectionPointer', selectionPointer);
  241. editor.setOption('scrollPastEnd', scrollPastEnd);
  242. editor.setOption('styleActiveLine', styleActiveLine);
  243. editor.setOption('styleSelectedText', styleSelectedText);
  244. editor.setOption('theme', theme);
  245. }
  246. });
  247. /**
  248. * A test for whether the tracker has an active widget.
  249. */
  250. function isEnabled(): boolean {
  251. return (
  252. tracker.currentWidget !== null &&
  253. tracker.currentWidget === app.shell.currentWidget
  254. );
  255. }
  256. /**
  257. * Create a menu for the editor.
  258. */
  259. const themeMenu = new Menu({ commands });
  260. const keyMapMenu = new Menu({ commands });
  261. const modeMenu = new Menu({ commands });
  262. themeMenu.title.label = trans.__('Text Editor Theme');
  263. keyMapMenu.title.label = trans.__('Text Editor Key Map');
  264. modeMenu.title.label = trans.__('Text Editor Syntax Highlighting');
  265. commands.addCommand(CommandIDs.changeTheme, {
  266. label: args => {
  267. if (args['theme'] === 'default') {
  268. return trans.__('codemirror');
  269. } else {
  270. return args['displayName'] as string;
  271. }
  272. },
  273. execute: args => {
  274. const key = 'theme';
  275. const value = (theme = (args['theme'] as string) || theme);
  276. return settingRegistry.set(id, key, value).catch((reason: Error) => {
  277. console.error(`Failed to set ${id}:${key} - ${reason.message}`);
  278. });
  279. },
  280. isToggled: args => args['theme'] === theme
  281. });
  282. commands.addCommand(CommandIDs.changeKeyMap, {
  283. label: args => {
  284. const title = args['displayName'] as string;
  285. const keyMap = args['keyMap'] as string;
  286. return keyMap === 'sublime' ? trans.__('Sublime Text') : title;
  287. },
  288. execute: args => {
  289. const key = 'keyMap';
  290. const value = (keyMap = (args['keyMap'] as string) || keyMap);
  291. return settingRegistry.set(id, key, value).catch((reason: Error) => {
  292. console.error(`Failed to set ${id}:${key} - ${reason.message}`);
  293. });
  294. },
  295. isToggled: args => args['keyMap'] === keyMap
  296. });
  297. commands.addCommand(CommandIDs.find, {
  298. label: trans.__('Find...'),
  299. execute: () => {
  300. const widget = tracker.currentWidget;
  301. if (!widget) {
  302. return;
  303. }
  304. const editor = widget.content.editor as CodeMirrorEditor;
  305. editor.execCommand('find');
  306. },
  307. isEnabled
  308. });
  309. commands.addCommand(CommandIDs.goToLine, {
  310. label: trans.__('Go to Line...'),
  311. execute: () => {
  312. const widget = tracker.currentWidget;
  313. if (!widget) {
  314. return;
  315. }
  316. const editor = widget.content.editor as CodeMirrorEditor;
  317. editor.execCommand('jumpToLine');
  318. },
  319. isEnabled
  320. });
  321. commands.addCommand(CommandIDs.changeMode, {
  322. label: args => args['name'] as string,
  323. execute: args => {
  324. const name = args['name'] as string;
  325. const widget = tracker.currentWidget;
  326. if (name && widget) {
  327. const spec = Mode.findByName(name);
  328. if (spec) {
  329. widget.content.model.mimeType = spec.mime;
  330. }
  331. }
  332. },
  333. isEnabled,
  334. isToggled: args => {
  335. const widget = tracker.currentWidget;
  336. if (!widget) {
  337. return false;
  338. }
  339. const mime = widget.content.model.mimeType;
  340. const spec = Mode.findByMIME(mime);
  341. const name = spec && spec.name;
  342. return args['name'] === name;
  343. }
  344. });
  345. Mode.getModeInfo()
  346. .sort((a, b) => {
  347. const aName = a.name || '';
  348. const bName = b.name || '';
  349. return aName.localeCompare(bName);
  350. })
  351. .forEach(spec => {
  352. // Avoid mode name with a curse word.
  353. if (spec.mode.indexOf('brainf') === 0) {
  354. return;
  355. }
  356. modeMenu.addItem({
  357. command: CommandIDs.changeMode,
  358. args: { ...spec } as any // TODO: Casting to `any` until lumino typings are fixed
  359. });
  360. });
  361. // FIXME-TRANS: Check this is working as expected
  362. [
  363. ['jupyter', trans.__('jupyter')],
  364. ['default', trans.__('default')],
  365. ['abcdef', trans.__('abcdef')],
  366. ['base16-dark', trans.__('base16-dark')],
  367. ['base16-light', trans.__('base16-light')],
  368. ['hopscotch', trans.__('hopscotch')],
  369. ['material', trans.__('material')],
  370. ['mbo', trans.__('mbo')],
  371. ['mdn-like', trans.__('mdn-like')],
  372. ['seti', trans.__('seti')],
  373. ['solarized dark', trans.__('solarized dark')],
  374. ['solarized light', trans.__('solarized light')],
  375. ['the-matrix', trans.__('the-matrix')],
  376. ['xq-light', trans.__('xq-light')],
  377. ['zenburn', trans.__('zenburn')]
  378. ].forEach(([name, displayName]) =>
  379. themeMenu.addItem({
  380. command: CommandIDs.changeTheme,
  381. args: { theme: name, displayName: displayName }
  382. })
  383. );
  384. // FIXME-TRANS: Check this is working as expected
  385. [
  386. ['default', trans.__('default')],
  387. ['sublime', trans.__('sublime')],
  388. ['vim', trans.__('vim')],
  389. ['emacs', trans.__('emacs')]
  390. ].forEach(([name, displayName]) => {
  391. keyMapMenu.addItem({
  392. command: CommandIDs.changeKeyMap,
  393. args: { keyMap: name, displayName: displayName }
  394. });
  395. });
  396. if (mainMenu) {
  397. // Add some of the editor settings to the settings menu.
  398. mainMenu.settingsMenu.addGroup(
  399. [
  400. { type: 'submenu' as Menu.ItemType, submenu: keyMapMenu },
  401. { type: 'submenu' as Menu.ItemType, submenu: themeMenu }
  402. ],
  403. 10
  404. );
  405. // Add the syntax highlighting submenu to the `View` menu.
  406. mainMenu.viewMenu.addGroup([{ type: 'submenu', submenu: modeMenu }], 40);
  407. // Add go to line capabilities to the edit menu.
  408. mainMenu.editMenu.goToLiners.add({
  409. tracker,
  410. goToLine: (widget: IDocumentWidget<FileEditor>) => {
  411. const editor = widget.content.editor as CodeMirrorEditor;
  412. editor.execCommand('jumpToLine');
  413. }
  414. } as IEditMenu.IGoToLiner<IDocumentWidget<FileEditor>>);
  415. }
  416. }