<commit_before>###############################################################################
#
#    CoCalc: Collaborative Calculation in the Cloud
#
#    Copyright (C) 2014--2016, SageMath, Inc.
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################

###############################################################################
# Editor for LaTeX documents (also support *.rnw files)
###############################################################################

$ = window.$

async           = require('async')
underscore      = require('underscore')

misc            = require('smc-util/misc')
misc_page       = require('../misc_page')
{defaults, required} = misc

{alert_message} = require('../alerts')
{redux}         = require('../smc-react')
editor          = require('../editor')
printing        = require('../printing')
{project_tasks} = require('../project_tasks')
{webapp_client} = require('../webapp_client')
{IS_TOUCH}      = require('../feature')

templates       = $("#webapp-editor-templates")

# this regex matches the `@_get()` content iff it is a compileable latex document
RE_FULL_LATEX_CODE = new RegExp('\\\\documentclass[^}]*}[^]*?\\\\begin{document}[^]*?\\\\end{document}', 'g')

# local storage keys -- prevents typos (and keys can be shorter)
LSkey =
    ignore_invalid_latex : 'iil'
    split_pos            : 'split_pos'
    config               : 'conf'
    render_preview       : 'rp'

MAX_LATEX_ERRORS   = 10
MAX_LATEX_WARNINGS = 50
TOOLTIP_CONFIG =
    delay: {show: 500, hide: 100}

