From 231dfe880118459f17344569bf1a5a1f94b843d6 2014-11-25 18:30:41 From: Min RK Date: 2014-11-25 18:30:41 Subject: [PATCH] Merge pull request #6866 from takluyver/nb-texteditor Notebook text editor --- diff --git a/IPython/html/edit/__init__.py b/IPython/html/edit/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/IPython/html/edit/__init__.py diff --git a/IPython/html/edit/handlers.py b/IPython/html/edit/handlers.py new file mode 100644 index 0000000..521bd2c --- /dev/null +++ b/IPython/html/edit/handlers.py @@ -0,0 +1,29 @@ +#encoding: utf-8 +"""Tornado handlers for the terminal emulator.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. + +from tornado import web +from ..base.handlers import IPythonHandler, path_regex +from ..utils import url_escape + +class EditorHandler(IPythonHandler): + """Render the text editor interface.""" + @web.authenticated + def get(self, path): + path = path.strip('/') + if not self.contents_manager.file_exists(path): + raise web.HTTPError(404, u'File does not exist: %s' % path) + + basename = path.rsplit('/', 1)[-1] + self.write(self.render_template('edit.html', + file_path=url_escape(path), + basename=basename, + page_title=basename + " (editing)", + ) + ) + +default_handlers = [ + (r"/edit%s" % path_regex, EditorHandler), +] \ No newline at end of file diff --git a/IPython/html/notebookapp.py b/IPython/html/notebookapp.py index b4aed10..270a423 100644 --- a/IPython/html/notebookapp.py +++ b/IPython/html/notebookapp.py @@ -199,6 +199,7 @@ class NotebookWebApplication(web.Application): handlers.extend(load_handlers('notebook.handlers')) handlers.extend(load_handlers('nbconvert.handlers')) handlers.extend(load_handlers('kernelspecs.handlers')) + handlers.extend(load_handlers('edit.handlers')) handlers.extend(load_handlers('services.config.handlers')) handlers.extend(load_handlers('services.kernels.handlers')) handlers.extend(load_handlers('services.contents.handlers')) diff --git a/IPython/html/services/contents/filemanager.py b/IPython/html/services/contents/filemanager.py index 92465e1..2a313ba 100644 --- a/IPython/html/services/contents/filemanager.py +++ b/IPython/html/services/contents/filemanager.py @@ -9,6 +9,7 @@ import io import os import shutil from contextlib import contextmanager +import mimetypes from tornado import web @@ -204,6 +205,7 @@ class FileContentsManager(ContentsManager): model['created'] = created model['content'] = None model['format'] = None + model['mimetype'] = None try: model['writable'] = os.access(os_path, os.W_OK) except OSError: @@ -264,8 +266,11 @@ class FileContentsManager(ContentsManager): """ model = self._base_model(path) model['type'] = 'file' + + os_path = self._get_os_path(path) + model['mimetype'] = mimetypes.guess_type(os_path)[0] or 'text/plain' + if content: - os_path = self._get_os_path(path) if not os.path.isfile(os_path): # could be FIFO raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path) diff --git a/IPython/html/static/base/js/notificationarea.js b/IPython/html/static/base/js/notificationarea.js new file mode 100644 index 0000000..53607fe --- /dev/null +++ b/IPython/html/static/base/js/notificationarea.js @@ -0,0 +1,83 @@ +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. + +define([ + 'jquery', + 'base/js/notificationwidget', +], function($, notificationwidget) { + "use strict"; + + // store reference to the NotificationWidget class + var NotificationWidget = notificationwidget.NotificationWidget; + + /** + * Construct the NotificationArea object. Options are: + * events: $(Events) instance + * save_widget: SaveWidget instance + * notebook: Notebook instance + * keyboard_manager: KeyboardManager instance + * + * @constructor + * @param {string} selector - a jQuery selector string for the + * notification area element + * @param {Object} [options] - a dictionary of keyword arguments. + */ + var NotificationArea = function (selector, options) { + this.selector = selector; + this.events = options.events; + if (this.selector !== undefined) { + this.element = $(selector); + } + this.widget_dict = {}; + }; + + /** + * Get a widget by name, creating it if it doesn't exist. + * + * @method widget + * @param {string} name - the widget name + */ + NotificationArea.prototype.widget = function (name) { + if (this.widget_dict[name] === undefined) { + return this.new_notification_widget(name); + } + return this.get_widget(name); + }; + + /** + * Get a widget by name, throwing an error if it doesn't exist. + * + * @method get_widget + * @param {string} name - the widget name + */ + NotificationArea.prototype.get_widget = function (name) { + if(this.widget_dict[name] === undefined) { + throw('no widgets with this name'); + } + return this.widget_dict[name]; + }; + + /** + * Create a new notification widget with the given name. The + * widget must not already exist. + * + * @method new_notification_widget + * @param {string} name - the widget name + */ + NotificationArea.prototype.new_notification_widget = function (name) { + if (this.widget_dict[name] !== undefined) { + throw('widget with that name already exists!'); + } + + // create the element for the notification widget and add it + // to the notification aread element + var div = $('
').attr('id', 'notification_' + name); + $(this.selector).append(div); + + // create the widget object and return it + this.widget_dict[name] = new NotificationWidget('#notification_' + name); + return this.widget_dict[name]; + }; + + return {'NotificationArea': NotificationArea}; +}); diff --git a/IPython/html/static/notebook/js/notificationwidget.js b/IPython/html/static/base/js/notificationwidget.js similarity index 100% rename from IPython/html/static/notebook/js/notificationwidget.js rename to IPython/html/static/base/js/notificationwidget.js diff --git a/IPython/html/static/edit/js/editor.js b/IPython/html/static/edit/js/editor.js new file mode 100644 index 0000000..48e2e88 --- /dev/null +++ b/IPython/html/static/edit/js/editor.js @@ -0,0 +1,74 @@ +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. + +define([ + 'jquery', + 'base/js/utils', + 'codemirror/lib/codemirror', + 'codemirror/mode/meta', + 'codemirror/addon/search/search' + ], +function($, + utils, + CodeMirror +) { + var Editor = function(selector, options) { + this.selector = selector; + this.contents = options.contents; + this.events = options.events; + this.base_url = options.base_url; + this.file_path = options.file_path; + + this.codemirror = CodeMirror($(this.selector)[0]); + + // It appears we have to set commands on the CodeMirror class, not the + // instance. I'd like to be wrong, but since there should only be one CM + // instance on the page, this is good enough for now. + CodeMirror.commands.save = $.proxy(this.save, this); + + this.save_enabled = false; + }; + + Editor.prototype.load = function() { + var that = this; + var cm = this.codemirror; + this.contents.get(this.file_path, {type: 'file', format: 'text'}) + .then(function(model) { + cm.setValue(model.content); + + // Find and load the highlighting mode + var modeinfo = CodeMirror.findModeByMIME(model.mimetype); + if (modeinfo) { + utils.requireCodeMirrorMode(modeinfo.mode, function() { + cm.setOption('mode', modeinfo.mode); + }); + } + that.save_enabled = true; + }, + function(error) { + cm.setValue("Error! " + error.message + + "\nSaving disabled."); + that.save_enabled = false; + } + ); + }; + + Editor.prototype.save = function() { + if (!this.save_enabled) { + console.log("Not saving, save disabled"); + return; + } + var model = { + path: this.file_path, + type: 'file', + format: 'text', + content: this.codemirror.getValue(), + }; + var that = this; + this.contents.save(this.file_path, model).then(function() { + that.events.trigger("save_succeeded.TextEditor"); + }); + }; + + return {Editor: Editor}; +}); diff --git a/IPython/html/static/edit/js/main.js b/IPython/html/static/edit/js/main.js new file mode 100644 index 0000000..79dac22 --- /dev/null +++ b/IPython/html/static/edit/js/main.js @@ -0,0 +1,53 @@ +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. + +require([ + 'base/js/namespace', + 'base/js/utils', + 'base/js/page', + 'base/js/events', + 'contents', + 'edit/js/editor', + 'edit/js/menubar', + 'edit/js/notificationarea', + 'custom/custom', +], function( + IPython, + utils, + page, + events, + contents, + editor, + menubar, + notificationarea + ){ + page = new page.Page(); + + var base_url = utils.get_body_data('baseUrl'); + var file_path = utils.get_body_data('filePath'); + contents = new contents.Contents({base_url: base_url}); + + var editor = new editor.Editor('#texteditor-container', { + base_url: base_url, + events: events, + contents: contents, + file_path: file_path, + }); + + // Make it available for debugging + IPython.editor = editor; + + var menus = new menubar.MenuBar('#menubar', { + base_url: base_url, + editor: editor, + }); + + var notification_area = new notificationarea.EditorNotificationArea( + '#notification_area', { + events: events, + }); + notification_area.init_notification_widgets(); + + editor.load(); + page.show(); +}); diff --git a/IPython/html/static/edit/js/menubar.js b/IPython/html/static/edit/js/menubar.js new file mode 100644 index 0000000..dd8a5ce --- /dev/null +++ b/IPython/html/static/edit/js/menubar.js @@ -0,0 +1,46 @@ +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. + +define([ + 'base/js/namespace', + 'jquery', + 'base/js/utils', + 'bootstrap', +], function(IPython, $, utils, bootstrap) { + "use strict"; + + var MenuBar = function (selector, options) { + // Constructor + // + // A MenuBar Class to generate the menubar of IPython notebook + // + // Parameters: + // selector: string + // options: dictionary + // Dictionary of keyword arguments. + // codemirror: CodeMirror instance + // contents: ContentManager instance + // events: $(Events) instance + // base_url : string + // file_path : string + options = options || {}; + this.base_url = options.base_url || utils.get_body_data("baseUrl"); + this.selector = selector; + this.editor = options.editor; + + if (this.selector !== undefined) { + this.element = $(selector); + this.bind_events(); + } + }; + + MenuBar.prototype.bind_events = function () { + // File + var that = this; + this.element.find('#save_file').click(function () { + that.editor.save(); + }); + }; + + return {'MenuBar': MenuBar}; +}); diff --git a/IPython/html/static/edit/js/notificationarea.js b/IPython/html/static/edit/js/notificationarea.js new file mode 100644 index 0000000..fd98ebf --- /dev/null +++ b/IPython/html/static/edit/js/notificationarea.js @@ -0,0 +1,29 @@ +define([ + 'base/js/notificationarea' +], function(notificationarea) { + "use strict"; + var NotificationArea = notificationarea.NotificationArea; + + var EditorNotificationArea = function(selector, options) { + NotificationArea.apply(this, [selector, options]); + } + + EditorNotificationArea.prototype = Object.create(NotificationArea.prototype); + + /** + * Initialize the default set of notification widgets. + * + * @method init_notification_widgets + */ + EditorNotificationArea.prototype.init_notification_widgets = function () { + var that = this; + var enw = this.new_notification_widget('editor'); + + this.events.on("save_succeeded.TextEditor", function() { + enw.set_message("File saved", 2000); + }); + }; + + + return {EditorNotificationArea: EditorNotificationArea}; +}); diff --git a/IPython/html/static/notebook/js/main.js b/IPython/html/static/notebook/js/main.js index e7ff773..7208d25 100644 --- a/IPython/html/static/notebook/js/main.js +++ b/IPython/html/static/notebook/js/main.js @@ -105,7 +105,7 @@ require([ save_widget: save_widget, quick_help: quick_help}, common_options)); - var notification_area = new notificationarea.NotificationArea( + var notification_area = new notificationarea.NotebookNotificationArea( '#notification_area', { events: events, save_widget: save_widget, diff --git a/IPython/html/static/notebook/js/notificationarea.js b/IPython/html/static/notebook/js/notificationarea.js index 93ad8a0..2cc4371 100644 --- a/IPython/html/static/notebook/js/notificationarea.js +++ b/IPython/html/static/notebook/js/notificationarea.js @@ -1,97 +1,29 @@ -// Copyright (c) IPython Development Team. -// Distributed under the terms of the Modified BSD License. - define([ 'base/js/namespace', 'jquery', 'base/js/utils', 'base/js/dialog', - 'notebook/js/notificationwidget', + 'base/js/notificationarea', 'moment' -], function(IPython, $, utils, dialog, notificationwidget, moment) { +], function(IPython, $, utils, dialog, notificationarea, moment) { "use strict"; - - // store reference to the NotificationWidget class - var NotificationWidget = notificationwidget.NotificationWidget; - - /** - * Construct the NotificationArea object. Options are: - * events: $(Events) instance - * save_widget: SaveWidget instance - * notebook: Notebook instance - * keyboard_manager: KeyboardManager instance - * - * @constructor - * @param {string} selector - a jQuery selector string for the - * notification area element - * @param {Object} [options] - a dictionary of keyword arguments. - */ - var NotificationArea = function (selector, options) { - this.selector = selector; - this.events = options.events; + var NotificationArea = notificationarea.NotificationArea; + + var NotebookNotificationArea = function(selector, options) { + NotificationArea.apply(this, [selector, options]); this.save_widget = options.save_widget; this.notebook = options.notebook; this.keyboard_manager = options.keyboard_manager; - if (this.selector !== undefined) { - this.element = $(selector); - } - this.widget_dict = {}; - }; - - /** - * Get a widget by name, creating it if it doesn't exist. - * - * @method widget - * @param {string} name - the widget name - */ - NotificationArea.prototype.widget = function (name) { - if (this.widget_dict[name] === undefined) { - return this.new_notification_widget(name); - } - return this.get_widget(name); - }; - - /** - * Get a widget by name, throwing an error if it doesn't exist. - * - * @method get_widget - * @param {string} name - the widget name - */ - NotificationArea.prototype.get_widget = function (name) { - if(this.widget_dict[name] === undefined) { - throw('no widgets with this name'); - } - return this.widget_dict[name]; - }; - - /** - * Create a new notification widget with the given name. The - * widget must not already exist. - * - * @method new_notification_widget - * @param {string} name - the widget name - */ - NotificationArea.prototype.new_notification_widget = function (name) { - if (this.widget_dict[name] !== undefined) { - throw('widget with that name already exists!'); - } - - // create the element for the notification widget and add it - // to the notification aread element - var div = $('
').attr('id', 'notification_' + name); - $(this.selector).append(div); - - // create the widget object and return it - this.widget_dict[name] = new NotificationWidget('#notification_' + name); - return this.widget_dict[name]; - }; - + } + + NotebookNotificationArea.prototype = Object.create(NotificationArea.prototype); + /** * Initialize the default set of notification widgets. * * @method init_notification_widgets */ - NotificationArea.prototype.init_notification_widgets = function () { + NotebookNotificationArea.prototype.init_notification_widgets = function () { this.init_kernel_notification_widget(); this.init_notebook_notification_widget(); }; @@ -101,7 +33,7 @@ define([ * * @method init_kernel_notification_widget */ - NotificationArea.prototype.init_kernel_notification_widget = function () { + NotebookNotificationArea.prototype.init_kernel_notification_widget = function () { var that = this; var knw = this.new_notification_widget('kernel'); var $kernel_ind_icon = $("#kernel_indicator_icon"); @@ -324,7 +256,7 @@ define([ * * @method init_notebook_notification_widget */ - NotificationArea.prototype.init_notebook_notification_widget = function () { + NotebookNotificationArea.prototype.init_notebook_notification_widget = function () { var nnw = this.new_notification_widget('notebook'); // Notebook events @@ -381,7 +313,8 @@ define([ }); }; - IPython.NotificationArea = NotificationArea; - - return {'NotificationArea': NotificationArea}; + // Backwards compatibility. + IPython.NotificationArea = NotebookNotificationArea; + + return {'NotebookNotificationArea': NotebookNotificationArea}; }); diff --git a/IPython/html/templates/edit.html b/IPython/html/templates/edit.html new file mode 100644 index 0000000..7841bd4 --- /dev/null +++ b/IPython/html/templates/edit.html @@ -0,0 +1,72 @@ +{% extends "page.html" %} + +{% block title %}{{page_title}}{% endblock %} + +{% block stylesheet %} + + + + +{{super()}} +{% endblock %} + +{% block params %} + +data-base-url="{{base_url}}" +data-file-path="{{file_path}}" + +{% endblock %} + +{% block header %} + +{{ basename }} + +{% endblock %} + +{% block site %} + + + +
+ +{% endblock %} + +{% block script %} + + {{super()}} + + +{% endblock %}