123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370 |
- // Copyright (c) Jupyter Development Team.
- // Distributed under the terms of the Modified BSD License.
- define([
- 'jquery',
- 'base/js/utils',
- 'base/js/i18n',
- 'base/js/dialog',
- 'codemirror/lib/codemirror',
- 'codemirror/mode/meta',
- 'codemirror/addon/comment/comment',
- 'codemirror/addon/dialog/dialog',
- 'codemirror/addon/edit/closebrackets',
- 'codemirror/addon/edit/matchbrackets',
- 'codemirror/addon/search/searchcursor',
- 'codemirror/addon/search/search',
- 'codemirror/keymap/emacs',
- 'codemirror/keymap/sublime',
- 'codemirror/keymap/vim',
- ],
- function(
- $,
- utils,
- i18n,
- dialog,
- CodeMirror
- ) {
- "use strict";
- var Editor = function(selector, options) {
- var that = this;
- this.selector = selector;
- this.clean = false;
- this.contents = options.contents;
- this.events = options.events;
- this.base_url = options.base_url;
- this.file_path = options.file_path;
- this.config = options.config;
- this.file_extension_modes = options.file_extension_modes || {};
- this.last_modified = null;
- this._changed_on_disk_dialog = null;
- this.codemirror = new CodeMirror($(this.selector)[0]);
- this.codemirror.on('changes', function(cm, changes){
- that._clean_state();
- });
- this.generation = -1;
-
- // 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;
-
- this.config.loaded.then(function () {
- // load codemirror config
- var cfg = that.config.data.Editor || {};
- var cmopts = $.extend(true, {}, // true = recursive copy
- Editor.default_codemirror_options,
- cfg.codemirror_options || {}
- );
- that._set_codemirror_options(cmopts);
- that.events.trigger('config_changed.Editor', {config: that.config});
- if (cfg.file_extension_modes) {
- // check for file extension in user preferences
- var modename = cfg.file_extension_modes[that._get_file_extension()];
- if (modename) {
- var modeinfo = CodeMirror.findModeByName(modename);
- if (modeinfo) {
- that.set_codemirror_mode(modeinfo);
- }
- }
- }
- that._clean_state();
- });
- this.clean_sel = $('<div/>');
- $('.last_modified').before(this.clean_sel);
- this.clean_sel.addClass('dirty-indicator-dirty');
- };
-
- // default CodeMirror options
- Editor.default_codemirror_options = {
- extraKeys: {
- "Cmd-Right": "goLineRight",
- "End": "goLineRight",
- "Cmd-Left": "goLineLeft",
- "Tab": "indentMore",
- "Shift-Tab" : "indentLess",
- "Cmd-/" : "toggleComment",
- "Ctrl-/" : "toggleComment",
- },
- indentUnit: 4,
- theme: "ipython",
- lineNumbers: true,
- lineWrapping: true
- };
-
- Editor.prototype.load = function() {
- /** load the file */
- var that = this;
- var cm = this.codemirror;
- return this.contents.get(this.file_path, {type: 'file', format: 'text'})
- .then(function(model) {
- cm.setValue(model.content);
- // Setting the file's initial value creates a history entry,
- // which we don't want.
- cm.clearHistory();
- that._set_mode_for_model(model);
- that.save_enabled = true;
- that.generation = cm.changeGeneration();
- that.events.trigger("file_loaded.Editor", model);
- that._clean_state();
- that.last_modified = new Date(model.last_modified);
- }).catch(
- function(error) {
- that.events.trigger("file_load_failed.Editor", error);
- console.warn('Error loading: ', error);
- cm.setValue("Error! " + error.message +
- "\nSaving disabled.\nSee Console for more details.");
- cm.setOption('readOnly','nocursor');
- that.save_enabled = false;
- }
- );
- };
-
- Editor.prototype._set_mode_for_model = function (model) {
- /** Set the CodeMirror mode based on the file model */
- // Find and load the highlighting mode,
- // first by mime-type, then by file extension
- var modeinfo;
- var ext = this._get_file_extension();
- if (ext) {
- // check if a mode has been remembered for this extension
- var modename = this.file_extension_modes[ext];
- if (modename) {
- modeinfo = CodeMirror.findModeByName(modename);
- }
- }
- // prioritize CodeMirror's filename identification
- if (!modeinfo || modeinfo.mode === "null") {
- modeinfo = CodeMirror.findModeByFileName(model.name);
- // codemirror's filename identification is case-sensitive.
- // try once more with lowercase extension
- if (!modeinfo && ext) {
- // CodeMirror wants lowercase ext without leading '.'
- modeinfo = CodeMirror.findModeByExtension(ext.slice(1).toLowerCase());
- }
- }
- if (model.mimetype && (!modeinfo || modeinfo.mode === "null")) {
- // mimetype is not set on file rename
- modeinfo = CodeMirror.findModeByMIME(model.mimetype);
- }
- if (modeinfo) {
- this.set_codemirror_mode(modeinfo);
- }
- };
- Editor.prototype.set_codemirror_mode = function (modeinfo) {
- /** set the codemirror mode from a modeinfo struct */
- var that = this;
- utils.requireCodeMirrorMode(modeinfo, function () {
- that.codemirror.setOption('mode', modeinfo.mime);
- that.events.trigger("mode_changed.Editor", modeinfo);
- }, function(err) {
- console.log('Error getting CodeMirror mode: ' + err);
- });
- };
- Editor.prototype.save_codemirror_mode = function (modeinfo) {
- /** save the selected codemirror mode for the current extension in config */
- var update_mode_map = {};
- var ext = this._get_file_extension();
- // no extension, nothing to save
- // TODO: allow remembering no-extension things like Makefile?
- if (!ext) return;
- update_mode_map[ext] = modeinfo.name;
- return this.config.update({
- Editor: {
- file_extension_modes: update_mode_map,
- }
- });
- };
- Editor.prototype.get_filename = function () {
- return utils.url_path_split(this.file_path)[1];
- };
- Editor.prototype._get_file_extension = function () {
- /** return file extension *including* .
-
- Returns undefined if no extension is found.
- */
- var filename = this.get_filename();
- var ext_idx = filename.lastIndexOf('.');
- if (ext_idx < 0) {
- return;
- } else {
- return filename.slice(ext_idx);
- }
- };
- /**
- * Rename the file.
- * @param {string} new_name
- * @return {Promise} promise that resolves when the file is renamed.
- */
- Editor.prototype.rename = function (new_name) {
- /** rename the file */
- var that = this;
- var parent = utils.url_path_split(this.file_path)[0];
- var new_path = utils.url_path_join(parent, new_name);
- return this.contents.rename(this.file_path, new_path).then(
- function (model) {
- that.file_path = model.path;
- that.events.trigger('file_renamed.Editor', model);
- that.last_modified = new Date(model.last_modified);
- that._set_mode_for_model(model);
- that._clean_state();
- }
- );
- };
-
- /**
- * Save this file on the server.
- *
- * @param {boolean} check_last_modified - checks if file has been modified on disk
- * @return {Promise} - promise that resolves when the notebook is saved.
- */
- Editor.prototype.save = function (check_last_modified) {
- /** save the file */
- if (!this.save_enabled) {
- console.log("Not saving, save disabled");
- return;
- }
- // used to check for last modified saves
- if (check_last_modified === undefined) {
- check_last_modified = true;
- }
- var model = {
- path: this.file_path,
- type: 'file',
- format: 'text',
- content: this.codemirror.getValue(),
- };
- var that = this;
- var _save = function () {
- that.events.trigger("file_saving.Editor");
- return that.contents.save(that.file_path, model).then(function(data) {
- // record change generation for isClean
- that.generation = that.codemirror.changeGeneration();
- that.events.trigger("file_saved.Editor", data);
- that.last_modified = new Date(data.last_modified);
- that._clean_state();
- });
- };
- /*
- * Gets the current working file, and checks if the file has been modified on disk. If so, it
- * creates & opens a modal that issues the user a warning and prompts them to overwrite the file.
- *
- * If it can't get the working file, it builds a new file and saves.
- */
- if (check_last_modified) {
- return this.contents.get(that.file_path, {content: false}).then(
- function check_if_modified(data) {
- var last_modified = new Date(data.last_modified);
- // We want to check last_modified (disk) > that.last_modified (our last save)
- // In some cases the filesystem reports an inconsistent time,
- // so we allow 0.5 seconds difference before complaining.
- if ((last_modified.getTime() - that.last_modified.getTime()) > 500) { // 500 ms
- console.warn("Last saving was done on `"+that.last_modified+"`("+that._last_modified+"), "+
- "while the current file seem to have been saved on `"+data.last_modified+"`");
- if (that._changed_on_disk_dialog !== null) {
- // since the modal's event bindings are removed when destroyed, we reinstate
- // save & reload callbacks on the confirmation & reload buttons
- that._changed_on_disk_dialog.find('.save-confirm-btn').click(_save);
- that._changed_on_disk_dialog.find('.btn-warning').click(function () {window.location.reload()});
-
- // redisplay existing dialog
- that._changed_on_disk_dialog.modal('show');
- } else {
- // create new dialog
- that._changed_on_disk_dialog = dialog.modal({
- keyboard_manager: that.keyboard_manager,
- title: i18n.msg._("File changed"),
- body: i18n.msg._("The file has changed on disk since the last time we opened or saved it. "
- + "Do you want to overwrite the file on disk with the version open here, or load "
- + "the version on disk (reload the page)?"),
- buttons: {
- Reload: {
- class: 'btn-warning',
- click: function () {
- window.location.reload();
- }
- },
- Cancel: {},
- Overwrite: {
- class: 'btn-danger save-confirm-btn',
- click: function () {
- _save();
- }
- },
- }
- });
- }
- } else {
- return _save();
- }
- }, function (error) {
- console.log(error);
- // maybe it has been deleted or renamed? Go ahead and save.
- return _save();
- })
- } else {
- return _save();
- }
- };
- Editor.prototype._clean_state = function(){
- var clean = this.codemirror.isClean(this.generation);
- if (clean === this.clean){
- return;
- } else {
- this.clean = clean;
- }
- if(clean){
- this.events.trigger("save_status_clean.Editor");
- this.clean_sel.attr('class','dirty-indicator-clean').attr('title','No changes to save');
- } else {
- this.events.trigger("save_status_dirty.Editor");
- this.clean_sel.attr('class','dirty-indicator-dirty').attr('title','Unsaved changes');
- }
- };
- Editor.prototype._set_codemirror_options = function (options) {
- // update codemirror options from a dict
- var codemirror = this.codemirror;
- $.map(options, function (value, opt) {
- if (value === null) {
- value = CodeMirror.defaults[opt];
- }
- codemirror.setOption(opt, value);
- });
- var that = this;
- };
- Editor.prototype.update_codemirror_options = function (options) {
- /** update codemirror options locally and save changes in config */
- var that = this;
- this._set_codemirror_options(options);
- return this.config.update({
- Editor: {
- codemirror_options: options
- }
- }).then(
- that.events.trigger('config_changed.Editor', {config: that.config})
- );
- };
- return {Editor: Editor};
- });
|