123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- import {
- ILayoutRestorer,
- JupyterLab,
- JupyterLabPlugin
- } from '@jupyterlab/application';
- import { ICommandPalette, InstanceTracker } from '@jupyterlab/apputils';
- import { CodeEditor, IEditorServices } from '@jupyterlab/codeeditor';
- import { IConsoleTracker } from '@jupyterlab/console';
- import {
- ISettingRegistry,
- MarkdownCodeBlocks,
- PathExt
- } from '@jupyterlab/coreutils';
- import { IDocumentWidget } from '@jupyterlab/docregistry';
- import { IFileBrowserFactory } from '@jupyterlab/filebrowser';
- import {
- FileEditor,
- FileEditorFactory,
- IEditorTracker,
- TabSpaceStatus
- } from '@jupyterlab/fileeditor';
- import { ILauncher } from '@jupyterlab/launcher';
- import {
- IEditMenu,
- IFileMenu,
- IMainMenu,
- IRunMenu,
- IViewMenu
- } from '@jupyterlab/mainmenu';
- import { IStatusBar } from '@jupyterlab/statusbar';
- import { JSONObject } from '@phosphor/coreutils';
- import { Menu } from '@phosphor/widgets';
- /**
- * The class name for the text editor icon from the default theme.
- */
- const EDITOR_ICON_CLASS = 'jp-TextEditorIcon';
- /**
- * The class name for the text editor icon from the default theme.
- */
- const MARKDOWN_ICON_CLASS = 'jp-MarkdownIcon';
- /**
- * The name of the factory that creates editor widgets.
- */
- const FACTORY = 'Editor';
- /**
- * The command IDs used by the fileeditor plugin.
- */
- namespace CommandIDs {
- export const createNew = 'fileeditor:create-new';
- export const createNewMarkdown = 'fileeditor:create-new-markdown-file';
- export const changeFontSize = 'fileeditor:change-font-size';
- export const lineNumbers = 'fileeditor:toggle-line-numbers';
- export const lineWrap = 'fileeditor:toggle-line-wrap';
- export const changeTabs = 'fileeditor:change-tabs';
- export const matchBrackets = 'fileeditor:toggle-match-brackets';
- export const autoClosingBrackets = 'fileeditor:toggle-autoclosing-brackets';
- export const createConsole = 'fileeditor:create-console';
- export const runCode = 'fileeditor:run-code';
- export const runAllCode = 'fileeditor:run-all';
- export const markdownPreview = 'fileeditor:markdown-preview';
- }
- /**
- * The editor tracker extension.
- */
- const plugin: JupyterLabPlugin<IEditorTracker> = {
- activate,
- id: '@jupyterlab/fileeditor-extension:plugin',
- requires: [
- IConsoleTracker,
- IEditorServices,
- IFileBrowserFactory,
- ILayoutRestorer,
- ISettingRegistry
- ],
- optional: [ICommandPalette, ILauncher, IMainMenu],
- provides: IEditorTracker,
- autoStart: true
- };
- /**
- * A plugin that provides a status item allowing the user to
- * switch tabs vs spaces and tab widths for text editors.
- */
- export const tabSpaceStatus: JupyterLabPlugin<void> = {
- id: '@jupyterlab/fileeditor-extension:tab-space-status',
- autoStart: true,
- requires: [IStatusBar, IEditorTracker, ISettingRegistry],
- activate: (
- app: JupyterLab,
- statusBar: IStatusBar,
- editorTracker: IEditorTracker,
- settingRegistry: ISettingRegistry
- ) => {
- // Create a menu for switching tabs vs spaces.
- const menu = new Menu({ commands: app.commands });
- const command = 'fileeditor:change-tabs';
- const args: JSONObject = {
- insertSpaces: false,
- size: 4,
- name: 'Indent with Tab'
- };
- menu.addItem({ command, args });
- for (let size of [1, 2, 4, 8]) {
- let args: JSONObject = {
- insertSpaces: true,
- size,
- name: `Spaces: ${size} `
- };
- menu.addItem({ command, args });
- }
- // Create the status item.
- const item = new TabSpaceStatus({ menu });
- // Keep a reference to the code editor config from the settings system.
- const updateSettings = (settings: ISettingRegistry.ISettings): void => {
- const cached = settings.get('editorConfig').composite as Partial<
- CodeEditor.IConfig
- >;
- const config: CodeEditor.IConfig = {
- ...CodeEditor.defaultConfig,
- ...cached
- };
- item.model!.config = config;
- };
- Promise.all([
- settingRegistry.load('@jupyterlab/fileeditor-extension:plugin'),
- app.restored
- ]).then(([settings]) => {
- updateSettings(settings);
- settings.changed.connect(updateSettings);
- });
- // Add the status item.
- statusBar.registerStatusItem(
- '@jupyterlab/fileeditor-extension:tab-space-status',
- {
- item,
- align: 'right',
- rank: 1,
- isActive: () => {
- return (
- app.shell.currentWidget &&
- editorTracker.has(app.shell.currentWidget)
- );
- }
- }
- );
- }
- };
- /**
- * Export the plugins as default.
- */
- const plugins: JupyterLabPlugin<any>[] = [plugin, tabSpaceStatus];
- export default plugins;
- /**
- * Activate the editor tracker plugin.
- */
- function activate(
- app: JupyterLab,
- consoleTracker: IConsoleTracker,
- editorServices: IEditorServices,
- browserFactory: IFileBrowserFactory,
- restorer: ILayoutRestorer,
- settingRegistry: ISettingRegistry,
- palette: ICommandPalette,
- launcher: ILauncher | null,
- menu: IMainMenu | null
- ): IEditorTracker {
- const id = plugin.id;
- const namespace = 'editor';
- const factory = new FileEditorFactory({
- editorServices,
- factoryOptions: {
- name: FACTORY,
- fileTypes: ['markdown', '*'], // Explicitly add the markdown fileType so
- defaultFor: ['markdown', '*'] // it outranks the defaultRendered viewer.
- }
- });
- const { commands, restored } = app;
- const tracker = new InstanceTracker<IDocumentWidget<FileEditor>>({
- namespace
- });
- const isEnabled = () =>
- tracker.currentWidget !== null &&
- tracker.currentWidget === app.shell.currentWidget;
- let config = { ...CodeEditor.defaultConfig };
- // Handle state restoration.
- restorer.restore(tracker, {
- command: 'docmanager:open',
- args: widget => ({ path: widget.context.path, factory: FACTORY }),
- name: widget => widget.context.path
- });
- /**
- * Update the setting values.
- */
- function updateSettings(settings: ISettingRegistry.ISettings): void {
- let cached = settings.get('editorConfig').composite as Partial<
- CodeEditor.IConfig
- >;
- Object.keys(config).forEach((key: keyof CodeEditor.IConfig) => {
- config[key] =
- cached[key] === null || cached[key] === undefined
- ? CodeEditor.defaultConfig[key]
- : cached[key];
- });
- // Trigger a refresh of the rendered commands
- app.commands.notifyCommandChanged();
- }
- /**
- * Update the settings of the current tracker instances.
- */
- function updateTracker(): void {
- tracker.forEach(widget => {
- updateWidget(widget.content);
- });
- }
- /**
- * Update the settings of a widget.
- */
- function updateWidget(widget: FileEditor): void {
- const editor = widget.editor;
- Object.keys(config).forEach((key: keyof CodeEditor.IConfig) => {
- editor.setOption(key, config[key]);
- });
- }
- // Add a console creator to the File menu
- // Fetch the initial state of the settings.
- Promise.all([settingRegistry.load(id), restored])
- .then(([settings]) => {
- updateSettings(settings);
- updateTracker();
- settings.changed.connect(() => {
- updateSettings(settings);
- updateTracker();
- });
- })
- .catch((reason: Error) => {
- console.error(reason.message);
- updateTracker();
- });
- factory.widgetCreated.connect((sender, widget) => {
- widget.title.icon = EDITOR_ICON_CLASS;
- // Notify the instance tracker if restore data needs to update.
- widget.context.pathChanged.connect(() => {
- tracker.save(widget);
- });
- tracker.add(widget);
- updateWidget(widget.content);
- });
- app.docRegistry.addWidgetFactory(factory);
- // Handle the settings of new widgets.
- tracker.widgetAdded.connect((sender, widget) => {
- updateWidget(widget.content);
- });
- // Add a command to change font size.
- commands.addCommand(CommandIDs.changeFontSize, {
- execute: args => {
- const delta = Number(args['delta']);
- if (Number.isNaN(delta)) {
- console.error(
- `${CommandIDs.changeFontSize}: delta arg must be a number`
- );
- return;
- }
- const style = window.getComputedStyle(document.documentElement);
- const cssSize = parseInt(
- style.getPropertyValue('--jp-code-font-size'),
- 10
- );
- const currentSize = config.fontSize || cssSize;
- config.fontSize = currentSize + delta;
- return settingRegistry
- .set(id, 'editorConfig', config)
- .catch((reason: Error) => {
- console.error(`Failed to set ${id}: ${reason.message}`);
- });
- },
- label: args => args['name'] as string
- });
- commands.addCommand(CommandIDs.lineNumbers, {
- execute: () => {
- config.lineNumbers = !config.lineNumbers;
- return settingRegistry
- .set(id, 'editorConfig', config)
- .catch((reason: Error) => {
- console.error(`Failed to set ${id}: ${reason.message}`);
- });
- },
- isEnabled,
- isToggled: () => config.lineNumbers,
- label: 'Line Numbers'
- });
- type wrappingMode = 'on' | 'off' | 'wordWrapColumn' | 'bounded';
- commands.addCommand(CommandIDs.lineWrap, {
- execute: args => {
- const lineWrap = (args['mode'] as wrappingMode) || 'off';
- config.lineWrap = lineWrap;
- return settingRegistry
- .set(id, 'editorConfig', config)
- .catch((reason: Error) => {
- console.error(`Failed to set ${id}: ${reason.message}`);
- });
- },
- isEnabled,
- isToggled: args => {
- const lineWrap = (args['mode'] as wrappingMode) || 'off';
- return config.lineWrap === lineWrap;
- },
- label: 'Word Wrap'
- });
- commands.addCommand(CommandIDs.changeTabs, {
- label: args => args['name'] as string,
- execute: args => {
- config.tabSize = (args['size'] as number) || 4;
- config.insertSpaces = !!args['insertSpaces'];
- return settingRegistry
- .set(id, 'editorConfig', config)
- .catch((reason: Error) => {
- console.error(`Failed to set ${id}: ${reason.message}`);
- });
- },
- isToggled: args => {
- const insertSpaces = !!args['insertSpaces'];
- const size = (args['size'] as number) || 4;
- return config.insertSpaces === insertSpaces && config.tabSize === size;
- }
- });
- commands.addCommand(CommandIDs.matchBrackets, {
- execute: () => {
- config.matchBrackets = !config.matchBrackets;
- return settingRegistry
- .set(id, 'editorConfig', config)
- .catch((reason: Error) => {
- console.error(`Failed to set ${id}: ${reason.message}`);
- });
- },
- label: 'Match Brackets',
- isEnabled,
- isToggled: () => config.matchBrackets
- });
- commands.addCommand(CommandIDs.autoClosingBrackets, {
- execute: () => {
- config.autoClosingBrackets = !config.autoClosingBrackets;
- return settingRegistry
- .set(id, 'editorConfig', config)
- .catch((reason: Error) => {
- console.error(`Failed to set ${id}: ${reason.message}`);
- });
- },
- label: 'Auto Close Brackets for Text Editor',
- isToggled: () => config.autoClosingBrackets
- });
- commands.addCommand(CommandIDs.createConsole, {
- execute: args => {
- const widget = tracker.currentWidget;
- if (!widget) {
- return;
- }
- return commands
- .execute('console:create', {
- activate: args['activate'],
- name: widget.context.contentsModel.name,
- path: widget.context.path,
- preferredLanguage: widget.context.model.defaultKernelLanguage,
- ref: widget.id,
- insertMode: 'split-bottom'
- })
- .then(console => {
- widget.context.pathChanged.connect((sender, value) => {
- console.session.setPath(value);
- console.session.setName(widget.context.contentsModel.name);
- });
- });
- },
- isEnabled,
- label: 'Create Console for Editor'
- });
- commands.addCommand(CommandIDs.runCode, {
- execute: () => {
- // Run the appropriate code, taking into account a ```fenced``` code block.
- const widget = tracker.currentWidget.content;
- if (!widget) {
- return;
- }
- let code = '';
- const editor = widget.editor;
- const path = widget.context.path;
- const extension = PathExt.extname(path);
- const selection = editor.getSelection();
- const { start, end } = selection;
- let selected = start.column !== end.column || start.line !== end.line;
- if (selected) {
- // Get the selected code from the editor.
- const start = editor.getOffsetAt(selection.start);
- const end = editor.getOffsetAt(selection.end);
- code = editor.model.value.text.substring(start, end);
- } else if (MarkdownCodeBlocks.isMarkdown(extension)) {
- const { text } = editor.model.value;
- const blocks = MarkdownCodeBlocks.findMarkdownCodeBlocks(text);
- for (let block of blocks) {
- if (block.startLine <= start.line && start.line <= block.endLine) {
- code = block.code;
- selected = true;
- break;
- }
- }
- }
- if (!selected) {
- // no selection, submit whole line and advance
- code = editor.getLine(selection.start.line);
- const cursor = editor.getCursorPosition();
- if (cursor.line + 1 === editor.lineCount) {
- let text = editor.model.value.text;
- editor.model.value.text = text + '\n';
- }
- editor.setCursorPosition({
- line: cursor.line + 1,
- column: cursor.column
- });
- }
- const activate = false;
- if (code) {
- return commands.execute('console:inject', { activate, code, path });
- } else {
- return Promise.resolve(void 0);
- }
- },
- isEnabled,
- label: 'Run Code'
- });
- commands.addCommand(CommandIDs.runAllCode, {
- execute: () => {
- let widget = tracker.currentWidget.content;
- if (!widget) {
- return;
- }
- let code = '';
- let editor = widget.editor;
- let text = editor.model.value.text;
- let path = widget.context.path;
- let extension = PathExt.extname(path);
- if (MarkdownCodeBlocks.isMarkdown(extension)) {
- // For Markdown files, run only code blocks.
- const blocks = MarkdownCodeBlocks.findMarkdownCodeBlocks(text);
- for (let block of blocks) {
- code += block.code;
- }
- } else {
- code = text;
- }
- const activate = false;
- if (code) {
- return commands.execute('console:inject', { activate, code, path });
- } else {
- return Promise.resolve(void 0);
- }
- },
- isEnabled,
- label: 'Run All Code'
- });
- commands.addCommand(CommandIDs.markdownPreview, {
- execute: () => {
- let widget = tracker.currentWidget;
- if (!widget) {
- return;
- }
- let path = widget.context.path;
- return commands.execute('markdownviewer:open', {
- path,
- options: {
- mode: 'split-right'
- }
- });
- },
- isVisible: () => {
- let widget = tracker.currentWidget;
- return (
- (widget && PathExt.extname(widget.context.path) === '.md') || false
- );
- },
- label: 'Show Markdown Preview'
- });
- // Function to create a new untitled text file, given
- // the current working directory.
- const createNew = (cwd: string, ext: string = 'txt') => {
- return commands
- .execute('docmanager:new-untitled', {
- path: cwd,
- type: 'file',
- ext
- })
- .then(model => {
- return commands.execute('docmanager:open', {
- path: model.path,
- factory: FACTORY
- });
- });
- };
- // Add a command for creating a new text file.
- commands.addCommand(CommandIDs.createNew, {
- label: args => (args['isPalette'] ? 'New Text File' : 'Text File'),
- caption: 'Create a new text file',
- iconClass: args => (args['isPalette'] ? '' : EDITOR_ICON_CLASS),
- execute: args => {
- let cwd = args['cwd'] || browserFactory.defaultBrowser.model.path;
- return createNew(cwd as string);
- }
- });
- // Add a command for creating a new Markdown file.
- commands.addCommand(CommandIDs.createNewMarkdown, {
- label: args => (args['isPalette'] ? 'New Markdown File' : 'Markdown File'),
- caption: 'Create a new markdown file',
- iconClass: args => (args['isPalette'] ? '' : MARKDOWN_ICON_CLASS),
- execute: args => {
- let cwd = args['cwd'] || browserFactory.defaultBrowser.model.path;
- return createNew(cwd as string, 'md');
- }
- });
- // Add a launcher item if the launcher is available.
- if (launcher) {
- launcher.add({
- command: CommandIDs.createNew,
- category: 'Other',
- rank: 1
- });
- launcher.add({
- command: CommandIDs.createNewMarkdown,
- category: 'Other',
- rank: 2
- });
- }
- if (palette) {
- const category = 'Text Editor';
- let args: JSONObject = {
- insertSpaces: false,
- size: 4,
- name: 'Indent with Tab'
- };
- let command = 'fileeditor:change-tabs';
- palette.addItem({ command, args, category });
- for (let size of [1, 2, 4, 8]) {
- let args: JSONObject = {
- insertSpaces: true,
- size,
- name: `Spaces: ${size} `
- };
- palette.addItem({ command, args, category });
- }
- args = { isPalette: true };
- command = CommandIDs.createNew;
- palette.addItem({ command, args, category });
- args = { isPalette: true };
- command = CommandIDs.createNewMarkdown;
- palette.addItem({ command, args, category });
- args = { name: 'Increase Font Size', delta: 1 };
- command = CommandIDs.changeFontSize;
- palette.addItem({ command, args, category });
- args = { name: 'Decrease Font Size', delta: -1 };
- command = CommandIDs.changeFontSize;
- palette.addItem({ command, args, category });
- }
- if (menu) {
- // Add the editing commands to the settings menu.
- const tabMenu = new Menu({ commands });
- tabMenu.title.label = 'Text Editor Indentation';
- let args: JSONObject = {
- insertSpaces: false,
- size: 4,
- name: 'Indent with Tab'
- };
- let command = 'fileeditor:change-tabs';
- tabMenu.addItem({ command, args });
- for (let size of [1, 2, 4, 8]) {
- let args: JSONObject = {
- insertSpaces: true,
- size,
- name: `Spaces: ${size} `
- };
- tabMenu.addItem({ command, args });
- }
- menu.settingsMenu.addGroup(
- [
- {
- command: CommandIDs.changeFontSize,
- args: { name: 'Increase Text Editor Font Size', delta: +1 }
- },
- {
- command: CommandIDs.changeFontSize,
- args: { name: 'Decrease Text Editor Font Size', delta: -1 }
- },
- { type: 'submenu', submenu: tabMenu },
- { command: CommandIDs.autoClosingBrackets }
- ],
- 30
- );
- // Add new text file creation to the file menu.
- menu.fileMenu.newMenu.addGroup([{ command: CommandIDs.createNew }], 30);
- // Add new markdown file creation to the file menu.
- menu.fileMenu.newMenu.addGroup(
- [{ command: CommandIDs.createNewMarkdown }],
- 30
- );
- // Add undo/redo hooks to the edit menu.
- menu.editMenu.undoers.add({
- tracker,
- undo: widget => {
- widget.content.editor.undo();
- },
- redo: widget => {
- widget.content.editor.redo();
- }
- } as IEditMenu.IUndoer<IDocumentWidget<FileEditor>>);
- // Add editor view options.
- menu.viewMenu.editorViewers.add({
- tracker,
- toggleLineNumbers: widget => {
- const lineNumbers = !widget.content.editor.getOption('lineNumbers');
- widget.content.editor.setOption('lineNumbers', lineNumbers);
- },
- toggleWordWrap: widget => {
- const oldValue = widget.content.editor.getOption('lineWrap');
- const newValue = oldValue === 'off' ? 'on' : 'off';
- widget.content.editor.setOption('lineWrap', newValue);
- },
- toggleMatchBrackets: widget => {
- const matchBrackets = !widget.content.editor.getOption('matchBrackets');
- widget.content.editor.setOption('matchBrackets', matchBrackets);
- },
- lineNumbersToggled: widget =>
- widget.content.editor.getOption('lineNumbers'),
- wordWrapToggled: widget =>
- widget.content.editor.getOption('lineWrap') !== 'off',
- matchBracketsToggled: widget =>
- widget.content.editor.getOption('matchBrackets')
- } as IViewMenu.IEditorViewer<IDocumentWidget<FileEditor>>);
- // Add a console creator the the Kernel menu.
- menu.fileMenu.consoleCreators.add({
- tracker,
- name: 'Editor',
- createConsole: current => {
- const options = {
- path: current.context.path,
- preferredLanguage: current.context.model.defaultKernelLanguage
- };
- return commands.execute('console:create', options);
- }
- } as IFileMenu.IConsoleCreator<IDocumentWidget<FileEditor>>);
- // Add a code runner to the Run menu.
- menu.runMenu.codeRunners.add({
- tracker,
- noun: 'Code',
- isEnabled: current => {
- let found = false;
- consoleTracker.forEach(console => {
- if (console.console.session.path === current.context.path) {
- found = true;
- }
- });
- return found;
- },
- run: () => commands.execute(CommandIDs.runCode),
- runAll: () => commands.execute(CommandIDs.runAllCode),
- restartAndRunAll: current => {
- return current.context.session.restart().then(restarted => {
- if (restarted) {
- commands.execute(CommandIDs.runAllCode);
- }
- return restarted;
- });
- }
- } as IRunMenu.ICodeRunner<IDocumentWidget<FileEditor>>);
- }
- app.contextMenu.addItem({
- command: CommandIDs.createConsole,
- selector: '.jp-FileEditor'
- });
- app.contextMenu.addItem({
- command: CommandIDs.markdownPreview,
- selector: '.jp-FileEditor'
- });
- return tracker;
- }
|