123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import CodeMirror from 'codemirror';
- import { Menu } from '@lumino/widgets';
- import {
- ILabShell,
- JupyterFrontEnd,
- JupyterFrontEndPlugin
- } from '@jupyterlab/application';
- import { IEditMenu, IMainMenu } from '@jupyterlab/mainmenu';
- import { IEditorServices } from '@jupyterlab/codeeditor';
- import {
- editorServices,
- EditorSyntaxStatus,
- CodeMirrorEditor,
- Mode,
- ICodeMirror
- } from '@jupyterlab/codemirror';
- import { IDocumentWidget } from '@jupyterlab/docregistry';
- import { IEditorTracker, FileEditor } from '@jupyterlab/fileeditor';
- import { ISettingRegistry } from '@jupyterlab/settingregistry';
- import { IStatusBar } from '@jupyterlab/statusbar';
- import { ITranslator } from '@jupyterlab/translation';
- /**
- * The command IDs used by the codemirror plugin.
- */
- namespace CommandIDs {
- export const changeKeyMap = 'codemirror:change-keymap';
- export const changeTheme = 'codemirror:change-theme';
- export const changeMode = 'codemirror:change-mode';
- export const find = 'codemirror:find';
- export const goToLine = 'codemirror:go-to-line';
- }
- /** The CodeMirror singleton. */
- const codemirrorSingleton: JupyterFrontEndPlugin<ICodeMirror> = {
- id: '@jupyterlab/codemirror-extension:codemirror',
- provides: ICodeMirror,
- activate: activateCodeMirror
- };
- /**
- * The editor services.
- */
- const services: JupyterFrontEndPlugin<IEditorServices> = {
- id: '@jupyterlab/codemirror-extension:services',
- provides: IEditorServices,
- activate: activateEditorServices
- };
- /**
- * The editor commands.
- */
- const commands: JupyterFrontEndPlugin<void> = {
- id: '@jupyterlab/codemirror-extension:commands',
- requires: [IEditorTracker, ISettingRegistry, ITranslator, ICodeMirror],
- optional: [IMainMenu],
- activate: activateEditorCommands,
- autoStart: true
- };
- /**
- * The JupyterLab plugin for the EditorSyntax status item.
- */
- export const editorSyntaxStatus: JupyterFrontEndPlugin<void> = {
- id: '@jupyterlab/codemirror-extension:editor-syntax-status',
- autoStart: true,
- requires: [IEditorTracker, ILabShell, ITranslator],
- optional: [IStatusBar],
- activate: (
- app: JupyterFrontEnd,
- tracker: IEditorTracker,
- labShell: ILabShell,
- translator: ITranslator,
- statusBar: IStatusBar | null
- ) => {
- if (!statusBar) {
- // Automatically disable if statusbar missing
- return;
- }
- const item = new EditorSyntaxStatus({ commands: app.commands, translator });
- labShell.currentChanged.connect(() => {
- const current = labShell.currentWidget;
- if (current && tracker.has(current) && item.model) {
- item.model.editor = (current as IDocumentWidget<
- FileEditor
- >).content.editor;
- }
- });
- statusBar.registerStatusItem(
- '@jupyterlab/codemirror-extension:editor-syntax-status',
- {
- item,
- align: 'left',
- rank: 0,
- isActive: () =>
- !!labShell.currentWidget &&
- !!tracker.currentWidget &&
- labShell.currentWidget === tracker.currentWidget
- }
- );
- }
- };
- /**
- * Export the plugins as default.
- */
- const plugins: JupyterFrontEndPlugin<any>[] = [
- commands,
- services,
- editorSyntaxStatus,
- codemirrorSingleton
- ];
- export default plugins;
- /**
- * The plugin ID used as the key in the setting registry.
- */
- const id = commands.id;
- /**
- * Set up the editor services.
- */
- function activateEditorServices(app: JupyterFrontEnd): IEditorServices {
- CodeMirror.prototype.save = () => {
- void app.commands.execute('docmanager:save');
- };
- return editorServices;
- }
- /**
- * Simplest implementation of the CodeMirror singleton provider.
- */
- class CodeMirrorSingleton implements ICodeMirror {
- get CodeMirror() {
- return CodeMirror;
- }
- async ensureVimKeymap() {
- if (!('Vim' in (CodeMirror as any))) {
- // @ts-expect-error
- await import('codemirror/keymap/vim.js');
- }
- }
- }
- /**
- * Set up the CodeMirror singleton.
- */
- function activateCodeMirror(app: JupyterFrontEnd): ICodeMirror {
- return new CodeMirrorSingleton();
- }
- /**
- * Set up the editor widget menu and commands.
- */
- function activateEditorCommands(
- app: JupyterFrontEnd,
- tracker: IEditorTracker,
- settingRegistry: ISettingRegistry,
- translator: ITranslator,
- codeMirror: ICodeMirror,
- mainMenu: IMainMenu | null
- ): void {
- const trans = translator.load('jupyterlab');
- const { commands, restored } = app;
- let {
- theme,
- keyMap,
- scrollPastEnd,
- styleActiveLine,
- styleSelectedText,
- selectionPointer,
- lineWiseCopyCut
- } = CodeMirrorEditor.defaultConfig;
- /**
- * Update the setting values.
- */
- async function updateSettings(
- settings: ISettingRegistry.ISettings
- ): Promise<void> {
- keyMap = (settings.get('keyMap').composite as string | null) || keyMap;
- // Lazy loading of vim mode
- if (keyMap === 'vim') {
- await codeMirror.ensureVimKeymap();
- }
- theme = (settings.get('theme').composite as string | null) || theme;
- // Lazy loading of theme stylesheets
- if (theme !== 'jupyter' && theme !== 'default') {
- const filename =
- theme === 'solarized light' || theme === 'solarized dark'
- ? 'solarized'
- : theme;
- await import(`codemirror/theme/${filename}.css`);
- }
- scrollPastEnd =
- (settings.get('scrollPastEnd').composite as boolean | null) ??
- scrollPastEnd;
- styleActiveLine =
- (settings.get('styleActiveLine').composite as
- | boolean
- | CodeMirror.StyleActiveLine) ?? styleActiveLine;
- styleSelectedText =
- (settings.get('styleSelectedText').composite as boolean) ??
- styleSelectedText;
- selectionPointer =
- (settings.get('selectionPointer').composite as boolean | string) ??
- selectionPointer;
- lineWiseCopyCut =
- (settings.get('lineWiseCopyCut').composite as boolean) ?? lineWiseCopyCut;
- }
- /**
- * Update the settings of the current tracker instances.
- */
- function updateTracker(): void {
- tracker.forEach(widget => {
- if (widget.content.editor instanceof CodeMirrorEditor) {
- const { editor } = widget.content;
- editor.setOption('keyMap', keyMap);
- editor.setOption('lineWiseCopyCut', lineWiseCopyCut);
- editor.setOption('scrollPastEnd', scrollPastEnd);
- editor.setOption('selectionPointer', selectionPointer);
- editor.setOption('styleActiveLine', styleActiveLine);
- editor.setOption('styleSelectedText', styleSelectedText);
- editor.setOption('theme', theme);
- }
- });
- }
- // Fetch the initial state of the settings.
- Promise.all([settingRegistry.load(id), restored])
- .then(async ([settings]) => {
- await updateSettings(settings);
- updateTracker();
- settings.changed.connect(async () => {
- await updateSettings(settings);
- updateTracker();
- });
- })
- .catch((reason: Error) => {
- console.error(reason.message);
- updateTracker();
- });
- /**
- * Handle the settings of new widgets.
- */
- tracker.widgetAdded.connect((sender, widget) => {
- if (widget.content.editor instanceof CodeMirrorEditor) {
- const { editor } = widget.content;
- editor.setOption('keyMap', keyMap);
- editor.setOption('lineWiseCopyCut', lineWiseCopyCut);
- editor.setOption('selectionPointer', selectionPointer);
- editor.setOption('scrollPastEnd', scrollPastEnd);
- editor.setOption('styleActiveLine', styleActiveLine);
- editor.setOption('styleSelectedText', styleSelectedText);
- editor.setOption('theme', theme);
- }
- });
- /**
- * A test for whether the tracker has an active widget.
- */
- function isEnabled(): boolean {
- return (
- tracker.currentWidget !== null &&
- tracker.currentWidget === app.shell.currentWidget
- );
- }
- /**
- * Create a menu for the editor.
- */
- const themeMenu = new Menu({ commands });
- const keyMapMenu = new Menu({ commands });
- const modeMenu = new Menu({ commands });
- themeMenu.title.label = trans.__('Text Editor Theme');
- keyMapMenu.title.label = trans.__('Text Editor Key Map');
- modeMenu.title.label = trans.__('Text Editor Syntax Highlighting');
- commands.addCommand(CommandIDs.changeTheme, {
- label: args => {
- if (args['theme'] === 'default') {
- return trans.__('codemirror');
- } else {
- return args['displayName'] as string;
- }
- },
- execute: args => {
- const key = 'theme';
- const value = (theme = (args['theme'] as string) || theme);
- return settingRegistry.set(id, key, value).catch((reason: Error) => {
- console.error(`Failed to set ${id}:${key} - ${reason.message}`);
- });
- },
- isToggled: args => args['theme'] === theme
- });
- commands.addCommand(CommandIDs.changeKeyMap, {
- label: args => {
- const title = args['displayName'] as string;
- const keyMap = args['keyMap'] as string;
- return keyMap === 'sublime' ? trans.__('Sublime Text') : title;
- },
- execute: args => {
- const key = 'keyMap';
- const value = (keyMap = (args['keyMap'] as string) || keyMap);
- return settingRegistry.set(id, key, value).catch((reason: Error) => {
- console.error(`Failed to set ${id}:${key} - ${reason.message}`);
- });
- },
- isToggled: args => args['keyMap'] === keyMap
- });
- commands.addCommand(CommandIDs.find, {
- label: trans.__('Find...'),
- execute: () => {
- const widget = tracker.currentWidget;
- if (!widget) {
- return;
- }
- const editor = widget.content.editor as CodeMirrorEditor;
- editor.execCommand('find');
- },
- isEnabled
- });
- commands.addCommand(CommandIDs.goToLine, {
- label: trans.__('Go to Line...'),
- execute: () => {
- const widget = tracker.currentWidget;
- if (!widget) {
- return;
- }
- const editor = widget.content.editor as CodeMirrorEditor;
- editor.execCommand('jumpToLine');
- },
- isEnabled
- });
- commands.addCommand(CommandIDs.changeMode, {
- label: args => args['name'] as string,
- execute: args => {
- const name = args['name'] as string;
- const widget = tracker.currentWidget;
- if (name && widget) {
- const spec = Mode.findByName(name);
- if (spec) {
- widget.content.model.mimeType = spec.mime;
- }
- }
- },
- isEnabled,
- isToggled: args => {
- const widget = tracker.currentWidget;
- if (!widget) {
- return false;
- }
- const mime = widget.content.model.mimeType;
- const spec = Mode.findByMIME(mime);
- const name = spec && spec.name;
- return args['name'] === name;
- }
- });
- Mode.getModeInfo()
- .sort((a, b) => {
- const aName = a.name || '';
- const bName = b.name || '';
- return aName.localeCompare(bName);
- })
- .forEach(spec => {
- // Avoid mode name with a curse word.
- if (spec.mode.indexOf('brainf') === 0) {
- return;
- }
- modeMenu.addItem({
- command: CommandIDs.changeMode,
- args: { ...spec } as any // TODO: Casting to `any` until lumino typings are fixed
- });
- });
- // FIXME-TRANS: Check this is working as expected
- [
- ['jupyter', trans.__('jupyter')],
- ['default', trans.__('default')],
- ['abcdef', trans.__('abcdef')],
- ['base16-dark', trans.__('base16-dark')],
- ['base16-light', trans.__('base16-light')],
- ['hopscotch', trans.__('hopscotch')],
- ['material', trans.__('material')],
- ['mbo', trans.__('mbo')],
- ['mdn-like', trans.__('mdn-like')],
- ['seti', trans.__('seti')],
- ['solarized dark', trans.__('solarized dark')],
- ['solarized light', trans.__('solarized light')],
- ['the-matrix', trans.__('the-matrix')],
- ['xq-light', trans.__('xq-light')],
- ['zenburn', trans.__('zenburn')]
- ].forEach(([name, displayName]) =>
- themeMenu.addItem({
- command: CommandIDs.changeTheme,
- args: { theme: name, displayName: displayName }
- })
- );
- // FIXME-TRANS: Check this is working as expected
- [
- ['default', trans.__('default')],
- ['sublime', trans.__('sublime')],
- ['vim', trans.__('vim')],
- ['emacs', trans.__('emacs')]
- ].forEach(([name, displayName]) => {
- keyMapMenu.addItem({
- command: CommandIDs.changeKeyMap,
- args: { keyMap: name, displayName: displayName }
- });
- });
- if (mainMenu) {
- // Add some of the editor settings to the settings menu.
- mainMenu.settingsMenu.addGroup(
- [
- { type: 'submenu' as Menu.ItemType, submenu: keyMapMenu },
- { type: 'submenu' as Menu.ItemType, submenu: themeMenu }
- ],
- 10
- );
- // Add the syntax highlighting submenu to the `View` menu.
- mainMenu.viewMenu.addGroup([{ type: 'submenu', submenu: modeMenu }], 40);
- // Add go to line capabilities to the edit menu.
- mainMenu.editMenu.goToLiners.add({
- tracker,
- goToLine: (widget: IDocumentWidget<FileEditor>) => {
- const editor = widget.content.editor as CodeMirrorEditor;
- editor.execCommand('jumpToLine');
- }
- } as IEditMenu.IGoToLiner<IDocumentWidget<FileEditor>>);
- }
- }
|