class exports.LatexEditor extends editor.FileEditor
    constructor: (@project_id, @filename, content, opts) ->
        super(@project_id, @filename)

        # The are three components:
        #     * latex_editor -- a CodeMirror editor
        #     * preview -- display the images (page forward/backward/resolution)
        #     * log -- log of latex command
        opts.mode = 'stex2'

        @element = templates.find(".webapp-editor-latex").clone()

        @_pages = {}

        # this maps tex line numbers to the ones in the Rnw file via an array
        @_rnw_concordance = null

        # initialize the latex_editor
        opts.latex_editor = true
        @latex_editor = editor.codemirror_session_editor(@project_id, @filename, opts)
        @_pages['latex_editor'] = @latex_editor
        @element.find(".webapp-editor-latex-latex_editor").append(@latex_editor.element)
        @element.find(".webapp-editor-codeedit-buttonbar-mode").remove()

        @latex_editor.action_key = @action_key
        @element.find(".webapp-editor-latex-buttons").show()

        latex_buttonbar = @element.find(".webapp-editor-latex-buttonbar")
        latex_buttonbar.show()

        @latex_editor.on 'saved', () =>
            saved_cb = =>
                if @_current_page == 'pdf-preview'
                    @preview_embed?.update()
            @update_preview(saved_cb)
            @spell_check()

        @latex_editor.syncdoc.on 'connect', () =>
            #if DEBUG then console.log("LatexEditor. syncdoc.on 'connect'")
            @preview.zoom_width = @load_conf().zoom_width ? 100
            setTimeout((=> @update_preview(undefined, false, false)), 1000)
            @spell_check()

        @latex_editor.print = () =>
            outfn = misc.change_filename_extension(@filename, 'pdf')
            printing.Printer(@, outfn).print()
            return false

        v = misc.path_split(@filename)
        @_path = v.head
        @_target = v.tail

        # initialize the previews
        #n = @filename.length

        # The pdf preview.
        {PNG_Preview} = require('./png_preview')
        @preview = new PNG_Preview(@project_id, @filename, undefined, {resolution:@get_resolution()})
        @element.find(".webapp-editor-latex-png-preview").append(@preview.element)
        @_pages['png-preview'] = @preview
        @preview.on 'shift-click', (opts) => @_inverse_search(opts)

        # Embedded pdf page (not really a "preview" -- it's the real thing).
        # see https://github.com/sagemathinc/cocalc/issues/1313
        # it was broken on Firefox, but as of version 53 it works
        preview_filename = misc.change_filename_extension(@filename, 'pdf')
        {PDF_PreviewEmbed} = require('./pdf_preview')
        @preview_embed = new PDF_PreviewEmbed(@project_id, preview_filename, undefined, {})
        @preview_embed.element.find(".webapp-editor-codemirror-button-row").remove()
        @element.find(".webapp-editor-latex-pdf-preview").append(@preview_embed.element)
        @_pages['pdf-preview'] = @preview_embed

        # Initalize the log
        @log = @element.find(".webapp-editor-latex-log")
        @log.find("a").tooltip(TOOLTIP_CONFIG)
        @_pages['log'] = @log
        @log_input = @log.find("input")
        @log_input.tooltip(TOOLTIP_CONFIG)
        save_custom_build_command = () =>
            @set_conf_doc(latex_command: @log_input.val())
            @save()
        @log_input.keyup (e) =>
            if e.keyCode == 13
                save_custom_build_command()
        @log_input.on('blur', save_custom_build_command)

        # Custom build command menu
        dropdown = @element.find(".smc-editor-latex-log-cmd .dropdown-menu")
        dropdown.on 'click', 'li', (ev) =>
            ev.preventDefault()
            flavor = ev.target.hash ? '#default'
            c = @preview.pdflatex.default_tex_command(flavor[1..])
            # console.log(c)
            @log_input.val(c)
            @set_conf_doc(latex_command: c)
            @save()
            return true

        for cm in @cms()
            cm._smc_inline_errors = {}
        @element.on 'blur', ->
            $('[data-toggle="popover"]').popover('hide')
        @errors = @element.find(".webapp-editor-latex-errors")
        @_pages['errors'] = @errors
        @_error_message_template = @element.find(".webapp-editor-latex-mesg-template")

        @help = @element.find('.webapp-editor-latex-help')
        markdown = require('../markdown')
        {help_md} = require('./help')
        @help.html(markdown.markdown_to_html(help_md).s)
        @_pages['help'] = @help

        @_init_buttons()
        @init_draggable_split()

        # if the latex file should be compiled and the preview rendered
        # must come after _init_buttons
        @_render_preview = @local_storage(LSkey.render_preview) ? true
        @set_toggle_preview_state(@_render_preview)

        # This synchronizes the editor and png preview -- it's kind of disturbing.
        # If people request it, make it a non-default option...
        ###
            @preview.page.on 'scroll', @_passive_inverse_search
            cm0 = @latex_editor.codemirror
            cm1 = @latex_editor.codemirror1
            cm0.on 'cursorActivity', @_passive_forward_search
            cm1.on 'cursorActivity', @_passive_forward_search
            cm0.on 'change', @_pause_passive_search
            cm1.on 'change', @_pause_passive_search
        ###

    cms: =>
        return @latex_editor.codemirrors()

    spell_check: (cb) =>
        @preview.pdflatex.spell_check
            lang : @load_conf_doc().lang
            cb   : (err, words) =>
                if err
                    cb?(err)
                else
                    for cm in @cms()
                        cm.spellcheck_highlight(words)

    init_draggable_split: () =>
        @_split_pos = @local_storage(LSkey.split_pos)
        @_dragbar = dragbar = @element.find(".webapp-editor-latex-resize-bar")
        @set_dragbar_position()
        update = =>
            misc_page.drag_stop_iframe_enable()
            # compute the position of bar as a number from 0 to 1
            left  = @element.offset().left
            width = @element.width()
            p     = dragbar.offset().left
            @_split_pos = (p - left) / width
            @local_storage(LSkey.split_pos, @_split_pos)
            dragbar.css(left: 0)
            @set_dragbar_position()

        if IS_TOUCH
            dragbar.width('12px')

        dragbar.draggable
            axis        : 'x'
            containment : @element
            zIndex      : 10
            stop        : update
            start       : misc_page.drag_start_iframe_disable

    set_dragbar_position: =>
        @_split_pos ?= @local_storage(LSkey.split_pos) ? 0.5
        @_split_pos = Math.max(editor.MIN_SPLIT, Math.min(editor.MAX_SPLIT, @_split_pos))
        @element.find(".webapp-editor-latex-latex_editor").css('flex-basis',"#{@_split_pos*100}%")

    set_conf: (obj) =>
        conf = @load_conf()
        for k, v of obj
            conf[k] = v
        @save_conf(conf)

    load_conf: () =>
        conf = @local_storage(LSkey.config)
        if not conf?
            conf = {}
        return conf

    save_conf: (conf) =>
        @local_storage(LSkey.config, conf)

    set_conf_doc: (obj) =>
        conf = @load_conf_doc()
        for k, v of obj
            conf[k] = v
        @save_conf_doc(conf)

    load_conf_doc: () =>
        if not @latex_editor.codemirror?
            return {}
        doc = @latex_editor.codemirror.getValue()
        i_old = doc.indexOf('%sagemathcloud=')
        i_new = doc.indexOf('%configuration=')
        i = Math.max(i_old, i_new)
        if i == -1
            return {}

        j = doc.indexOf('=',i)
        k = doc.indexOf('\n',i)
        if k == -1
            k = doc.length
        try
            conf = misc.from_json(doc.slice(j+1,k))
        catch e
            conf = {}

        return conf

    save_conf_doc: (conf) =>
        cm  = @latex_editor.codemirror
        if not cm?
            return
        doc = cm.getValue()
        i_old = doc.indexOf('%sagemathcloud=')
        i_new = doc.indexOf('%configuration=')
        i = Math.max(i_old, i_new)
        line = '%configuration=' + misc.to_json(conf)
        if i != -1
            # find the line m where it is already
            for n in [cm.doc.lastLine()..0]
                z = cm.getLine(n)
                i2_old = z.indexOf('%sagemathcloud=')
                i2_new = z.indexOf('%configuration=')
                i2 = Math.max(i2_old, i2_new)
                if i2 != -1
                    m = n
                    break
            cm.replaceRange(line+'\n', {line:m,ch:0}, {line:m+1,ch:0})
        else
            if misc.len(conf) == 0
                # don't put it in there if empty
                return
            cm.replaceRange('\n'+line, {line:cm.doc.lastLine()+1,ch:0})
        @latex_editor.syncdoc.sync()

    _pause_passive_search: (cb) =>
        @_passive_forward_search_disabled = true
        @_passive_inverse_search_disabled = true
        f = () =>
            @_passive_inverse_search_disabled = false
            @_passive_forward_search_disabled = false

        setTimeout(f, 3000)

    _passive_inverse_search: (cb) =>
        if @_passive_inverse_search_disabled
            cb?(); return
        @_pause_passive_search()
        @inverse_search
            active : false
            cb     : (err) =>
                cb?()

    _passive_forward_search: (cb) =>
        if @_passive_forward_search_disabled
            cb?(); return
        @forward_search
            active : false
            cb     : (err) =>
                @_pause_passive_search()
                cb?()

    action_key: () =>
        @show_page('png-preview')
        @forward_search(active:true)

    remove: () =>
        @latex_editor.remove()
        @element.remove()
        @preview.remove()
        @preview_embed?.remove()

    _init_buttons: () =>
        @element.find("a").tooltip(TOOLTIP_CONFIG)

        @element.find('a[href="#forward-search"]').click () =>
            @show_page('png-preview')
            @forward_search(active:true)
            return false

        @element.find('a[href="#inverse-search"]').click () =>
            @show_page('png-preview')
            @inverse_search(active:true)
            return false

        @element.find('a[href="#png-preview"]').click () =>
            @show_page('png-preview')
            @preview.focus()
            @save()
            return false

        @element.find('a[href="#help"]').click () =>
            @show_page('help')
            return false

        @element.find('a[href="#zoom-preview-out"]').click () =>
            @preview.zoom(delta: -5)
            @set_conf(zoom_width: @preview.zoom_width)
            return false

        @element.find('a[href="#zoom-preview-in"]').click () =>
            @preview.zoom(delta:5)
            @set_conf(zoom_width:@preview.zoom_width)
            return false

        @element.find('a[href="#zoom-preview-fullpage"]').click () =>
            @preview.zoom(width:100)
            @set_conf(zoom_width:@preview.zoom_width)
            return false

        @element.find('a[href="#zoom-preview-width"]').click () =>
            @preview.zoom(width:160)
            @set_conf(zoom_width:@preview.zoom_width)
            return false

        @element.find('a[href="#pdf-preview"]').click () =>
            # see https://github.com/sagemathinc/cocalc/issues/1313
            # it was broken on Firefox, but as of version 53 it works
            @show_page('pdf-preview')
            @preview_embed.focus()
            @preview_embed.update()
            return false

        @element.find('a[href="#log"]').click () =>
            @show_page('log')
            t = @log.find("textarea")
            t.scrollTop(t[0].scrollHeight)
            return false

        @element.find('a[href="#errors"]').click () =>
            @show_page('errors')
            return false

        @number_of_errors = @element.find('a[href="#errors"]').find(".webapp-latex-errors-counter")
        @number_of_warnings = @element.find('a[href="#errors"]').find(".webapp-latex-warnings-counter")

        @element.find('a[href="#pdf-download"]').click () =>
            @download_pdf()
            return false

        @element.find('a[href="#preview-resolution"]').click () =>
            @set_resolution()
            return false

        @_toggle_preview_button = @element.find('a[href="#toggle-preview"]')
        @_toggle_preview_button.click () =>
            @toggle_preview()
            return false

        #@element.find("a[href=\"#latex-command-undo\"]").click () =>
        #    c = @preview.pdflatex.default_tex_command()
        #    @log_input.val(c)
        #    @set_conf_doc(latex_command: c)
        #    return false

        trash_aux_button = @element.find('a[href="#latex-trash-aux"]')
        trash_aux_button.click () =>
            trash_aux_button.icon_spin(true, disable=true)
            log_output = @log.find("textarea")
            log_output.text('')
            # now we actually delete the files and report any errors in the log
            @preview.pdflatex.trash_aux_files (err, log) =>
                if err
                    log_output.text(err)
                else
                    log_output.text(log)
                # after deleting the log also clear all issues!
                @preview.pdflatex.last_latex_log = ''
                @render_error_page()
                trash_aux_button.icon_spin(false, disable=true)
            return false

        run_sage = @element.find('a[href="#latex-sage"]')
        run_sage.click () =>
            @log.find("textarea").text("Running Sage...")
            run_sage.icon_spin(true, disable=true)
            @preview.pdflatex._run_sage undefined, (err, log) =>
                run_sage.icon_spin(false, disable=true)
                @log.find("textarea").text(log)
            return false

        run_latex = @element.find('a[href="#latex-latex"]')
        run_latex.click () =>
            @log.find("textarea").text("Running Latex...")
            run_latex.icon_spin(true, disable=true)
            @preview.pdflatex._run_latex @load_conf_doc().latex_command, (err, log) =>
                @log.find("textarea").text(log)
                # the log changed, there could be issues, render them
                @render_error_page()
                run_latex.icon_spin(false, disable=true)
            return false

        run_recompile = @element.find('a[href="#latex-recompile"]')
        run_recompile.click () =>
            log_box = @log.find("textarea")
            log_box.text("Recompiling ...")
            run_recompile.icon_spin(true, disable=true)
            async.series([
                (cb) =>
                    @preview.pdflatex.trash_aux_files (err, _log) =>
                        cb(err)
                (cb) =>
                    @update_preview(cb, true, true)
            ], (err) =>
                run_recompile.icon_spin(false, disable=true)
            )
            return false

        run_bibtex = @element.find('a[href="#latex-bibtex"]')
        run_bibtex.click () =>
            @log.find("textarea").text("Running Bibtex...")
            run_bibtex.icon_spin(true, disable=true)
            @preview.pdflatex._run_bibtex (err, log) =>
                run_bibtex.icon_spin(false, disable=true)
                @log.find("textarea").text(log)
            return false

    set_resolution: (res) =>
        if not res?
            bootbox.prompt "Change preview resolution from #{@get_resolution()} dpi to...", (result) =>
                if result
                    @set_resolution(result)
        else
            try
                res = parseInt(res)
                if res < 75
                    res = 75
                else if res > 600
                    res = 600
                @preview.opts.resolution = res
                @set_conf(resolution : res)
                @preview.update()
            catch e
                alert_message
                    type    : "error"
                    message : "Invalid resolution #{res}"

    get_resolution: () =>
        if not @preview?
            return @load_conf()['resolution'] ? 150
        return @preview.opts.resolution

    # This function isn't called on save button click since
    # the codemirror's save button is called...
    click_save_button: () =>
        @latex_editor.click_save_button()

    # This function isn't called on save
    # @latex_editor.save is called instead for some reason
    save: (cb, force) =>
        force ?= false
        @latex_editor.save (err) =>
            cb?(err)
            if not err
                sf =  =>
                    if @_current_page == 'pdf-preview'
                        @preview_embed?.update()
                @update_preview(sf, force)
                @spell_check()

    update_preview: (cb, force, only_compile) =>
        force        ?= false
        only_compile ?= false
        # force: continue, even when content hasn't changed
        # only_compile: avoid the preview rendering
        # obvious TODO: untangle preview update and run_latex
        if not only_compile and not @_render_preview
            cb?()
            return

        # no content available, hence nothing to preview
        if not @latex_editor.syncdoc._fully_loaded
            cb?()
            return

        content = @_get()
        # issue #1814
        if not content?
            cb?()
            return

        if not force and content == @_last_update_preview
            cb?()
            return

        latex_command = @load_conf_doc().latex_command
        filename_tex  = @preview.pdflatex.filename_tex
        # a check, if we're really compiling the edited latex file
        if latex_command?  # could be undefined
            compiling_this_file = latex_command.indexOf(filename_tex) >= 0
        else
            compiling_this_file = true

        # check if latex file is invalid iff we're compiling it
        iil = @local_storage(LSkey.ignore_invalid_latex) ? false
        if not iil and not content.match(RE_FULL_LATEX_CODE) and compiling_this_file and not only_compile
            msg = @invalid_latex(filename_tex)
            @preview.show_message(msg)
            cb?()
            return

        if @_render_preview and not only_compile
            @preview.show_pages(true)
        preview_button = @element.find('a[href="#png-preview"]')
        async.series([
            (cb) =>
                @_last_update_preview = content
                if @latex_editor.has_unsaved_changes()
                    @latex_editor.save(cb)
                else
                    cb()
            (cb) =>
                if @latex_editor.has_uncommitted_changes()
                    delete @_last_update_preview  # running latex on stale version
                    @get_rnw_concordance_error = false
                #@dbg("update_preview(force=#{force}, only_compile=#{only_compile}): run_latex")
                @run_latex
                    command      : @load_conf_doc().latex_command
                    cb           : cb
            (cb) =>
                if only_compile
                    cb(); return
                #@dbg("update_preview(force=#{force}, only_compile=#{only_compile}): preview update")
                preview_button.icon_spin(true, disable=true)
                @preview.update
                    cb: cb
        ], (err) =>
            if not only_compile
                preview_button.icon_spin(false, disable=true)
            if err<commit_msg>webapp/latex: converting LatexEditor::update_preview to use the "opts" design pattern<commit_after>###############################################################################
#
#    CoCalc: Collaborative Calculation in the Cloud
#
#    Copyright (C) 2014--2016, SageMath, Inc.
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################

###############################################################################
# Editor for LaTeX documents (also support *.rnw files)
###############################################################################

$ = window.$

async           = require('async')
underscore      = require('underscore')

misc            = require('smc-util/misc')
misc_page       = require('../misc_page')
{defaults, required} = misc

{alert_message} = require('../alerts')
{redux}         = require('../smc-react')
editor          = require('../editor')
printing        = require('../printing')
{project_tasks} = require('../project_tasks')
{webapp_client} = require('../webapp_client')
{IS_TOUCH}      = require('../feature')

templates       = $("#webapp-editor-templates")

# this regex matches the `@_get()` content iff it is a compileable latex document
RE_FULL_LATEX_CODE = new RegExp('\\\\documentclass[^}]*}[^]*?\\\\begin{document}[^]*?\\\\end{document}', 'g')

# local storage keys -- prevents typos (and keys can be shorter)
LSkey =
    ignore_invalid_latex : 'iil'
    split_pos            : 'split_pos'
    config               : 'conf'
    render_preview       : 'rp'

MAX_LATEX_ERRORS   = 10
MAX_LATEX_WARNINGS = 50
TOOLTIP_CONFIG =
    delay: {show: 500, hide: 100}

class exports.LatexEditor extends editor.FileEditor
    constructor: (@project_id, @filename, content, opts) ->
        super(@project_id, @filename)

        # The are three components:
        #     * latex_editor -- a CodeMirror editor
        #     * preview -- display the images (page forward/backward/resolution)
        #     * log -- log of latex command
        opts.mode = 'stex2'

        @element = templates.find(".webapp-editor-latex").clone()

        @_pages = {}

        # this maps tex line numbers to the ones in the Rnw file via an array
        @_rnw_concordance = null

        # initialize the latex_editor
        opts.latex_editor = true
        @latex_editor = editor.codemirror_session_editor(@project_id, @filename, opts)
        @_pages['latex_editor'] = @latex_editor
        @element.find(".webapp-editor-latex-latex_editor").append(@latex_editor.element)
        @element.find(".webapp-editor-codeedit-buttonbar-mode").remove()

        @latex_editor.action_key = @action_key
        @element.find(".webapp-editor-latex-buttons").show()

        latex_buttonbar = @element.find(".webapp-editor-latex-buttonbar")
        latex_buttonbar.show()

        @latex_editor.on 'saved', () =>
            saved_cb = =>
                if @_current_page == 'pdf-preview'
                    @preview_embed?.update()
            @update_preview(cb:saved_cb)
            @spell_check()

        @latex_editor.syncdoc.on 'connect', () =>
            #if DEBUG then console.log("LatexEditor. syncdoc.on 'connect'")
            @preview.zoom_width = @load_conf().zoom_width ? 100
            setTimeout((=> @update_preview()), 1000)
            @spell_check()

        @latex_editor.print = () =>
            outfn = misc.change_filename_extension(@filename, 'pdf')
            printing.Printer(@, outfn).print()
            return false

        v = misc.path_split(@filename)
        @_path = v.head
        @_target = v.tail

        # initialize the previews
        #n = @filename.length

        # The pdf preview.
        {PNG_Preview} = require('./png_preview')
        @preview = new PNG_Preview(@project_id, @filename, undefined, {resolution:@get_resolution()})
        @element.find(".webapp-editor-latex-png-preview").append(@preview.element)
        @_pages['png-preview'] = @preview
        @preview.on 'shift-click', (opts) => @_inverse_search(opts)

        # Embedded pdf page (not really a "preview" -- it's the real thing).
        # see https://github.com/sagemathinc/cocalc/issues/1313
        # it was broken on Firefox, but as of version 53 it works
        preview_filename = misc.change_filename_extension(@filename, 'pdf')
        {PDF_PreviewEmbed} = require('./pdf_preview')
        @preview_embed = new PDF_PreviewEmbed(@project_id, preview_filename, undefined, {})
        @preview_embed.element.find(".webapp-editor-codemirror-button-row").remove()
        @element.find(".webapp-editor-latex-pdf-preview").append(@preview_embed.element)
        @_pages['pdf-preview'] = @preview_embed

        # Initalize the log
        @log = @element.find(".webapp-editor-latex-log")
        @log.find("a").tooltip(TOOLTIP_CONFIG)
        @_pages['log'] = @log
        @log_input = @log.find("input")
        @log_input.tooltip(TOOLTIP_CONFIG)
        save_custom_build_command = () =>
            @set_conf_doc(latex_command: @log_input.val())
            @save()
        @log_input.keyup (e) =>
            if e.keyCode == 13
                save_custom_build_command()
        @log_input.on('blur', save_custom_build_command)

        # Custom build command menu
        dropdown = @element.find(".smc-editor-latex-log-cmd .dropdown-menu")
        dropdown.on 'click', 'li', (ev) =>
            ev.preventDefault()
            flavor = ev.target.hash ? '#default'
            c = @preview.pdflatex.default_tex_command(flavor[1..])
            # console.log(c)
            @log_input.val(c)
            @set_conf_doc(latex_command: c)
            @save()
            return true

        for cm in @cms()
            cm._smc_inline_errors = {}
        @element.on 'blur', ->
            $('[data-toggle="popover"]').popover('hide')
        @errors = @element.find(".webapp-editor-latex-errors")
        @_pages['errors'] = @errors
        @_error_message_template = @element.find(".webapp-editor-latex-mesg-template")

        @help = @element.find('.webapp-editor-latex-help')
        markdown = require('../markdown')
        {help_md} = require('./help')
        @help.html(markdown.markdown_to_html(help_md).s)
        @_pages['help'] = @help

        @_init_buttons()
        @init_draggable_split()

        # if the latex file should be compiled and the preview rendered
        # must come after _init_buttons
        @_render_preview = @local_storage(LSkey.render_preview) ? true
        @set_toggle_preview_state(@_render_preview)

        # This synchronizes the editor and png preview -- it's kind of disturbing.
        # If people request it, make it a non-default option...
        ###
            @preview.page.on 'scroll', @_passive_inverse_search
            cm0 = @latex_editor.codemirror
            cm1 = @latex_editor.codemirror1
            cm0.on 'cursorActivity', @_passive_forward_search
            cm1.on 'cursorActivity', @_passive_forward_search
            cm0.on 'change', @_pause_passive_search
            cm1.on 'change', @_pause_passive_search
        ###

    cms: =>
        return @latex_editor.codemirrors()

    spell_check: (cb) =>
        @preview.pdflatex.spell_check
            lang : @load_conf_doc().lang
            cb   : (err, words) =>
                if err
                    cb?(err)
                else
                    for cm in @cms()
                        cm.spellcheck_highlight(words)

    init_draggable_split: () =>
        @_split_pos = @local_storage(LSkey.split_pos)
        @_dragbar = dragbar = @element.find(".webapp-editor-latex-resize-bar")
        @set_dragbar_position()
        update = =>
            misc_page.drag_stop_iframe_enable()
            # compute the position of bar as a number from 0 to 1
            left  = @element.offset().left
            width = @element.width()
            p     = dragbar.offset().left
            @_split_pos = (p - left) / width
            @local_storage(LSkey.split_pos, @_split_pos)
            dragbar.css(left: 0)
            @set_dragbar_position()

        if IS_TOUCH
            dragbar.width('12px')

        dragbar.draggable
            axis        : 'x'
            containment : @element
            zIndex      : 10
            stop        : update
            start       : misc_page.drag_start_iframe_disable

    set_dragbar_position: =>
        @_split_pos ?= @local_storage(LSkey.split_pos) ? 0.5
        @_split_pos = Math.max(editor.MIN_SPLIT, Math.min(editor.MAX_SPLIT, @_split_pos))
        @element.find(".webapp-editor-latex-latex_editor").css('flex-basis',"#{@_split_pos*100}%")

    set_conf: (obj) =>
        conf = @load_conf()
        for k, v of obj
            conf[k] = v
        @save_conf(conf)

    load_conf: () =>
        conf = @local_storage(LSkey.config)
        if not conf?
            conf = {}
        return conf

    save_conf: (conf) =>
        @local_storage(LSkey.config, conf)

    set_conf_doc: (obj) =>
        conf = @load_conf_doc()
        for k, v of obj
            conf[k] = v
        @save_conf_doc(conf)

    load_conf_doc: () =>
        if not @latex_editor.codemirror?
            return {}
        doc = @latex_editor.codemirror.getValue()
        i_old = doc.indexOf('%sagemathcloud=')
        i_new = doc.indexOf('%configuration=')
        i = Math.max(i_old, i_new)
        if i == -1
            return {}

        j = doc.indexOf('=',i)
        k = doc.indexOf('\n',i)
        if k == -1
            k = doc.length
        try
            conf = misc.from_json(doc.slice(j+1,k))
        catch e
            conf = {}

        return conf

    save_conf_doc: (conf) =>
        cm  = @latex_editor.codemirror
        if not cm?
            return
        doc = cm.getValue()
        i_old = doc.indexOf('%sagemathcloud=')
        i_new = doc.indexOf('%configuration=')
        i = Math.max(i_old, i_new)
        line = '%configuration=' + misc.to_json(conf)
        if i != -1
            # find the line m where it is already
            for n in [cm.doc.lastLine()..0]
                z = cm.getLine(n)
                i2_old = z.indexOf('%sagemathcloud=')
                i2_new = z.indexOf('%configuration=')
                i2 = Math.max(i2_old, i2_new)
                if i2 != -1
                    m = n
                    break
            cm.replaceRange(line+'\n', {line:m,ch:0}, {line:m+1,ch:0})
        else
            if misc.len(conf) == 0
                # don't put it in there if empty
                return
            cm.replaceRange('\n'+line, {line:cm.doc.lastLine()+1,ch:0})
        @latex_editor.syncdoc.sync()

    _pause_passive_search: (cb) =>
        @_passive_forward_search_disabled = true
        @_passive_inverse_search_disabled = true
        f = () =>
            @_passive_inverse_search_disabled = false
            @_passive_forward_search_disabled = false

        setTimeout(f, 3000)

    _passive_inverse_search: (cb) =>
        if @_passive_inverse_search_disabled
            cb?(); return
        @_pause_passive_search()
        @inverse_search
            active : false
            cb     : (err) =>
                cb?()

    _passive_forward_search: (cb) =>
        if @_passive_forward_search_disabled
            cb?(); return
        @forward_search
            active : false
            cb     : (err) =>
                @_pause_passive_search()
                cb?()

    action_key: () =>
        @show_page('png-preview')
        @forward_search(active:true)

    remove: () =>
        @latex_editor.remove()
        @element.remove()
        @preview.remove()
        @preview_embed?.remove()

    _init_buttons: () =>
        @element.find("a").tooltip(TOOLTIP_CONFIG)

        @element.find('a[href="#forward-search"]').click () =>
            @show_page('png-preview')
            @forward_search(active:true)
            return false

        @element.find('a[href="#inverse-search"]').click () =>
            @show_page('png-preview')
            @inverse_search(active:true)
            return false

        @element.find('a[href="#png-preview"]').click () =>
            @show_page('png-preview')
            @preview.focus()
            @save()
            return false

        @element.find('a[href="#help"]').click () =>
            @show_page('help')
            return false

        @element.find('a[href="#zoom-preview-out"]').click () =>
            @preview.zoom(delta: -5)
            @set_conf(zoom_width: @preview.zoom_width)
            return false

        @element.find('a[href="#zoom-preview-in"]').click () =>
            @preview.zoom(delta:5)
            @set_conf(zoom_width:@preview.zoom_width)
            return false

        @element.find('a[href="#zoom-preview-fullpage"]').click () =>
            @preview.zoom(width:100)
            @set_conf(zoom_width:@preview.zoom_width)
            return false

        @element.find('a[href="#zoom-preview-width"]').click () =>
            @preview.zoom(width:160)
            @set_conf(zoom_width:@preview.zoom_width)
            return false

        @element.find('a[href="#pdf-preview"]').click () =>
            # see https://github.com/sagemathinc/cocalc/issues/1313
            # it was broken on Firefox, but as of version 53 it works
            @show_page('pdf-preview')
            @preview_embed.focus()
            @preview_embed.update()
            return false

        @element.find('a[href="#log"]').click () =>
            @show_page('log')
            t = @log.find("textarea")
            t.scrollTop(t[0].scrollHeight)
            return false

        @element.find('a[href="#errors"]').click () =>
            @show_page('errors')
            return false

        @number_of_errors = @element.find('a[href="#errors"]').find(".webapp-latex-errors-counter")
        @number_of_warnings = @element.find('a[href="#errors"]').find(".webapp-latex-warnings-counter")

        @element.find('a[href="#pdf-download"]').click () =>
            @download_pdf()
            return false

        @element.find('a[href="#preview-resolution"]').click () =>
            @set_resolution()
            return false

        @_toggle_preview_button = @element.find('a[href="#toggle-preview"]')
        @_toggle_preview_button.click () =>
            @toggle_preview()
            return false

        #@element.find("a[href=\"#latex-command-undo\"]").click () =>
        #    c = @preview.pdflatex.default_tex_command()
        #    @log_input.val(c)
        #    @set_conf_doc(latex_command: c)
        #    return false

        trash_aux_button = @element.find('a[href="#latex-trash-aux"]')
        trash_aux_button.click () =>
            trash_aux_button.icon_spin(true, disable=true)
            log_output = @log.find("textarea")
            log_output.text('')
            # now we actually delete the files and report any errors in the log
            @preview.pdflatex.trash_aux_files (err, log) =>
                if err
                    log_output.text(err)
                else
                    log_output.text(log)
                # after deleting the log also clear all issues!
                @preview.pdflatex.last_latex_log = ''
                @render_error_page()
                trash_aux_button.icon_spin(false, disable=true)
            return false

        run_sage = @element.find('a[href="#latex-sage"]')
        run_sage.click () =>
            @log.find("textarea").text("Running Sage...")
            run_sage.icon_spin(true, disable=true)
            @preview.pdflatex._run_sage undefined, (err, log) =>
                run_sage.icon_spin(false, disable=true)
                @log.find("textarea").text(log)
            return false

        run_latex = @element.find('a[href="#latex-latex"]')
        run_latex.click () =>
            @log.find("textarea").text("Running Latex...")
            run_latex.icon_spin(true, disable=true)
            @preview.pdflatex._run_latex @load_conf_doc().latex_command, (err, log) =>
                @log.find("textarea").text(log)
                # the log changed, there could be issues, render them
                @render_error_page()
                run_latex.icon_spin(false, disable=true)
            return false

        run_recompile = @element.find('a[href="#latex-recompile"]')
        run_recompile.click () =>
            log_box = @log.find("textarea")
            log_box.text("Recompiling ...")
            run_recompile.icon_spin(true, disable=true)
            async.series([
                (cb) =>
                    @preview.pdflatex.trash_aux_files (err, _log) =>
                        cb(err)
                (cb) =>
                    @update_preview(cb:cb, force:true, only_compile:true)
            ], (err) =>
                run_recompile.icon_spin(false, disable=true)
            )
            return false

        run_bibtex = @element.find('a[href="#latex-bibtex"]')
        run_bibtex.click () =>
            @log.find("textarea").text("Running Bibtex...")
            run_bibtex.icon_spin(true, disable=true)
            @preview.pdflatex._run_bibtex (err, log) =>
                run_bibtex.icon_spin(false, disable=true)
                @log.find("textarea").text(log)
            return false

    set_resolution: (res) =>
        if not res?
            bootbox.prompt "Change preview resolution from #{@get_resolution()} dpi to...", (result) =>
                if result
                    @set_resolution(result)
        else
            try
                res = parseInt(res)
                if res < 75
                    res = 75
                else if res > 600
                    res = 600
                @preview.opts.resolution = res
                @set_conf(resolution : res)
                @preview.update()
            catch e
                alert_message
                    type    : "error"
                    message : "Invalid resolution #{res}"

    get_resolution: () =>
        if not @preview?
            return @load_conf()['resolution'] ? 150
        return @preview.opts.resolution

    # This function isn't called on save button click since
    # the codemirror's save button is called...
    click_save_button: () =>
        @latex_editor.click_save_button()

    # This function isn't called on save
    # @latex_editor.save is called instead for some reason
    save: (cb, force) =>
        force ?= false
        @latex_editor.save (err) =>
            cb?(err)
            if not err
                save_cb =  =>
                    if @_current_page == 'pdf-preview'
                        @preview_embed?.update()
                @update_preview(cb:save_cb, force:force)
                @spell_check()

    update_preview: (opts) =>
        {cb, force, only_compile} = opts = defaults opts,
            cb           : undefined
            force        : false
            only_compile : false
        # force: continue, even when content hasn't changed
        # only_compile: avoid the preview rendering
        # obvious TODO: untangle preview update and run_latex
        if not only_compile and not @_render_preview
            cb?()
            return

        # no content available, hence nothing to preview
        if not @latex_editor.syncdoc._fully_loaded
            cb?()
            return

        content = @_get()
        # issue #1814
        if not content?
            cb?()
            return

        if not force and content == @_last_update_preview
            cb?()
            return

        latex_command = @load_conf_doc().latex_command
        filename_tex  = @preview.pdflatex.filename_tex
        # a check, if we're really compiling the edited latex file
        if latex_command?  # could be undefined
            compiling_this_file = latex_command.indexOf(filename_tex) >= 0
        else
            compiling_this_file = true

        # check if latex file is invalid iff we're compiling it
        iil = @local_storage(LSkey.ignore_invalid_latex) ? false
        if not iil and not content.match(RE_FULL_LATEX_CODE) and compiling_this_file and not only_compile
            msg = @invalid_latex(filename_tex)
            @preview.show_message(msg)
            cb?()
            return

        if @_render_preview and not only_compile
            @preview.show_pages(true)
        preview_button = @element.find('a[href="#png-preview"]')
        async.series([
            (cb) =>
                @_last_update_preview = content
                if @latex_editor.has_unsaved_changes()
                    @latex_editor.save(cb)
                else
                    cb()
            (cb) =>
                if @latex_editor.has_uncommitted_changes()
                    delete @_last_update_preview  # running latex on stale version
                    @get_rnw_concordance_error = false
                #@dbg("update_preview(#{misc.to_json(opts)}): run_latex")
                @run_latex
                    command      : @load_conf_doc().latex_command
                    cb           : cb
            (cb) =>
                if only_compile
                    cb(); return
                #@dbg("update_preview(#{misc.to_json(opts)}): preview update")
                preview_button.icon_spin(true, disable=true)
                @preview.update
                    cb: cb
        ], (err) =>
            if not only_compile
                preview_button.icon_spin(false, disable=true)
            if err
{'fraction_non_alphanumeric': 0.13529158059610016, 'fraction_numerical': 0.0049939845186479925, 'mean_word_length': 1.9254266551563848, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 153, 'https://': 4, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3610293', 'n_tokens_mistral': 13270, 'n_tokens_neox': 12620, 'n_words': 2543}