Created
October 12, 2013 22:17
-
-
Save williamstein/6955561 to your computer and use it in GitHub Desktop.
This is the code I wrote as part of https://cloud.sagemath.com in order to integrate IPython notebooks into my existing document synchronization infrastructure. It's CoffeeScript code that isn't meant to be run-able (I just copied it out of a bigger file). I just thought there could be some value in making this available under a BSD license, sin…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
############################################################################### | |
# Copyright (c) 2013, William Stein | |
# All rights reserved. | |
# | |
# Redistribution and use in source and binary forms, with or without | |
# modification, are permitted provided that the following conditions are met: | |
# | |
# 1. Redistributions of source code must retain the above copyright notice, this | |
# list of conditions and the following disclaimer. | |
# 2. Redistributions in binary form must reproduce the above copyright notice, | |
# this list of conditions and the following disclaimer in the documentation | |
# and/or other materials provided with the distribution. | |
# | |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | |
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | |
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
############################################################################### | |
#************************************************** | |
# IPython Support | |
#************************************************** | |
ipython_notebook_server = (opts) -> | |
opts = defaults opts, | |
project_id : required | |
path : required # directory from which the files are served | |
cb : required # cb(err, server) | |
I = new IPythonNotebookServer(opts.project_id, opts.path) | |
I.start_server (err, base) => | |
opts.cb(err, I) | |
class IPythonNotebookServer # call ipython_notebook_server above | |
constructor: (@project_id, @path) -> | |
start_server: (cb) => | |
salvus_client.exec | |
project_id : @project_id | |
path : @path | |
command : "ipython-notebook" | |
args : ['start'] | |
bash : false | |
timeout : 10 | |
err_on_exit: false | |
cb : (err, output) => | |
if err | |
cb?(err) | |
else | |
try | |
info = misc.from_json(output.stdout) | |
if info.error? | |
cb?(info.error) | |
else | |
@url = info.base; @pid = info.pid; @port = info.port | |
get_with_retry | |
url : @url | |
cb : (err, data) => cb?(err) | |
catch e | |
cb?(true) | |
notebooks: (cb) => # cb(err, [{kernel_id:?, name:?, notebook_id:?}, ...] # kernel_id is null if not running | |
get_with_retry | |
url : @url + 'notebooks' | |
cb : (err, data) => | |
if not err | |
cb(false, misc.from_json(data)) | |
else | |
cb(err) | |
stop_server: (cb) => | |
if not @pid? | |
cb?(); return | |
salvus_client.exec | |
project_id : @project_id | |
path : @path | |
command : "ipython-notebook" | |
args : ['stop'] | |
bash : false | |
timeout : 15 | |
cb : (err, output) => | |
cb?(err) | |
# Download a remote URL, possibly retrying repeatedly with exponetial backoff, only failing | |
# if the delay until next retry hits max_delay. | |
# If the downlaod URL contains bad_string (default: 'ECONNREFUSED'), also retry. | |
get_with_retry = (opts) -> | |
opts = defaults opts, | |
url : required | |
initial_delay : 100 | |
max_delay : 7000 # once delay hits this, give up | |
factor : 1.2 # for exponential backoff | |
bad_string : 'ECONNREFUSED' | |
cb : required # cb(err, data) # data = content of that url | |
delay = opts.initial_delay | |
f = () => | |
if delay >= opts.max_delay # too many attempts | |
opts.cb("unable to connect to remote server") | |
return | |
$.get(opts.url, (data) -> | |
if data.indexOf(opts.bad_string) != -1 | |
delay *= opts.factor | |
setTimeout(f, delay) | |
else | |
opts.cb(false, data) | |
).fail(() -> | |
delay *= 1.2 | |
setTimeout(f, delay) | |
) | |
f() | |
# Embedded editor for editing IPython notebooks. Enhanced with sync and integrated into the | |
# overall cloud look. | |
class IPythonNotebook extends FileEditor | |
constructor: (@editor, @filename, url, opts) -> | |
opts = @opts = defaults opts, | |
sync_interval : 500 | |
cursor_interval : 2000 | |
@element = templates.find(".salvus-ipython-notebook").clone() | |
if window.salvus_base_url != "" | |
# TODO: having a base_url doesn't imply necessarily that we're in a dangerous devel mode... | |
# (this is just a warning). | |
# The solutiion for this issue will be to set a password whenever ipython listens on localhost. | |
@element.find(".salvus-ipython-notebook-danger").show() | |
@status_element = @element.find(".salvus-ipython-notebook-status-messages") | |
@init_buttons() | |
s = path_split(@filename) | |
@path = s.head | |
@file = s.tail | |
if @path | |
@syncdoc_filename = @path + '/.' + @file + ".syncdoc" | |
else | |
@syncdoc_filename = '.' + @file + ".syncdoc" | |
# This is where we put the page itself | |
@notebook = @element.find(".salvus-ipython-notebook-notebook") | |
@con = @element.find(".salvus-ipython-notebook-connecting") | |
@setup () => | |
# TODO: We have to do this stupid thing because in IPython's notebook.js they don't systematically use | |
# set_dirty, sometimes instead just directly seting the flag. So there's no simple way to know exactly | |
# when the notebook is dirty. | |
# Also, note there are cases where IPython doesn't set the dirty flag | |
# even though the output has changed. For example, if you type "123" in a cell, run, then | |
# comment out the line and shift-enter again, the empty output doesn't get sync'd out until you do | |
# something else. If any output appears then the dirty happens. I guess this is a bug that should be fixed in ipython. | |
@_autosync_interval = setInterval(@autosync, @opts.sync_interval) | |
@_cursor_interval = setInterval(@broadcast_cursor_pos, @opts.cursor_interval) | |
status: (text) => | |
if not text? | |
text = "" | |
@status_element.html(text) | |
setup: (cb) => | |
if @_setting_up | |
cb?("already setting up") | |
return # already setting up | |
@_setting_up = true | |
@con.show().icon_spin(start:true) | |
# Delete all the cached cursors in the DOM | |
delete @_cursors | |
delete @nb | |
delete @frame | |
async.series([ | |
(cb) => | |
@status("determining newest ipynb file") | |
salvus_client.exec | |
project_id : @editor.project_id | |
path : @path | |
command : "ls" | |
args : ['-lt', "--time-style=+%s", @file, @syncdoc_filename] | |
timeout : 10 | |
err_on_exit: false | |
cb : (err, output) => | |
if err? | |
cb(err) | |
else if output.stderr.indexOf('No such file or directory') != -1 | |
# nothing to do -- the syncdoc file doesn't even exist. | |
cb() | |
else | |
# figure out the two times and see if the .ipynb file is at least 10 seconds (say) | |
# newer than the syncdoc. | |
#~$ ls -l --time-style=+%s .2013-09-06-080011.ipynb.syncdoc 2013-09-06-080011.ipynb | |
#-rw-rw-r-- 1 ccnIX7aT ccnIX7aT 43560 1378514636 2013-09-06-080011.ipynb | |
#-rw-rw-r-- 1 ccnIX7aT ccnIX7aT 41821 1378513328 .2013-09-06-080011.ipynb.syncdoc | |
v = output.stdout.split('\n') | |
a = {} | |
a[v[0][6]] = parseInt(v[0][5]) | |
a[v[1][6]] = parseInt(v[1][5]) | |
if a[@file] >= a[@syncdoc_filename] + 10 | |
@_use_disk_file = true | |
cb() | |
(cb) => | |
@status("ensuring syncdoc exists") | |
@editor.project_page.ensure_file_exists | |
path : @syncdoc_filename | |
cb : cb | |
(cb) => | |
@initialize(cb) | |
(cb) => | |
@init_autosave() | |
@_init_doc(cb) | |
], (err) => | |
@con.show().icon_spin(false).hide() | |
@_setting_up = false | |
if err | |
@save_button.addClass("disabled") | |
@status("failed to start -- #{err}") | |
cb?("Unable to start IPython server -- #{err}") | |
else | |
cb?() | |
) | |
_init_doc: (cb) => | |
#console.log("_init_doc") | |
@status("connecting to sync session") | |
@doc = syncdoc.synchronized_string | |
project_id : @editor.project_id | |
filename : @syncdoc_filename | |
sync_interval : @opts.sync_interval | |
cb : (err) => | |
@status() | |
if err | |
cb?("Unable to connect to synchronized document server -- #{err}") | |
else | |
if @_use_disk_file | |
@doc.live('') | |
@_config_doc() | |
cb?() | |
_config_doc: () => | |
#console.log("_config_doc") | |
# todo -- should check if .ipynb file is newer... ? | |
@status("setting visible document to sync") | |
if @doc.live() == '' | |
@doc.live(@to_doc()) | |
else | |
@set_live_from_syncdoc() | |
#console.log("DONE SETTING!") | |
@iframe.animate(opacity:1) | |
@doc._presync = () => | |
if not @nb? | |
# no point -- reinitializing the notebook frame right now... | |
return | |
@doc.live(@to_doc()) | |
apply_edits = @doc.dsync_client._apply_edits_to_live | |
apply_edits2 = (patch, cb) => | |
#console.log("_apply_edits_to_live ")#-- #{JSON.stringify(patch)}") | |
before = @to_doc() | |
@doc.dsync_client.live = before | |
apply_edits(patch) | |
if @doc.dsync_client.live != before | |
@from_doc(@doc.dsync_client.live) | |
#console.log("edits should now be applied!")#, @doc.dsync_client.live) | |
cb?() | |
@doc.dsync_client._apply_edits_to_live = apply_edits2 | |
@doc.on "reconnect", () => | |
if not @doc.dsync_client? | |
# this could be an older connect emit that didn't get handled -- ignore. | |
return | |
apply_edits = @doc.dsync_client._apply_edits_to_live | |
@doc.dsync_client._apply_edits_to_live = apply_edits2 | |
# Update the live document with the edits that we missed when offline | |
@status("reconnect - updating live doc with missed edits") | |
@from_doc(@doc.dsync_client.live) | |
@status() | |
# TODO: we should just create a class that derives from SynchronizedString at this point. | |
@doc.draw_other_cursor = (pos, color, name) => | |
if not @_cursors? | |
@_cursors = {} | |
id = color + name | |
cursor_data = @_cursors[id] | |
if not cursor_data? | |
cursor = templates.find(".salvus-editor-codemirror-cursor").clone().show() | |
# craziness -- now move it into the iframe! | |
cursor = @frame.$("<div>").html(cursor.html()) | |
cursor.css(position: 'absolute', width:'15em') | |
inside = cursor.find(".salvus-editor-codemirror-cursor-inside") | |
inside.css | |
'background-color': color | |
position : 'absolute' | |
top : '-1.3em' | |
left: '.5ex' | |
height : '1.15em' | |
width : '.1ex' | |
'border-left': '2px solid black' | |
border : '1px solid #aaa' | |
opacity :'.7' | |
label = cursor.find(".salvus-editor-codemirror-cursor-label") | |
label.css | |
color:'color' | |
position:'absolute' | |
top:'-2.3em' | |
left:'1.5ex' | |
'font-size':'8pt' | |
'font-family':'serif' | |
'z-index':10000 | |
label.text(name) | |
cursor_data = {cursor: cursor, pos:pos} | |
@_cursors[id] = cursor_data | |
else | |
cursor_data.pos = pos | |
# first fade the label out | |
cursor_data.cursor.find(".salvus-editor-codemirror-cursor-label").stop().show().animate(opacity:1).fadeOut(duration:16000) | |
# Then fade the cursor out (a non-active cursor is a waste of space). | |
cursor_data.cursor.stop().show().animate(opacity:1).fadeOut(duration:60000) | |
@nb.get_cell(pos.index).code_mirror.addWidget( | |
{line:pos.line,ch:pos.ch}, cursor_data.cursor[0], false) | |
@status() | |
broadcast_cursor_pos: () => | |
if not @nb? | |
# no point -- reloading or loading | |
return | |
index = @nb.get_selected_index() | |
cell = @nb.get_cell(index) | |
pos = cell.code_mirror.getCursor() | |
s = misc.to_json(pos) | |
if s != @_last_cursor_pos | |
@_last_cursor_pos = s | |
@doc.broadcast_cursor_pos(index:index, line:pos.line, ch:pos.ch) | |
remove: () => | |
if @_sync_check_interval? | |
clearInterval(@_sync_check_interval) | |
if @_cursor_interval? | |
clearInterval(@_cursor_interval) | |
if @_autosync_interval? | |
clearInterval(@_autosync_interval) | |
@element.remove() | |
@doc?.disconnect_from_session() | |
get_ids: (cb) => # cb(err); if no error, sets @kernel_id and @notebook_id, though @kernel_id will be null if not started | |
if not @server? | |
cb("cannot call get_ids until connected to the ipython notebook server."); return | |
@status("getting notebook and kernel id") | |
@server.notebooks (err, notebooks) => | |
@status() | |
if err | |
cb(err); return | |
for n in notebooks | |
if n.name + '.ipynb' == @file | |
@kernel_id = n.kernel_id # will be null if kernel not yet started | |
@notebook_id = n.notebook_id | |
cb(); return | |
cb("no ipython notebook listed by server with name '#{@file}'") | |
initialize: (cb) => | |
async.series([ | |
(cb) => | |
@status("getting or starting ipython server") | |
ipython_notebook_server | |
project_id : @editor.project_id | |
path : @path | |
cb : (err, server) => | |
@server = server | |
cb(err) | |
(cb) => | |
@get_ids(cb) | |
(cb) => | |
@_init_iframe(cb) | |
(cb) => | |
# start polling until we get the kernel_id | |
attempts = 0 | |
f = () => | |
attempts += 1 | |
if attempts < 20 | |
@get_ids () => | |
if not @kernel_id? | |
setTimeout(f,500) | |
else | |
cb() | |
else | |
cb("unable to get kernel id") | |
setTimeout(f, 250) | |
], cb) | |
_init_iframe: (cb) => | |
if not @notebook_id? | |
# assumes @notebook_id has been set | |
cb("must first call get_ids"); return | |
@status("initializing iframe") | |
get_with_retry | |
url : @server.url | |
cb : (err) => | |
if err | |
@status() | |
cb(err); return | |
@iframe_uuid = misc.uuid() | |
@status("loading iframe") | |
@iframe = $("<iframe name=#{@iframe_uuid} id=#{@iframe_uuid}>").css('opacity','.3').attr('src', @server.url + @notebook_id) | |
@notebook.html('').append(@iframe) | |
@show() | |
# Monkey patch the IPython html so clicking on the IPython logo pops up a a new tab with the dashboard, | |
# instead of messing up our embedded view. | |
attempts = 0 | |
f = () => | |
#console.log("kernel = ", @frame?.IPython?.notebook?.kernel) | |
attempts += 1 | |
if attempts >= 40 | |
# just give up -- this isn't at all critical; don't want to waste resources | |
@status() | |
cb() | |
return | |
@frame = window.frames[@iframe_uuid] | |
if not @frame? or not @frame.$? or not @frame.IPython? or not @frame.IPython.notebook? or not @frame.IPython.notebook.kernel? | |
setTimeout(f, 250) | |
else | |
a = @frame.$("#ipython_notebook").find("a") | |
if a.length == 0 | |
setTimeout(f, 250) | |
else | |
@ipython = @frame.IPython | |
@nb = @ipython.notebook | |
a.click () => | |
@info() | |
return false | |
# Replace the IPython Notebook logo, which is for some weird reason an ugly png, with proper HTML; this ensures the size | |
# and color match everything else. | |
a.html('<span style="font-size: 18pt;"><span style="color:black">IP</span>[<span style="color:black">y</span>]: Notebook</span>') | |
# proper file rename with sync not supported yet (but will be -- TODO; needs to work with sync system) | |
@frame.$("#notebook_name").unbind('click').css("line-height",'0em') | |
# Get rid of file menu, which weirdly and wrongly for sync replicates everything. | |
for cmd in ['new', 'open', 'copy', 'rename'] | |
@frame.$("#" + cmd + "_notebook").remove() | |
@frame.$("#kill_and_exit").remove() | |
#@frame.$("#save_checkpoint").remove() | |
#@frame.$("#restore_checkpoint").remove() | |
@frame.$("#menus").find("li:first").find(".divider").remove() | |
#@frame.$("#autosave_status").remove() | |
#@frame.$("#checkpoint_status").remove() | |
@frame.$('<style type=text/css></style>').html(".container{width:98%; margin-left: 0;}").appendTo(@frame.$("body")) | |
@nb._save_checkpoint = @nb.save_checkpoint | |
@nb.save_checkpoint = @save | |
# Ipython doesn't consider a load (e.g., snapshot restore) "dirty" (for obvious reasons!) | |
@nb._load_notebook_success = @nb.load_notebook_success | |
@nb.load_notebook_success = (data,status,xhr) => | |
@nb._load_notebook_success(data,status,xhr) | |
@sync() | |
@status() | |
cb() | |
setTimeout(f, 100) | |
# although highly unlikely, this could happen if something else steals our port before we can restart... | |
check_for_moved_server: () => | |
if @nb?.kernel? # only try if nb is already loaded | |
if not @nb.kernel.shell_channel # if backend is gone/replaced, then this would get set to null | |
ipython_notebook_server | |
project_id : @editor.project_id | |
path : @path | |
cb : (err, server) => | |
if err | |
# nothing to be done. | |
return | |
if server.url != @server.url | |
# server moved!? | |
@server = server | |
@reload() # -- only thing we can do, really | |
autosync: () => | |
@check_for_moved_server() # only bother if document being changed. | |
if @frame?.IPython?.notebook?.dirty | |
@save_button.removeClass('disabled') | |
@sync() | |
@nb.dirty = false | |
sync: () => | |
@save_button.icon_spin(start:true,delay:1000) | |
@doc.sync () => | |
@save_button.icon_spin(false) | |
has_unsaved_changes: () => | |
return not @save_button.hasClass('disabled') | |
save: (cb) => | |
if not @nb? | |
cb?(); return | |
@save_button.icon_spin(start:true,delay:500) | |
@nb._save_checkpoint?() | |
@doc.save () => | |
@save_button.icon_spin(false) | |
@save_button.addClass('disabled') | |
cb?() | |
set_live_from_syncdoc: () => | |
if not @doc?.dsync_client? # could be re-initializing | |
return | |
current = @to_doc() | |
if @doc.dsync_client.live != current | |
@from_doc(@doc.dsync_client.live) | |
info: () => | |
t = "<h3>The IPython Notebook</h3>" | |
t += "<h4>Enhanced with Sagemath Cloud Sync</h4>" | |
t += "You are editing this document using the IPython Notebook enhanced with realtime synchronization." | |
if @kernel_id? | |
t += "<h4>Sage mode by pasting this into a cell</h4>" | |
t += "<pre>%load_ext sage.misc.sage_extension</pre>" | |
if @kernel_id? | |
t += "<h4>Connect to this IPython kernel in a terminal</h4>" | |
t += "<pre>ipython console --existing #{@kernel_id}</pre>" | |
if @server.url? | |
t += "<h4>Pure IPython notebooks</h4>" | |
t += "You can also directly use an <a target='_blank' href='#{@server.url}'>unmodified version of the IPython Notebook server</a> (this link works for all project collaborators). " | |
t += "<br><br>To start your own unmodified IPython Notebook server that is securely accessible to collaborators, type in a terminal <br><br><pre>ipython-notebook run</pre>" | |
bootbox.alert(t) | |
return false | |
reload: () => | |
if @_reloading | |
return | |
@_reloading = true | |
@reload_button.icon_spin(true) | |
@setup (e) => | |
@_reloading = false | |
@reload_button.icon_spin(false) | |
init_buttons: () => | |
@element.find("a").tooltip() | |
@save_button = @element.find("a[href=#save]").click () => | |
@save() | |
return false | |
@reload_button = @element.find("a[href=#reload]").click () => | |
@reload() | |
return false | |
#@element.find("a[href=#json]").click () => | |
# console.log(@to_obj()) | |
@element.find("a[href=#info]").click () => | |
@info() | |
return false | |
@element.find("a[href=#close]").click () => | |
@editor.project_page.display_tab("project-file-listing") | |
return false | |
@element.find("a[href=#execute]").click () => | |
@nb.execute_selected_cell() | |
return false | |
@element.find("a[href=#interrupt]").click () => | |
@nb.kernel.interrupt() | |
return false | |
@element.find("a[href=#tab]").click () => | |
@nb.get_cell(@nb?.get_selected_index()).completer.startCompletion() | |
return false | |
to_obj: () => | |
#console.log("to_obj: start"); t = misc.mswalltime() | |
obj = @nb.toJSON() | |
obj.metadata.name = @nb.notebook_name | |
obj.nbformat = @nb.nbformat | |
obj.nbformat_minor = @nb.nbformat_minor | |
#console.log("to_obj: done", misc.mswalltime(t)) | |
return obj | |
from_obj: (obj) => | |
#console.log("from_obj: start"); t = misc.mswalltime() | |
i = @nb.get_selected_index() | |
st = @nb.element.scrollTop() | |
@nb.fromJSON(obj) | |
@nb.dirty = false | |
@nb.select(i) | |
@nb.element.scrollTop(st) | |
#console.log("from_obj: done", misc.mswalltime(t)) | |
# Notebook Doc Format: line 0 is meta information in JSON; one line with the JSON of each cell for reset of file | |
to_doc: () => | |
#console.log("to_doc: start"); t = misc.mswalltime() | |
obj = @to_obj() | |
doc = misc.to_json({notebook_name:obj.metadata.name}) | |
for cell in obj.worksheets[0].cells | |
doc += '\n' + misc.to_json(cell) | |
#console.log("to_doc: done", misc.mswalltime(t)) | |
return doc | |
### | |
# simplistic version of modifying the notebook in place. VERY slow when new cell added. | |
from_doc0: (doc) => | |
#console.log("from_doc: start"); t = misc.mswalltime() | |
nb = @nb | |
v = doc.split('\n') | |
nb.metadata.name = v[0].notebook_name | |
cells = [] | |
for line in v.slice(1) | |
try | |
c = misc.from_json(line) | |
cells.push(c) | |
catch e | |
console.log("error de-jsoning '#{line}'", e) | |
obj = @to_obj() | |
obj.worksheets[0].cells = cells | |
@from_obj(obj) | |
console.log("from_doc: done", misc.mswalltime(t)) | |
### | |
delete_cell: (index) => | |
@nb.delete_cell(index) | |
insert_cell: (index, cell_data) => | |
new_cell = @nb.insert_cell_at_index(cell_data.cell_type, index) | |
new_cell.fromJSON(cell_data) | |
set_cell: (index, cell_data) => | |
#console.log("set_cell: start"); t = misc.mswalltime() | |
cell = @nb.get_cell(index) | |
if cell? and cell_data.cell_type == cell.cell_type | |
#console.log("setting in place") | |
if cell.output_area? | |
# for some reason fromJSON doesn't clear the output (it should, imho), and the clear_output method | |
# on the output_area doesn't work as expected. | |
wrapper = cell.output_area.wrapper | |
wrapper.empty() | |
cell.output_area = new @ipython.OutputArea(wrapper, true) | |
cell.fromJSON(cell_data) | |
### for debugging that we properly update a cell in place -- if this is wrong, | |
# all hell breaks loose, and sync loops ensue. | |
a = misc.to_json(cell_data) | |
b = misc.to_json(cell.toJSON()) | |
if a != b | |
console.log("didn't work:") | |
console.log(a) | |
console.log(b) | |
@nb.delete_cell(index) | |
new_cell = @nb.insert_cell_at_index(cell_data.cell_type, index) | |
new_cell.fromJSON(cell_data) | |
### | |
else | |
#console.log("replacing") | |
@nb.delete_cell(index) | |
new_cell = @nb.insert_cell_at_index(cell_data.cell_type, index) | |
new_cell.fromJSON(cell_data) | |
#console.log("set_cell: done", misc.mswalltime(t)) | |
### | |
# simplistic version of setting from doc; *very* slow on cell insert. | |
from_doc0: (doc) => | |
console.log("goal='#{doc}'") | |
console.log("live='#{@to_doc()}'") | |
console.log("from_doc: start"); t = misc.mswalltime() | |
goal = doc.split('\n') | |
live = @to_doc().split('\n') | |
@nb.metadata.name = goal[0].notebook_name | |
for i in [1...Math.max(goal.length, live.length)] | |
index = i-1 | |
if i >= goal.length | |
console.log("deleting cell #{index}") | |
@nb.delete_cell(index) | |
else if goal[i] != live[i] | |
console.log("replacing cell #{index}") | |
try | |
cell_data = JSON.parse(goal[i]) | |
@set_cell(index, cell_data) | |
catch e | |
console.log("error de-jsoning '#{goal[i]}'", e) | |
console.log("from_doc: done", misc.mswalltime(t)) | |
### | |
from_doc: (doc) => | |
#console.log("goal='#{doc}'") | |
#console.log("live='#{@to_doc()}'") | |
#console.log("from_doc: start"); tm = misc.mswalltime() | |
goal = doc.split('\n') | |
live = @to_doc().split('\n') | |
@nb.metadata.name = goal[0].notebook_name | |
v0 = live.slice(1) | |
v1 = goal.slice(1) | |
string_mapping = new misc.StringCharMapping() | |
v0_string = string_mapping.to_string(v0) | |
v1_string = string_mapping.to_string(v1) | |
diff = diffsync.dmp.diff_main(v0_string, v1_string) | |
index = 0 | |
i = 0 | |
parse = (s) -> | |
try | |
return JSON.parse(s) | |
catch e | |
console.log("UNABLE to parse '#{s}' -- not changing this cell.") | |
#console.log("diff=#{misc.to_json(diff)}") | |
i = 0 | |
while i < diff.length | |
chunk = diff[i] | |
op = chunk[0] # -1 = delete, 0 = leave unchanged, 1 = insert | |
val = chunk[1] | |
if op == 0 | |
# skip over cells | |
index += val.length | |
else if op == -1 | |
# delete cells: | |
# A common special case arises when one is editing a single cell, which gets represented | |
# here as deleting then inserting. Replacing is far more efficient than delete and add, | |
# due to the overhead of creating codemirror instances (presumably). (Also, there is a | |
# chance to maintain the cursor later.) | |
if i < diff.length - 1 and diff[i+1][0] == 1 and diff[i+1][1].length == val.length | |
#console.log("replace") | |
for x in diff[i+1][1] | |
obj = parse(string_mapping._to_string[x]) | |
if obj? | |
@set_cell(index, obj) | |
index += 1 | |
i += 1 # skip over next chunk | |
else | |
#console.log("delete") | |
for j in [0...val.length] | |
@delete_cell(index) | |
else if op == 1 | |
# insert new cells | |
#console.log("insert") | |
for x in val | |
obj = parse(string_mapping._to_string[x]) | |
if obj? | |
@insert_cell(index, obj) | |
index += 1 | |
else | |
console.log("BUG -- invalid diff!", diff) | |
i += 1 | |
#console.log("from_doc: done", misc.mswalltime(tm)) | |
#if @to_doc() != doc | |
# console.log("FAIL!") | |
# console.log("goal='#{doc}'") | |
# console.log("live='#{@to_doc()}'") | |
# @from_doc0(doc) | |
focus: () => | |
# TODO | |
# console.log("ipython notebook focus: todo") | |
show: () => | |
@element.show() | |
top = @editor.editor_top_position() | |
@element.css(top:top) | |
if top == 0 | |
@element.css('position':'fixed') | |
w = $(window).width() | |
@iframe?.attr('width',w).maxheight() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment