             for e in @_close_on_action_elements
                 e.remove()
             @_close_on_action_elements = []
+
+
+
+class SynchronizedDocument0 extends EventEmitter
+    constructor: (@editor, opts, cb) ->  # if given, cb will be called when done initializing.
+        @opts = defaults opts,
+            cursor_interval : 1000
+            sync_interval   : 1000
+        @editor.save = @save
+        @codemirror = @editor.codemirror
+        @element    = @editor.element
+        @filename   = @editor.filename
+
+        @init_cursorActivity_event()
+        @init_chat()
+
+        @codemirror.setOption('readOnly', true)
+
+        @connect (err, resp) =>
+            if err
+                bootbox.alert "<h3>Unable to open '#{@filename}'</h3> - #{err}", () =>
+                    @editor.editor.close(@filename)
+            else
+                @ui_synced(false)
+                @editor.init_autosave()
+                first_sync = () =>
+                    @sync (err) =>
+                        if err
+                            setTimeout(first_sync, 1000)
+                        else
+                            @codemirror.setOption('readOnly', false)
+                first_sync()
+
+                @codemirror.on 'change', (instance, changeObj) =>
+                    #console.log("change #{misc.to_json(changeObj)}")
+                    if changeObj.origin?
+                        if changeObj.origin == 'undo'
+                            @on_undo(instance, changeObj)
+                        if changeObj.origin == 'redo'
+                            @on_redo(instance, changeObj)
+                        if changeObj.origin != 'setValue'
+                            @ui_synced(false)
+                            @sync_soon()
+            # Done initializing and have got content.
+            cb?()
+
+    on_undo: (instance, changeObj) =>
+        # do nothing in base class
+    on_redo: (instance, changeObj) =>
+        # do nothing in base class
+
+    _add_listeners: () =>
+        salvus_client.on 'codemirror_diffsync_ready', @_diffsync_ready
+        salvus_client.on 'codemirror_bcast', @_receive_broadcast
+
+    _remove_listeners: () =>
+        salvus_client.removeListener 'codemirror_diffsync_ready', @_diffsync_ready
+        salvus_client.removeListener 'codemirror_bcast', @_receive_broadcast
+
+    disconnect_from_session: (cb) =>
+        @_remove_listeners()
+        salvus_client.call
+            timeout : 10
+            message : message.codemirror_disconnect(session_uuid : @session_uuid)
+            cb      : cb
+
+        # store pref in localStorage to not auto-open this file next time
+        @editor.local_storage('auto_open', false)
+
+    execute_code: (opts) =>
+        opts = defaults opts,
+            code     : required
+            data     : undefined
+            preparse : true
+            cb       : undefined
+        uuid = misc.uuid()
+        salvus_client.send(
+            message.codemirror_execute_code
+                id   : uuid
+                code : opts.code
+                data : opts.data
+                preparse : opts.preparse
+                session_uuid : @session_uuid
+        )
+        if opts.cb?
+            salvus_client.execute_callbacks[uuid] = opts.cb
+
+    introspect_line: (opts) =>
+        opts = defaults opts,
+            line     : required
+            preparse : true
+            cb       : required
+
+        salvus_client.call
+            message: message.codemirror_introspect
+                line         : opts.line
+                preparse     : opts.preparse
+                session_uuid : @session_uuid
+            cb : opts.cb
+
+    connect: (cb) =>
+        # We queue up the calls, since we don't want to call the server more than once to connect.
+        if @_connect_callbacks?
+            @_connect_callbacks.push(cb)
+            return
+        @_connect_callbacks = [cb]
+        @element.find(".salvus-editor-codemirror-loading").show()
+        @_remove_listeners()
+        salvus_client.call
+            timeout : 30     # a reasonable amount of time, since file could be *large*
+            message : message.codemirror_get_session
+                path         : @filename
+                project_id   : @editor.project_id
+            cb      : (err, resp) =>
+                cbs = @_connect_callbacks
+                delete @_connect_callbacks
+                @element.find(".salvus-editor-codemirror-loading").hide()
+                #console.log("new session: ", resp)
+                if resp.event == 'error'
+                    err = resp.error
+                if err
+                    for cb in cbs
+                        cb(err)
+                    return
+
+                @session_uuid = resp.session_uuid
+
+                # Render the chat
+                @element.find(".salvus-editor-codemirror-chat-output").html('')
+                for m in resp.chat
+                    @_receive_chat(m)
+                @new_chat_indicator(false)  # not necessarily new
+
+                # If our content is already set, we'll end up doing a merge.
+                resetting = @_previous_successful_set? and @_previous_successful_set
+
+                if not resetting
+                    # very easy
+                    @_previous_successful_set = true
+                    @editor._set(resp.content)
+                    live_content = resp.content
+                    # Reset the undo history here, since we do not want it to start with "empty document":
+                    @codemirror.clearHistory()
+                else
+                    # Doing a reset -- apply all the edits to the current version of the document.
+                    edit_stack = @dsync_client.edit_stack
+                    # Apply our offline edits to the new live version of the document.
+                    r = new DiffSyncDoc(string:resp.content)
+                    for p in edit_stack
+                        r = r.patch(p.edits)
+
+                    # Compute the patch comparing last known shadow
+                    # with our current live, and apply that patch to what the server
+                    # just sent us.  We declare that to be our new live.
+                    patch = @dsync_client.shadow.diff(@dsync_client.live)
+                    r2 = r.patch(patch)
+                    live_content = r2.string()
+
+
+                @dsync_client = codemirror_diffsync_client(@, resp.content)
+                @dsync_server = new DiffSyncHub(@)
+                @dsync_client.connect(@dsync_server)
+                @dsync_server.connect(@dsync_client)
+                @_add_listeners()
+
+                if resetting
+                    #console.log("RESETTING now.")
+                    @codemirror.setValueNoJump(live_content)
+                    cur = @codemirror._cm_session_cursor_before_reset
+                    if cur?
+                        @codemirror.setCursor(cur)
+                    @sync()
+
+                if not resetting
+                    @editor.save_button.addClass('disabled')   # start with no unsaved changes
+
+                for cb in cbs
+                    cb()
+
+    _diffsync_ready: (mesg) =>
+        if mesg.session_uuid == @session_uuid
+            @sync_soon()
+
+    call: (opts) =>
+        opts = defaults opts,
+            message     : required
+            timeout     : 45
+            cb          : undefined
+        opts.message.session_uuid = @session_uuid
+        salvus_client.call
+            message : opts.message
+            timeout : opts.timeout
+            cb      : (err, result) =>
+                if result? and result.event == 'reconnect'
+                    #console.log("codemirror sync session #{@session_uuid}: reconnecting ")
+                    do_reconnect = () =>
+                        @connect (err) =>
+                            if err
+                                #console.log("codemirror sync session #{@session_uuid}: failed to reconnect")
+                                opts.cb?(err)
+                            else
+                                #console.log("codemirror sync session #{@session_uuid}: successful reconnect")
+                                opts.cb?('reconnect')  # still an error condition
+                    setTimeout(do_reconnect, 1000)  # give server some room
+                else
+                    opts.cb?(err, result)
+
+    sync_soon: (wait) =>
+        if not wait?
+            wait = @opts.sync_interval
+        else if wait <= 0
+            wait = 1
+        if @_sync_soon?
+            # We have already set a timer to do a sync soon.
+            #console.log("not sync_soon since -- We have already set a timer to do a sync soon.")
+            return
+        do_sync = () =>
+            @sync (didnt_sync) =>
+                delete @_sync_soon
+                if didnt_sync
+                    @sync_soon(Math.min(3000, (wait*1.5)))
+        @_sync_soon = setTimeout(do_sync, wait)
+
+    ui_synced: (synced) =>
+        if synced
+            if @_ui_synced_timer?
+                clearTimeout(@_ui_synced_timer)
+                delete @_ui_synced_timer
+            @element.find(".salvus-editor-codemirror-not-synced").hide()
+            #@element.find(".salvus-editor-codemirror-synced").show()
+        else
+            if @_ui_synced_timer?
+                return
+            show_spinner = () =>
+                @element.find(".salvus-editor-codemirror-not-synced").show()
+                #@element.find(".salvus-editor-codemirror-synced").hide()
+            @_ui_synced_timer = setTimeout(show_spinner, 4*@opts.sync_interval)
+
+    sync: (cb) =>    # cb(false if a sync occured; true-ish if anything prevented a sync from happening)
+        #console.log("sync: @_syncing=", @_syncing)
+        if @_syncing? and @_syncing
+            # can only sync once a complete cycle is done, or declared failure.
+            cb?()
+            #console.log('skipping since already syncing')
+            return
+
+        if not @dsync_client
+            cb?("document synchronization not initialized at all yet")
+            return
+
+        @_syncing = true
+        if @_sync_soon?
+            clearTimeout(@_sync_soon)
+            delete @_sync_soon
+        #console.log("sync started")
+        @_sync_cursor_pos = @codemirror.getCursor()
+        @dsync_client.push_edits (err) =>
+            if err
+                # console.log("sync done -- error = ", err)
+                @_syncing = false
+                if not @_sync_failures?
+                    @_sync_failures = 1
+                else
+                    @_sync_failures += 1
+                if @_sync_failures % 6 == 0 and err != 'retry'
+                    # TODO
+                    #alert_message(type:"error", message:"Unable to synchronize '#{@filename}' with server; changes not saved until you next connect to the server. (You can refresh your browser, but might loose some changes.)")
+                    @sync_soon(10000)
+                else
+                    @sync_soon()
+                cb?(err)
+            else
+                # We just completed a successful sync.
+                #console.log("sync done -- success")
+                @_sync_failures = 0
+                @_syncing = false
+                @emit('sync')
+                @ui_synced(true)
+                cb?()
+
+    init_cursorActivity_event: () =>
+        @codemirror.on 'cursorActivity', (instance) =>
+            if not @_syncing
+                @send_cursor_info_to_hub_soon()
+            @editor.local_storage('cursor', @codemirror.getCursor())
+
+    init_chat: () =>
+        chat = @element.find(".salvus-editor-codemirror-chat")
+        input = chat.find(".salvus-editor-codemirror-chat-input")
+        input.keydown (evt) =>
+            if evt.which == 13 # enter
+                content = $.trim(input.val())
+                if content != ""
+                    input.val("")
+                    @send_broadcast_message({event:'chat', content:content}, true)
+                return false
+
+        @init_chat_toggle()
+
+    init_chat_toggle: () =>
+        title = @element.find(".salvus-editor-chat-title")
+        title.click () =>
+            if @editor._chat_is_hidden? and @editor._chat_is_hidden
+                @show_chat_window()
+            else
+                @hide_chat_window()
+        @hide_chat_window()  #start hidden for now, until we have a way to save this.
+
+    show_chat_window: () =>
+        # SHOW the chat window
+        @editor._chat_is_hidden = false
+        @element.find(".salvus-editor-chat-show").hide()
+        @element.find(".salvus-editor-chat-hide").show()
+        @element.find(".salvus-editor-codemirror-input-box").removeClass('span12').addClass('span9')
+        @element.find(".salvus-editor-codemirror-chat-column").show()
+        # see http://stackoverflow.com/questions/4819518/jquery-ui-resizable-does-not-support-position-fixed-any-recommendations
+        # if you want to try to make this resizable
+        output = @element.find(".salvus-editor-codemirror-chat-output")
+        output.scrollTop(output[0].scrollHeight)
+        @new_chat_indicator(false)
+        @editor.show()  # updates editor width
+
+    hide_chat_window: () =>
+        # HIDE the chat window
+        @editor._chat_is_hidden = true
+        @element.find(".salvus-editor-chat-hide").hide()
+        @element.find(".salvus-editor-chat-show").show()
+        @element.find(".salvus-editor-codemirror-input-box").removeClass('span9').addClass('span12')
+        @element.find(".salvus-editor-codemirror-chat-column").hide()
+        @editor.show()  # update size/display of editor (especially the width)
+
+    new_chat_indicator: (new_chats) =>
+        # Show a new chat indicator of the chat window is closed.
+        # if new_chats, indicate that there are new chats
+        # if new_chats, don't indicate new chats.
+        elt = @element.find(".salvus-editor-chat-new-chats")
+        if new_chats and @editor._chat_is_hidden
+            elt.show()
+        else
+            elt.hide()
+
+    _receive_chat: (mesg) =>
+        @new_chat_indicator(true)
+        output = @element.find(".salvus-editor-codemirror-chat-output")
+        date = new Date(mesg.date)
+        entry = templates.find(".salvus-chat-entry").clone()
+        output.append(entry)
+        header = entry.find(".salvus-chat-header")
+        if (not @_last_chat_name?) or @_last_chat_name != mesg.name or ((date.getTime() - @_last_chat_time) > 60000)
+            header.find(".salvus-chat-header-name").text(mesg.name).css(color:"#"+mesg.color)
+            header.find(".salvus-chat-header-date").attr('title', date.toISOString()).timeago()
+        else
+            header.hide()
+        @_last_chat_name = mesg.name
+        @_last_chat_time = new Date(mesg.date).getTime()
+        entry.find(".salvus-chat-entry-content").text(mesg.mesg.content).mathjax()
+        output.scrollTop(output[0].scrollHeight)
+
+    send_broadcast_message: (mesg, self) ->
+        m = message.codemirror_bcast
+            session_uuid : @session_uuid
+            mesg         : mesg
+            self         : self    #if true, then also send include this client to receive message
+        salvus_client.send(m)
+
+    send_cursor_info_to_hub: () =>
+        delete @_waiting_to_send_cursor
+        if not @session_uuid # not yet connected to a session
+            return
+        @send_broadcast_message({event:'cursor', pos:@codemirror.getCursor()})
+
+    send_cursor_info_to_hub_soon: () =>
+        if @_waiting_to_send_cursor?
+            return
+        @_waiting_to_send_cursor = setTimeout(@send_cursor_info_to_hub, @opts.cursor_interval)
+
+    _receive_broadcast: (mesg) =>
+        if mesg.session_uuid != @session_uuid
+            return
+        switch mesg.mesg.event
+            when 'cursor'
+                @_receive_cursor(mesg)
+            when 'chat'
+                @_receive_chat(mesg)
+
+    _receive_cursor: (mesg) =>
+        # If the cursor has moved, draw it.  Don't bother if it hasn't moved, since it can get really
+        # annoying having a pointless indicator of another person.
+        if not @_last_cursor_pos?
+            @_last_cursor_pos = {}
+        else
+            pos = @_last_cursor_pos[mesg.color]
+            if pos? and pos.line == mesg.mesg.pos.line and pos.ch == mesg.mesg.pos.ch
+                return
+        # cursor moved.
+        @_last_cursor_pos[mesg.color] = mesg.mesg.pos   # record current position
+        @_draw_other_cursor(mesg.mesg.pos, '#' + mesg.color, mesg.name)
+
+    # Move the cursor with given color to the given pos.
+    _draw_other_cursor: (pos, color, name) =>
+        if not @codemirror?
+            return
+        if not @_cursors?
+            @_cursors = {}
+        id = color + name
+        cursor_data = @_cursors[id]
+        if not cursor_data?
+            cursor = templates.find(".salvus-editor-codemirror-cursor").clone().show()
+            inside = cursor.find(".salvus-editor-codemirror-cursor-inside")
+            inside.css
+                'background-color': color
+            label = cursor.find(".salvus-editor-codemirror-cursor-label")
+            label.css('color':color)
+            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:100).fadeOut(duration:8000)
+        # Then fade the cursor out (a non-active cursor is a waste of space).
+        cursor_data.cursor.stop().show().animate(opacity:100).fadeOut(duration:60000)
+        #console.log("Draw #{name}'s #{color} cursor at position #{pos.line},#{pos.ch}", cursor_data.cursor)
+        @codemirror.addWidget(pos, cursor_data.cursor[0], false)
+
+    click_save_button: () =>
+        if not @save_button.hasClass('disabled')
+            # Only show the spin/saving indicator *after* a short delay, in case we can't save super-quickly.
+            show_save = () =>
+                @save_button.find('span').text("Saving...")
+                @save_button.find(".spinner").show()
+            spin = setTimeout(show_save, 250)
+            @save (err) =>
+                clearTimeout(spin)
+                @save_button.find(".spinner").hide()
+                @save_button.find('span').text('Save')
+                if not err
+                    @save_button.addClass('disabled')
+                    @has_unsaved_changes(false)
+                else
+                    alert_message(type:"error", message:"Error saving '#{@filename}' to disk -- #{err}")
+        return false
+
+    save: (cb) =>
+        if @editor.opts.delete_trailing_whitespace
+            @codemirror.delete_trailing_whitespace()
+        if @dsync_client?
+            @sync () =>
+                @call
+                    message: message.codemirror_write_to_disk()
+                    cb : cb
+        else
+            cb("Unable to save '#{@filename}' since it is not yet loaded.")
+
+    _apply_changeObj: (changeObj) =>
+        @codemirror.replaceRange(changeObj.text, changeObj.from, changeObj.to)
+        if changeObj.next?
+            @_apply_changeObj(changeObj.next)
+
+    refresh_soon: (wait) =>
+        if not wait?
+            wait = 1000
+        if @_refresh_soon?
+            # We have already set a timer to do a refresh soon.
+            #console.log("not refresh_soon since -- We have already set a timer to do a refresh soon.")
+            return
+        do_refresh = () =>
+            delete @_refresh_soon
+            @codemirror.refresh()
+        @_refresh_soon = setTimeout(do_refresh, wait)
+
+    interrupt: () =>
+        @close_on_action()
+
+    close_on_action: (element) =>
+        # Close popups (e.g., introspection) that are set to be closed when an
+        # action, such as "execute", occurs.
+        if element?
+            if not @_close_on_action_elements?
+                @_close_on_action_elements = [element]
+            else
+                @_close_on_action_elements.push(element)
+        else if @_close_on_action_elements?
+            for e in @_close_on_action_elements
+                e.remove()
+            @_close_on_action_elements = []
+
 
 
 { MARKERS, FLAGS, ACTION_FLAGS } = diffsync

                cb?()

    _add_listeners: () =>
        salvus_client.on 'codemirror_diffsync_ready', @_diffsync_ready
        #salvus_client.on 'codemirror_bcast', @_receive_broadcast

    _remove_listeners: () =>
        salvus_client.removeListener 'codemirror_diffsync_ready', @_diffsync_ready
        #salvus_client.removeListener 'codemirror_bcast', @_receive_broadcast

    _diffsync_ready: (mesg) =>
        if mesg.session_uuid == @session_uuid
            @sync()

    #_receive_broadcast: (mesg) =>
    #    if mesg.session_uuid != @session_uuid
    #        return

    # @live(): the current live version of this document as a string, or
    # @live(s): set the live version


{EventEmitter} = require('events')

class SynchronizedString extends EventEmitter
    constructor: (opts) ->
        opts = defaults opts,
            project_id : required
