ソースを参照

Merge pull request #937 from blink1073/console-for-editor

Allow starting a console attached to a text editor
Brian E. Granger 8 年 前
コミット
b6eb769eef

+ 15 - 19
src/console/content.ts

@@ -316,13 +316,17 @@ class ConsoleContent extends Widget {
     let prompt = this.prompt;
     prompt.trusted = true;
     if (force) {
-      return this._execute();
+      // Create a new prompt before kernel execution to allow typeahead.
+      this.newPrompt();
+      return this._execute(prompt);
     }
 
     // Check whether we should execute.
     return this._shouldExecute().then(value => {
       if (value) {
-        return this._execute();
+        // Create a new prompt before kernel execution to allow typeahead.
+        this.newPrompt();
+        return this._execute(prompt);
       }
     });
   }
@@ -333,16 +337,11 @@ class ConsoleContent extends Widget {
   inject(code: string): void {
     // Create a new cell using the prompt renderer.
     let cell = this._renderer.createPrompt(this._rendermime);
-    let onSuccess = (value: KernelMessage.IExecuteReplyMsg) => {
-      let content = this._content;
-      cell.readOnly = true;
-      content.addWidget(cell);
-      this.update();
-    };
-    let onFailure = () => { this.update(); };
-
     cell.model.source = code;
-    cell.execute(this._session.kernel).then(onSuccess, onFailure);
+    cell.mimetype = this._mimetype;
+    cell.readOnly = true;
+    this._content.addWidget(cell);
+    this._execute(cell);
   }
 
   /**
@@ -490,11 +489,8 @@ class ConsoleContent extends Widget {
   /**
    * Execute the code in the current prompt.
    */
-  private _execute(): Promise<void> {
-    let prompt = this.prompt;
-    this._history.push(prompt.model.source);
-    // Create a new prompt before kernel execution to allow typeahead.
-    this.newPrompt();
+  private _execute(cell: CodeCellWidget): Promise<void> {
+    this._history.push(cell.model.source);
     let onSuccess = (value: KernelMessage.IExecuteReplyMsg) => {
       this.executed.emit(new Date());
       if (!value) {
@@ -511,15 +507,15 @@ class ConsoleContent extends Widget {
           })[0];
           if (setNextInput) {
             let text = (setNextInput as any).text;
-            // Ignore the `replace` value and always set the next prompt.
-            this.prompt.model.source = text;
+            // Ignore the `replace` value and always set the next cell.
+            cell.model.source = text;
           }
         }
       }
       this.update();
     };
     let onFailure = () => { this.update(); };
-    return prompt.execute(this._session.kernel).then(onSuccess, onFailure);
+    return cell.execute(this._session.kernel).then(onSuccess, onFailure);
   }
 
   /**

+ 145 - 52
src/console/plugin.ts

@@ -2,9 +2,13 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  ContentsManager, IKernel, ISession
+  ContentsManager, IKernel, ISession, utils
 } from 'jupyter-js-services';
 
+import {
+  JSONObject
+} from 'phosphor/lib/algorithm/json';
+
 import {
   FocusTracker
 } from 'phosphor/lib/ui/focustracker';
@@ -84,10 +88,16 @@ const LANDSCAPE_ICON_CLASS = 'jp-MainAreaLandscapeIcon';
  */
 const CONSOLE_ICON_CLASS = 'jp-ImageConsole';
 
+
 /**
- * The file extension for consoles.
+ * The interface for a start console.
  */
-const FILE_EXTENSION = 'jpcon';
+interface ICreateConsoleArgs extends JSONObject {
+  sessionId?: string;
+  path?: string;
+  kernel: IKernel.IModel;
+  preferredLanguage?: string;
+}
 
 
 /**
@@ -111,14 +121,15 @@ function activateConsole(app: JupyterLab, services: IServiceManager, rendermime:
   // Add the ability to create new consoles for each kernel.
   let specs = services.kernelspecs;
   let displayNameMap: { [key: string]: string } = Object.create(null);
+  let kernelNameMap: { [key: string]: string } = Object.create(null);
   for (let kernelName in specs.kernelspecs) {
     let displayName = specs.kernelspecs[kernelName].spec.display_name;
-    displayNameMap[displayName] = kernelName;
+    kernelNameMap[displayName] = kernelName;
+    displayNameMap[kernelName] = displayName;
   }
-  let displayNames = Object.keys(displayNameMap).sort((a, b) => {
+  let displayNames = Object.keys(kernelNameMap).sort((a, b) => {
     return a.localeCompare(b);
   });
-  let count = 0;
 
   // If there are available kernels, populate the "New" menu item.
   if (displayNames.length) {
@@ -128,51 +139,12 @@ function activateConsole(app: JupyterLab, services: IServiceManager, rendermime:
   }
 
   for (let displayName of displayNames) {
-    command = `console:create-${displayNameMap[displayName]}`;
+    command = `console:create-${kernelNameMap[displayName]}`;
     commands.addCommand(command, {
       label: `${displayName} console`,
       execute: () => {
-        count++;
-        let file = `console-${count}`;
-        let path = `${pathTracker.path}/${file}.${FILE_EXTENSION}`;
-        let label = `Console ${count}`;
-        let kernelName = `${displayNameMap[displayName]}`;
-        let captionOptions: Private.ICaptionOptions = {
-          label, displayName, path,
-          connected: new Date()
-        };
-        manager.startNew({ path, kernelName }).then(session => {
-          let panel = new ConsolePanel({
-            session,
-            rendermime: rendermime.clone(),
-            renderer: renderer
-          });
-          panel.id = file;
-          panel.title.label = label;
-          panel.title.caption = Private.caption(captionOptions);
-          panel.title.icon = `${LANDSCAPE_ICON_CLASS} ${CONSOLE_ICON_CLASS}`;
-          panel.title.closable = true;
-          app.shell.addToMainArea(panel);
-          // Update the caption of the tab with the last execution time.
-          panel.content.executed.connect((sender, executed) => {
-            captionOptions.executed = executed;
-            panel.title.caption = Private.caption(captionOptions);
-          });
-          // Set the source of the code inspector to the current console.
-          panel.activated.connect(() => {
-            inspector.source = panel.content.inspectionHandler;
-          });
-          // Update the caption of the tab when the kernel changes.
-          panel.content.session.kernelChanged.connect(() => {
-            let name = panel.content.session.kernel.name;
-            name = specs.kernelspecs[name].spec.display_name;
-            captionOptions.displayName = name;
-            captionOptions.connected = new Date();
-            captionOptions.executed = null;
-            panel.title.caption = Private.caption(captionOptions);
-          });
-          tracker.add(panel);
-        });
+        let name = `${kernelNameMap[displayName]}`;
+        commands.execute('console:create', { kernel: { name } });
       }
     });
     palette.addItem({ command, category });
@@ -202,9 +174,9 @@ function activateConsole(app: JupyterLab, services: IServiceManager, rendermime:
   });
 
 
-  command = 'console:execute';
+  command = 'console:run';
   commands.addCommand(command, {
-    label: 'Execute Cell',
+    label: 'Run Cell',
     execute: () => {
       if (tracker.currentWidget) {
         tracker.currentWidget.content.execute();
@@ -215,9 +187,9 @@ function activateConsole(app: JupyterLab, services: IServiceManager, rendermime:
   menu.addItem({ command });
 
 
-  command = 'console:execute-forced';
+  command = 'console:run-forced';
   commands.addCommand(command, {
-    label: 'Execute Cell (forced)',
+    label: 'Run Cell (forced)',
     execute: () => {
       if (tracker.currentWidget) {
         tracker.currentWidget.content.execute(true);
@@ -255,6 +227,123 @@ function activateConsole(app: JupyterLab, services: IServiceManager, rendermime:
   menu.addItem({ command });
 
 
+  command = 'console:create';
+  commands.addCommand(command, {
+    execute: (args: ICreateConsoleArgs) => {
+      // If we get a session, use it.
+      if (args.sessionId) {
+        return manager.connectTo(args.sessionId).then(session => {
+          createConsole(session);
+          return session.id;
+        });
+      }
+
+      // Find the correct path for the new session.
+      // Use the given path or the cwd.
+      let path = args.path || pathTracker.path;
+      if (ContentsManager.extname(path)) {
+        path = ContentsManager.dirname(path);
+      }
+      path = `${path}/console-${utils.uuid()}`;
+
+      // Get the kernel model.
+      return getKernel(args).then(kernel => {
+        if (!kernel) {
+          return;
+        }
+        // Start the session.
+        let options: ISession.IOptions = {
+          path,
+          kernelName: kernel.name,
+          kernelId: kernel.id
+        };
+        return manager.startNew(options).then(session => {
+          createConsole(session);
+          return session.id;
+        });
+      });
+    }
+  });
+
+  command = 'console:inject';
+  commands.addCommand(command, {
+    execute: (args: JSONObject) => {
+      let id = args['id'];
+      for (let i = 0; i < tracker.widgets.length; i++) {
+        let widget = tracker.widgets.at(i);
+        if (widget.content.session.id === id) {
+          widget.content.inject(args['code'] as string);
+        }
+      }
+    }
+  });
+
+  /**
+   * Get the kernel given the create args.
+   */
+  function getKernel(args: ICreateConsoleArgs): Promise<IKernel.IModel> {
+    if (args.kernel) {
+      return Promise.resolve(args.kernel);
+    }
+    return manager.listRunning().then((sessions: ISession.IModel[]) => {
+      let options = {
+        name: 'New Console',
+        specs,
+        sessions,
+        preferredLanguage: args.preferredLanguage || '',
+        host: document.body
+      };
+      return selectKernel(options);
+    });
+  }
+
+
+  let count = 0;
+
+  /**
+   * Create a console for a given session.
+   */
+  function createConsole(session: ISession): void {
+    let panel = new ConsolePanel({
+      session,
+      rendermime: rendermime.clone(),
+      renderer: renderer
+    });
+    count++;
+    let displayName = displayNameMap[session.kernel.name];
+    let label = `Console ${count}`;
+    let captionOptions: Private.ICaptionOptions = {
+      label, displayName,
+      path: session.path,
+      connected: new Date()
+    };
+    panel.id = `console-${session.id}`;
+    panel.title.label = label;
+    panel.title.caption = Private.caption(captionOptions);
+    panel.title.icon = `${LANDSCAPE_ICON_CLASS} ${CONSOLE_ICON_CLASS}`;
+    panel.title.closable = true;
+    app.shell.addToMainArea(panel);
+    // Update the caption of the tab with the last execution time.
+    panel.content.executed.connect((sender, executed) => {
+      captionOptions.executed = executed;
+      panel.title.caption = Private.caption(captionOptions);
+    });
+    // Set the source of the code inspector to the current console.
+    panel.activated.connect(() => {
+      inspector.source = panel.content.inspectionHandler;
+    });
+    // Update the caption of the tab when the kernel changes.
+    panel.content.session.kernelChanged.connect(() => {
+      let name = panel.content.session.kernel.name;
+      name = specs.kernelspecs[name].spec.display_name;
+      captionOptions.displayName = name;
+      captionOptions.connected = new Date();
+      captionOptions.executed = null;
+      panel.title.caption = Private.caption(captionOptions);
+    });
+    tracker.add(panel);
+  }
+
   command = 'console:switch-kernel';
   commands.addCommand(command, {
     label: 'Switch Kernel',
@@ -351,3 +440,7 @@ namespace Private {
     return caption;
   }
 }
+
+
+
+

+ 2 - 1
src/docmanager/context.ts

@@ -49,7 +49,8 @@ class Context<T extends IDocumentModel> implements IDocumentContext<T> {
     this._factory = options.factory;
     this._opener = options.opener;
     this._path = options.path;
-    let lang = this._factory.preferredLanguage(this._path);
+    let ext = ContentsManager.extname(this._path);
+    let lang = this._factory.preferredLanguage(ext);
     this._model = this._factory.createNew(lang);
     manager.sessions.runningChanged.connect(this._onSessionsChanged, this);
     this._saver = new SaveHandler({ context: this, manager });

+ 6 - 3
src/docregistry/kernelselector.ts

@@ -183,16 +183,19 @@ function populateKernels(node: HTMLSelectElement, specs: IKernel.ISpecModels, ru
   // Create mappings of display names and languages for kernel name.
   let displayNames: { [key: string]: string } = Object.create(null);
   let languages: { [key: string]: string } = Object.create(null);
+  let modes: { [key: string]: string } = Object.create(null);
   for (let name in specs.kernelspecs) {
-    displayNames[name] = specs.kernelspecs[name].spec.display_name;
+    let spec = specs.kernelspecs[name].spec;
+    displayNames[name] = spec.display_name;
     maxLength = Math.max(maxLength, displayNames[name].length);
-    languages[name] = specs.kernelspecs[name].spec.language;
+    languages[name] = spec.language;
+    modes[name] = spec.codemirror_mode;
   }
   // Handle a preferred kernel language in order of display name.
   let names: string[] = [];
   if (preferredLanguage) {
     for (let name in specs.kernelspecs) {
-      if (languages[name] === preferredLanguage) {
+      if (languages[name] === preferredLanguage || modes[name] === preferredLanguage) {
         names.push(name);
       }
     }

+ 78 - 24
src/editorwidget/plugin.ts

@@ -5,6 +5,10 @@ import {
   each
 } from 'phosphor/lib/algorithm/iteration';
 
+import {
+  AttachedProperty
+} from 'phosphor/lib/core/properties';
+
 import {
   FocusTracker
 } from 'phosphor/lib/ui/focustracker';
@@ -80,7 +84,9 @@ const cmdIds = {
   matchBrackets: 'editor:match-brackets',
   vimMode: 'editor:vim-mode',
   closeAll: 'editor:close-all',
-  changeTheme: 'editor:change-theme'
+  changeTheme: 'editor:change-theme',
+  createConsole: 'editor:create-console',
+  runCode: 'editor:run-code'
 };
 
 
@@ -106,47 +112,95 @@ function activateEditorHandler(app: JupyterLab, registry: IDocumentRegistry, mai
 
   mainMenu.addMenu(createMenu(app, tracker), {rank: 30});
 
-  addCommands(app, tracker);
-
-  [
-    cmdIds.lineNumbers,
-    cmdIds.lineWrap,
-    cmdIds.matchBrackets,
-    cmdIds.vimMode,
-    cmdIds.closeAll,
-  ].forEach(command => palette.addItem({ command, category: 'Editor' }));
-
-  return tracker;
-}
-
+  let commands = app.commands;
 
-/**
- * Add the editor commands to the application's command registry.
- */
-function addCommands(app: JupyterLab, tracker: IEditorTracker): void {
-  app.commands.addCommand(cmdIds.lineNumbers, {
+  commands.addCommand(cmdIds.lineNumbers, {
     execute: () => { toggleLineNums(tracker); },
     label: 'Toggle Line Numbers',
   });
-  app.commands.addCommand(cmdIds.lineWrap, {
+
+  commands.addCommand(cmdIds.lineWrap, {
     execute: () => { toggleLineWrap(tracker); },
     label: 'Toggle Line Wrap',
   });
-  app.commands.addCommand(cmdIds.matchBrackets, {
+
+  commands.addCommand(cmdIds.matchBrackets, {
     execute: () => { toggleMatchBrackets(tracker); },
     label: 'Toggle Match Brackets',
   });
-  app.commands.addCommand(cmdIds.vimMode, {
+
+  commands.addCommand(cmdIds.vimMode, {
     execute: () => { toggleVim(tracker); },
     label: 'Toggle Vim Mode'
   });
-  app.commands.addCommand(cmdIds.closeAll, {
+
+  commands.addCommand(cmdIds.closeAll, {
     execute: () => { closeAllFiles(tracker); },
     label: 'Close all files'
   });
+
+  commands.addCommand(cmdIds.createConsole, {
+    execute: () => {
+      let widget = tracker.currentWidget;
+      if (!widget) {
+        return;
+      }
+      let options: any = {
+        path: widget.context.path,
+        preferredLanguage: widget.context.model.defaultKernelLanguage
+      };
+      commands.execute('console:create', options).then(id => {
+        sessionIdProperty.set(widget, id);
+      });
+    },
+    label: 'Create Console for Editor'
+  });
+
+  commands.addCommand(cmdIds.runCode, {
+    execute: () => {
+      let widget = tracker.currentWidget;
+      if (!widget) {
+        return;
+      }
+      // Get the session id.
+      let id = sessionIdProperty.get(widget);
+      if (!id) {
+        return;
+      }
+      // Get the selected code from the editor.
+      let doc = widget.editor.getDoc();
+      let code = doc.getSelection();
+      if (!code) {
+        let { line } = doc.getCursor();
+        code = doc.getLine(line);
+      }
+      commands.execute('console:inject', { id, code });
+    },
+    label: 'Run Code',
+  });
+
+  [
+    cmdIds.lineNumbers,
+    cmdIds.lineWrap,
+    cmdIds.matchBrackets,
+    cmdIds.vimMode,
+    cmdIds.closeAll,
+    cmdIds.createConsole,
+    cmdIds.runCode,
+  ].forEach(command => palette.addItem({ command, category: 'Editor' }));
+
+  return tracker;
 }
 
 
+
+/**
+ * An attached property for the session id associated with an editor widget.
+ */
+const sessionIdProperty = new AttachedProperty<EditorWidget, string>({ name: 'sessionId' });
+
+
+
 /**
  * Toggle editor line numbers
  */
@@ -230,7 +284,7 @@ function createMenu(app: JupyterLab, tracker: IEditorTracker): Menu {
 
   [
    'jupyter', 'default', 'abcdef', 'base16-dark', 'base16-light',
-   'hopscotch', 'material', 'mbo', 'mdn-like', 'seti', 'the-matrix', 
+   'hopscotch', 'material', 'mbo', 'mdn-like', 'seti', 'the-matrix',
    'xq-light', 'zenburn'
   ].forEach(name => theme.addItem({
     command: 'editor:change-theme',

+ 11 - 0
src/editorwidget/widget.ts

@@ -70,6 +70,7 @@ class EditorWidget extends CodeMirrorWidget {
     super({
       extraKeys: {
         'Tab': 'indentMore',
+        'Shift-Enter': () => { /* no-op */ }
       },
       indentUnit: 4,
       theme: DEFAULT_CODEMIRROR_THEME,
@@ -77,6 +78,7 @@ class EditorWidget extends CodeMirrorWidget {
       lineWrapping: true,
     });
     this.addClass(EDITOR_CLASS);
+    this._context = context;
     let editor = this.editor;
     let model = context.model;
     let doc = editor.getDoc();
@@ -109,6 +111,15 @@ class EditorWidget extends CodeMirrorWidget {
       }
     });
   }
+
+  /**
+   * Get the context for the editor widget.
+   */
+  get context(): IDocumentContext<IDocumentModel> {
+    return this._context;
+  }
+
+  private _context: IDocumentContext<IDocumentModel>;
 }
 
 

+ 7 - 2
src/shortcuts/plugin.ts

@@ -35,6 +35,11 @@ const SHORTCUTS = [
     selector: 'body',
     keys: ['Accel Shift P']
   },
+  {
+    command: 'editor:run-code',
+    selector: '.jp-EditorWidget',
+    keys: ['Shift Enter']
+  },
   {
     command: 'file-browser:toggle',
     selector: 'body',
@@ -251,12 +256,12 @@ const SHORTCUTS = [
     keys: ['Ctrl M']
   },
   {
-    command: 'console:execute',
+    command: 'console:run',
     selector: '.jp-ConsolePanel',
     keys: ['Enter']
   },
   {
-    command: 'console:execute-forced',
+    command: 'console:run-forced',
     selector: '.jp-ConsolePanel',
     keys: ['Shift Enter']
   },