Path: blob/master/src/packages/frontend/editor.coffee
5653 views
#########################################################################1# This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2# License: MS-RSL – see LICENSE.md for details3#########################################################################45$ = window.$67# Do this first, before any templates are initialized (e.g., elsewhere too).89{TEMPLATES_HTML} = require("./editor-templates")1011$("body").append(TEMPLATES_HTML);1213templates = $("#webapp-editor-templates")1415{ init_buttonbars } = require("./editors/editor-button-bar")16init_buttonbars()1718# Editor files in a project19# Show button labels if there are at most this many file tabs opened.20# This is in exports so that an elite user could customize this by doing, e.g.,21# require('./editor').SHOW_BUTTON_LABELS=022exports.SHOW_BUTTON_LABELS = 42324exports.MIN_SPLIT = MIN_SPLIT = 0.0225exports.MAX_SPLIT = MAX_SPLIT = 0.98 # maximum pane split proportion for editing2627TOOLTIP_DELAY = delay: {show: 500, hide: 100}2829async = require('async')3031message = require('@cocalc/util/message')3233{redux} = require('./app-framework')3435_ = underscore = require('underscore')3637{webapp_client} = require('./webapp-client')38{EventEmitter} = require('events')39{alert_message} = require('./alerts')40{ appBasePath } = require("@cocalc/frontend/customize/app-base-path");4142feature = require('./feature')43IS_MOBILE = feature.IS_MOBILE4445misc = require('@cocalc/util/misc')46{drag_start_iframe_disable, drag_stop_iframe_enable, sagews_canonical_mode} = require('./misc')4748# Ensure CodeMirror is available and configured49CodeMirror = require("codemirror")5051# SMELL: undo doing the import below -- just use misc.[stuff] is more readable.52{copy, trunc, from_json, to_json, keys, defaults, required, filename_extension, filename_extension_notilde,53len, path_split, uuid} = require('@cocalc/util/misc')5455syncdoc = require('./syncdoc')56sagews = require('./sagews/sagews')57printing = require('./printing')5859{file_nonzero_size} = require('./project/utils')6061copypaste = require('./copy-paste-buffer')6263extra_alt_keys = (extraKeys, editor, opts) ->64misc.merge extraKeys,65"Shift-Alt-L" : (cm) => cm.align_assignments()66'Alt-Z' : (cm) => cm.undo()67'Shift-Alt-Z' : (cm) => cm.redo()68'Alt-A' : (cm) => cm.execCommand('selectAll')69'Shift-Alt-A' : (cm) => cm.execCommand('selectAll')70'Alt-K' : (cm) => cm.execCommand('killLine')71'Alt-D' : (cm) => cm.execCommand('selectNextOccurrence')72'Alt-F' : (cm) => cm.execCommand('find')73'Shift-Alt-F' : (cm) => cm.execCommand('replace')74'Shift-Alt-R' : (cm) => cm.execCommand('replaceAll')75'Shift-Alt-D' : (cm) => cm.execCommand('duplicateLine')76'Alt-G' : (cm) => cm.execCommand('findNext')77'Shift-Alt-G' : (cm) => cm.execCommand('findPrev')78'Alt-Up' : (cm) => cm.execCommand('goPageUp')79'Alt-Down' : (cm) => cm.execCommand('goPageDown')80'Alt-K' : (cm) => cm.execCommand('goPageUp')81'Alt-J' : (cm) => cm.execCommand('goPageDown')82'Alt-P' : (cm) => cm.execCommand('goLineUp')83'Alt-N' : (cm) => cm.execCommand('goLineDown')8485if editor?.goto_line?86extraKeys['Alt-L'] = (cm) => editor.goto_line(cm)87if editor?.toggle_split_view?88extraKeys['Alt-I'] = (cm) => editor.toggle_split_view(cm)89if editor?.copy?90extraKeys['Alt-C'] = (cm) => editor.copy(cm) # gets overwritten for vim mode, of course91else92extraKeys['Alt-C'] = (cm) => copypaste.set_buffer(cm.getSelection())9394if editor?.cut?95extraKeys['Alt-X'] = (cm) => editor.cut(cm)96else97extraKeys['Alt-X'] = (cm) =>98copypaste.set_buffer(cm.getSelection())99cm.replaceSelection('')100if editor?.paste?101extraKeys['Alt-V'] = (cm) => editor.paste(cm)102else103extraKeys['Alt-V'] = (cm) => cm.replaceSelection(copypaste.get_buffer())104if editor?.click_save_button?105extraKeys['Alt-S'] = (cm) => editor.click_save_button()106else if editor?.save?107extraKeys['Alt-S'] = (cm) => editor.save()108109if opts.bindings == 'vim'110# An additional key to get to visual mode in vim (added for ipad Smart Keyboard)111extraKeys["Alt-C"] = (cm) =>112CodeMirror.Vim.exitInsertMode(cm)113extraKeys["Alt-F"] = (cm) =>114cm.execCommand('goPageDown')115extraKeys["Alt-B"] = (cm) =>116cm.execCommand('goPageUp')117118119{file_associations, VIDEO_EXTS} = require('./file-associations')120121file_nonzero_size_cb = (project_id, path, cb) =>122try123if not await file_nonzero_size(project_id, path)124cb("Unable to convert file to PDF")125else126cb()127catch err128cb(err)129130131initialize_new_file_type_list = () ->132file_types_so_far = {}133v = misc.keys(file_associations)134v.sort()135f = (elt, ext, exclude) ->136if not ext137return138data = file_associations[ext]139if exclude and data.exclude_from_menu140return141if data.name? and not file_types_so_far[data.name]142file_types_so_far[data.name] = true143e = $("<li><a href='#new-file' data-ext='#{ext}'><i style='width: 18px;' class='fa #{data.icon}'></i> <span style='text-transform:capitalize'>#{data.name} </span> <span class='lighten'>(.#{ext})</span></a></li>")144elt.append(e)145146elt = $(".smc-new-file-type-list")147for ext in v148f(elt, ext, true)149150elt = $(".smc-mini-new-file-type-list")151file_types_so_far = {}152for ext in ['sagews', 'term', 'ipynb', 'tex', 'md', 'tasks', 'course', 'sage', 'py']153f(elt, ext)154elt.append($("<li class='divider'></li><li><a href='#new-folder'><i style='width: 18px;' class='fa fa-folder'></i> <span>Folder </span></a></li>"))155156elt.append($("<li class='divider'></li><li><a href='#projects-add-collaborators'><i style='width: 18px;' class='fa fa-user'></i> <span>Collaborators... </span></a></li>"))157158initialize_new_file_type_list()159160exports.file_icon_class = file_icon_class = (ext) ->161assoc = exports.file_options('x.' + ext)162return assoc.icon163164# This defines a bunch of custom modes and gets some info about special case of sagews165{sagews_decorator_modes} = require('./codemirror/custom-modes')166167exports.file_options = require("./editor-tmp").file_options168169# old "local storage" code was here, now moved to TS170editor_local_storage = require('./editor-local-storage')171exports.local_storage_delete = editor_local_storage.local_storage_delete172exports.local_storage = editor_local_storage.local_storage173local_storage = exports.local_storage # used below174175###############################################176# Abstract base class for editors (not exports.Editor)177###############################################178# Derived classes must:179# (1) implement the _get and _set methods180# (2) show/hide/remove181#182# Events ensure that *all* users editor the same file see the same183# thing (synchronized).184#185186class FileEditor extends EventEmitter187# ATTN it is crucial to call this constructor in subclasses via super(@project_id, @filename)188constructor: (project_id, filename) ->189super()190@project_id = project_id191@filename = filename192@ext = misc.filename_extension_notilde(@filename)?.toLowerCase()193@_show = underscore.debounce(@_show, 50)194195is_active: () =>196misc.tab_to_path(redux.getProjectStore(@project_id).get('active_project_tab')) == @filename197198# call it, to set the @default_font_size from the account settings199init_font_size: () =>200@default_font_size = redux.getStore('account').get('font_size')201202val: (content) =>203if not content?204# If content not defined, returns current value.205return @_get()206else207# If content is defined, sets value.208@_set(content)209210# has_unsaved_changes() returns the state, where true means that211# there are unsaved changed. To set the state, do212# has_unsaved_changes(true or false).213has_unsaved_changes: (val) =>214if not val?215return @_has_unsaved_changes216else217if not @_has_unsaved_changes? or @_has_unsaved_changes != val218if val219@save_button.removeClass('disabled')220else221@_when_had_no_unsaved_changes = new Date() # when we last knew for a fact there are no unsaved changes222@save_button.addClass('disabled')223@_has_unsaved_changes = val224225# committed means "not saved to the database/server", whereas save above226# means "saved to *disk*".227has_uncommitted_changes: (val) =>228if not val?229return @_has_uncommitted_changes230else231@_has_uncommitted_changes = val232if val233if not @_show_uncommitted_warning_timeout?234# We have not already started a timer, so start one -- if we do not hear otherwise, show235# the warning in 30s.236@_show_uncommitted_warning_timeout = setTimeout((()=>@_show_uncommitted_warning()), 30000)237else238if @_show_uncommitted_warning_timeout?239clearTimeout(@_show_uncommitted_warning_timeout)240delete @_show_uncommitted_warning_timeout241@uncommitted_element?.hide()242243_show_uncommitted_warning: () =>244delete @_show_uncommitted_warning_timeout245@uncommitted_element?.show()246247focus: () => # FUTURE in derived class (???)248249_get: () =>250console.warn("Incomplete: editor -- needs to implement _get in derived class")251252_set: (content) =>253console.warn("Incomplete: editor -- needs to implement _set in derived class")254255restore_cursor_position: () =>256# implement in a derived class if you need this257258disconnect_from_session: (cb) =>259# implement in a derived class if you need this260261local_storage: (key, value) =>262return local_storage(@project_id, @filename, key, value)263264show: (opts) =>265if not opts?266if @_last_show_opts?267opts = @_last_show_opts268else269opts = {}270@_last_show_opts = opts271272# only re-render the editor if it is active. that's crucial, because e.g. the autosave273# of latex triggers a build, which in turn calls @show to update itself. that would cause274# the latex editor to be visible despite not being the active editor.275if not @is_active?()276return277278@element.show()279# if above line reveals it, give it a bit time to do the layout first280@_show(opts) # critical -- also do an initial layout! Otherwise get a horrible messed up animation effect.281setTimeout((=> @_show(opts)), 10)282if DEBUG283window?.smc?.doc = @ # useful for debugging...284285_show: (opts={}) =>286# define in derived class287288hide: () =>289#@element?.hide()290291remove: () =>292@syncdoc?.close()293@element?.remove()294@removeAllListeners()295296terminate_session: () =>297# If some backend session on a remote machine is serving this session, terminate it.298299exports.FileEditor = FileEditor300301###############################################302# Codemirror-based File Editor303304# - 'saved' : when the file is successfully saved by the user305# - 'show' :306# - 'toggle-split-view' :307###############################################308class CodeMirrorEditor extends FileEditor309constructor: (project_id, filename, content, opts) ->310super(project_id, filename)311312editor_settings = redux.getStore('account').get_editor_settings()313opts = @opts = defaults opts,314mode : undefined315geometry : undefined # (default=full screen);316read_only : false317delete_trailing_whitespace: editor_settings.strip_trailing_whitespace # delete on save318show_trailing_whitespace : editor_settings.show_trailing_whitespace319allow_javascript_eval : true # if false, the one use of eval isn't allowed.320line_numbers : editor_settings.line_numbers321first_line_number : editor_settings.first_line_number322indent_unit : editor_settings.indent_unit323tab_size : editor_settings.tab_size324smart_indent : editor_settings.smart_indent325electric_chars : editor_settings.electric_chars326undo_depth : editor_settings.undo_depth # no longer relevant, since done via sync system327match_brackets : editor_settings.match_brackets328code_folding : editor_settings.code_folding329auto_close_brackets : editor_settings.auto_close_brackets330match_xml_tags : editor_settings.match_xml_tags331auto_close_xml_tags : editor_settings.auto_close_xml_tags332line_wrapping : editor_settings.line_wrapping333spaces_instead_of_tabs : editor_settings.spaces_instead_of_tabs334style_active_line : 15 # editor_settings.style_active_line # (a number between 0 and 127)335bindings : editor_settings.bindings # 'standard', 'vim', or 'emacs'336theme : editor_settings.theme337track_revisions : editor_settings.track_revisions338public_access : false339latex_editor : false340341# I'm making the times below very small for now. If we have to adjust these to reduce load, due to lack342# of capacity, then we will. Or, due to lack of optimization (e.g., for big documents). These parameters343# below would break editing a huge file right now, due to slowness of applying a patch to a codemirror editor.344345cursor_interval : 1000 # minimum time (in ms) between sending cursor position info to hub -- used in sync version346sync_interval : 500 # minimum time (in ms) between synchronizing text with hub. -- used in sync version below347348completions_size : 20 # for tab completions (when applicable, e.g., for sage sessions)349350#console.log("mode =", opts.mode)351352@element = templates.find(".webapp-editor-codemirror").clone()353354@element.data('editor', @)355356@init_save_button()357@init_uncommitted_element()358@init_history_button()359@init_edit_buttons()360361@init_file_actions()362363filename = @filename364if filename.length > 30365filename = "…" + filename.slice(filename.length-30)366367# not really needed due to highlighted tab; annoying.368#@element.find(".webapp-editor-codemirror-filename").text(filename)369370@show_exec_warning = redux.getStore('account').getIn(['editor_settings', 'show_exec_warning']) ? true371if @show_exec_warning and @ext in ['py', 'r', 'sage', 'f90']372msg = "<strong>INFO:</strong> you can only run <code>*.#{@ext}</code> files in a terminal or create a worksheet/notebook. <a href='#'>Close</a>"373msg_el = @element.find('.webapp-editor-codemirror-message')374msg_el.html(msg)375msg_el.find('a').click ->376msg_el.hide()377redux.getTable('account').set(editor_settings:{show_exec_warning:false})378379@_video_is_on = @local_storage("video_is_on")380if not @_video_is_on?381@_video_is_on = false382383extraKeys =384"Alt-Enter" : (editor) => @action_key(execute: true, advance:false, split:false)385"Cmd-Enter" : (editor) => @action_key(execute: true, advance:false, split:false)386"Ctrl-Enter" : (editor) => @action_key(execute: true, advance:true, split:true)387"Ctrl-;" : (editor) => @action_key(split:true, execute:false, advance:false)388"Cmd-;" : (editor) => @action_key(split:true, execute:false, advance:false)389"Ctrl-\\" : (editor) => @action_key(execute:false, toggle_input:true)390#"Cmd-x" : (editor) => @action_key(execute:false, toggle_input:true)391"Shift-Ctrl-\\" : (editor) => @action_key(execute:false, toggle_output:true)392#"Shift-Cmd-y" : (editor) => @action_key(execute:false, toggle_output:true)393394"Cmd-S" : (editor) => @click_save_button()395"Alt-S" : (editor) => @click_save_button()396397"Ctrl-L" : (editor) => @goto_line(editor)398"Cmd-L" : (editor) => @goto_line(editor)399400"Shift-Ctrl-I" : (editor) => @toggle_split_view(editor)401"Shift-Cmd-I" : (editor) => @toggle_split_view(editor)402403"Shift-Cmd-L" : (editor) => editor.align_assignments()404"Shift-Ctrl-L" : (editor) => editor.align_assignments()405406"Shift-Ctrl-." : (editor) => @change_font_size(editor, +1)407"Shift-Ctrl-," : (editor) => @change_font_size(editor, -1)408409"Shift-Cmd-." : (editor) => @change_font_size(editor, +1)410"Shift-Cmd-," : (editor) => @change_font_size(editor, -1)411412"Shift-Tab" : (editor) => editor.unindent_selection()413414"Ctrl-'" : "indentAuto"415"Cmd-'" : "indentAuto"416417"Cmd-/" : "toggleComment"418"Ctrl-/" : "toggleComment" # shortcut chosen by jupyter project (undocumented)419420"Tab" : (editor) => @press_tab_key(editor)421"Shift-Ctrl-C" : (editor) => @interrupt_key()422423"Ctrl-Space" : "autocomplete"424"Alt-Space": "autocomplete"425426if feature.IS_TOUCH427# Better more external keyboard friendly shortcuts, motivated by iPad.428extra_alt_keys(extraKeys, @, opts)429430if opts.match_xml_tags431extraKeys['Ctrl-J'] = "toMatchingTag"432433if opts.bindings != 'emacs'434# Emacs uses control s for find.435extraKeys["Ctrl-S"] = (editor) => @click_save_button()436437# FUTURE: We will replace this by a general framework...438if misc.filename_extension_notilde(filename).toLowerCase() == "sagews"439evaluate_key = redux.getStore('account').get('evaluate_key').toLowerCase()440if evaluate_key == "enter"441evaluate_key = "Enter"442else443evaluate_key = "Shift-Enter"444extraKeys[evaluate_key] = (editor) => @action_key(execute: true, advance:true, split:false)445else446extraKeys["Shift-Enter"] = =>447alert_message448type : "error"449message : "You can only evaluate code in a file that ends with the extension 'sagews' or 'ipynb'. Create a Sage Worksheet or Jupyter notebook instead."450451# Layouts:452# 0 - one single editor453# 1 - two editors, one on top of the other454# 2 - two editors, one next to the other455456if IS_MOBILE457@_layout = 0458else459@_layout = @local_storage("layout") ? 0 # WARNING/UGLY: used by syncdoc.coffee and sagews.coffee !460if @_layout not in [0, 1, 2]461# IMPORTANT: If this were anything other than what is listed, the user462# would never be able to open tex files. So it's important that this be valid.463@_layout = 0464@_last_layout = undefined465466if feature.isMobile.Android()467# see https://github.com/sragemathinc/smc/issues/1360468opts.style_active_line = false469470make_editor = (node) =>471options =472firstLineNumber : opts.first_line_number473autofocus : false474mode : {name:opts.mode, globalVars: true}475lineNumbers : opts.line_numbers476showTrailingSpace : opts.show_trailing_whitespace477indentUnit : opts.indent_unit478tabSize : opts.tab_size479smartIndent : opts.smart_indent480electricChars : opts.electric_chars481undoDepth : opts.undo_depth482matchBrackets : opts.match_brackets483autoCloseBrackets : opts.auto_close_brackets and (misc.filename_extension_notilde(filename) not in ['hs', 'lhs']) #972484autoCloseTags : opts.auto_close_xml_tags485lineWrapping : opts.line_wrapping486readOnly : opts.read_only487styleActiveLine : opts.style_active_line488indentWithTabs : not opts.spaces_instead_of_tabs489showCursorWhenSelecting : true490extraKeys : extraKeys491cursorScrollMargin : 6492viewportMargin : Infinity # larger than the default of 10 specifically so *sage worksheets* (which are the only thing that uses this)493# don't feel jumpy when re-rendering output.494# NOTE that in cocalc right now, no remaining non-sagews editors use this code.495# with images, even using a viewport of 300 causes major jumpiness problems with images.496# See https://github.com/sagemathinc/cocalc/issues/7654497# Browser caching may have changed in newer browsers as well making 300 feel jumpy in "modern times".498499if opts.match_xml_tags500options.matchTags = {bothTags: true}501502if opts.code_folding503extraKeys["Ctrl-Q"] = (cm) -> cm.foldCodeSelectionAware()504extraKeys["Alt-Q"] = (cm) -> cm.foldCodeSelectionAware()505options.foldGutter = true506options.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"]507508if opts.latex_editor509options.gutters ?= []510options.gutters.push("Codemirror-latex-errors")511512if opts.bindings? and opts.bindings != "standard"513options.keyMap = opts.bindings514#cursorBlinkRate: 1000515516if opts.theme? and opts.theme != "standard"517options.theme = opts.theme518519cm = CodeMirror.fromTextArea(node, options)520cm.save = () => @click_save_button()521522# The Codemirror themes impose their own weird fonts, but most users want whatever523# they've configured as "monospace" in their browser. So we force that back:524e = $(cm.getWrapperElement())525e.attr('style', e.attr('style') + '; height:100%; font-family:monospace !important;')526# see http://stackoverflow.com/questions/2655925/apply-important-css-style-using-jquery527528if opts.bindings == 'vim'529# annoying due to api change in vim mode530cm.setOption("vimMode", true)531532return cm533534elt = @element.find(".webapp-editor-textarea-0"); elt.text(content)535536@codemirror = make_editor(elt[0])537@codemirror.name = '0'538#window.cm = @codemirror539540elt1 = @element.find(".webapp-editor-textarea-1")541542@codemirror1 = make_editor(elt1[0])543@codemirror1.name = '1'544545buf = @codemirror.linkedDoc({sharedHist: true})546@codemirror1.swapDoc(buf)547548@codemirror.on 'focus', () =>549@codemirror_with_last_focus = @codemirror550551@codemirror1.on 'focus', () =>552@codemirror_with_last_focus = @codemirror1553554if @opts.bindings == 'vim'555@_vim_mode = 'visual'556@codemirror.on 'vim-mode-change', (obj) =>557if obj.mode == 'normal'558@_vim_mode = 'visual'559@element.find("a[href='#vim-mode-toggle']").text('esc')560else561@_vim_mode = 'insert'562@element.find("a[href='#vim-mode-toggle']").text('i')563564if feature.IS_TOUCH565# ugly hack so more usable on touch...566@element.find(".webapp-editor-resize-bar-layout-1").height('12px')567@element.find(".webapp-editor-resize-bar-layout-2").width('12px')568569@init_font_size() # get the @default_font_size570@restore_font_size()571572@init_draggable_splits()573574if opts.read_only575@set_readonly_ui()576577if misc.filename_extension(@filename)?.toLowerCase() == 'sagews'578@init_sagews_edit_buttons()579580@snippets_dialog = null581# Render all icons using React.582@element.processIcons()583584programmatical_goto_line: (line) =>585cm = @codemirror_with_last_focus586return if not cm?587pos = {line:line-1, ch:0}588info = cm.getScrollInfo()589cm.scrollIntoView(pos, info.clientHeight/2)590591get_users_cursors: (account_id) =>592return @syncdoc?.get_users_cursors(account_id)593594init_file_actions: () =>595if not @element?596return597dom_node = @element.find('.smc-editor-file-info-dropdown')[0]598require('./editors/file-info-dropdown').render_file_info_dropdown(@filename, @project_id, dom_node, @opts.public_access)599600init_draggable_splits: () =>601@_layout1_split_pos = @local_storage("layout1_split_pos")602@_layout2_split_pos = @local_storage("layout2_split_pos")603604layout1_bar = @element.find(".webapp-editor-resize-bar-layout-1")605layout1_bar.draggable606axis : 'y'607containment : @element608zIndex : 10609start : drag_start_iframe_disable610stop : (event, ui) =>611drag_stop_iframe_enable()612# compute the position of bar as a number from 0 to 1, with613# 0 being at top (left), 1 at bottom (right), and .5 right in the middle614e = @element.find(".webapp-editor-codemirror-input-container-layout-1")615top = e.offset().top616ht = e.height()617p = layout1_bar.offset().top + layout1_bar.height()/2618@_layout1_split_pos = (p - top) / ht619@local_storage("layout1_split_pos", @_layout1_split_pos)620# redraw, which uses split info621@show()622623layout2_bar = @element.find(".webapp-editor-resize-bar-layout-2")624layout2_bar.draggable625axis : 'x'626containment : @element627zIndex : 100628start : drag_start_iframe_disable629stop : (event, ui) =>630drag_stop_iframe_enable()631# compute the position of bar as a number from 0 to 1, with632# 0 being at top (left), 1 at bottom (right), and .5 right in the middle633e = @element.find(".webapp-editor-codemirror-input-container-layout-2")634left = e.offset().left635width = e.width()636p = layout2_bar.offset().left637@_layout2_split_pos = (p - left) / width638@local_storage("layout2_split_pos", @_layout2_split_pos)639# redraw, which uses split info640@show()641642hide_content: () =>643@element.find(".webapp-editor-codemirror-content").hide()644645show_content: () =>646@hide_startup_message()647@element.find(".webapp-editor-codemirror-content").show()648for cm in @codemirrors()649cm_refresh(cm)650651hide_startup_message: () =>652@element.find(".webapp-editor-codemirror-startup-message").hide()653654show_startup_message: (mesg, type='info') =>655@hide_content()656if typeof(mesg) != 'string'657mesg = JSON.stringify(mesg)658e = @element.find(".webapp-editor-codemirror-startup-message").show().text(mesg)659for t in ['success', 'info', 'warning', 'danger']660e.removeClass("alert-#{t}")661e.addClass("alert-#{type}")662663is_active: () =>664return @codemirror? and misc.tab_to_path(redux.getProjectStore(@project_id).get('active_project_tab')) == @filename665666set_theme: (theme) =>667# Change the editor theme after the editor has been created668for cm in @codemirrors()669cm.setOption('theme', theme)670@opts.theme = theme671672# add something visual to the UI to suggest that the file is read only673set_readonly_ui: (readonly=true) =>674@opts.read_only = readonly675@element.find(".webapp-editor-write-only").toggle(!readonly)676@element.find(".webapp-editor-read-only").toggle(readonly)677for cm in @codemirrors()678cm.setOption('readOnly', readonly)679680set_cursor_center_focus: (pos, tries=5) =>681if tries <= 0682return683cm = @codemirror_with_last_focus684if not cm?685cm = @codemirror686if not cm?687return688cm.setCursor(pos)689info = cm.getScrollInfo()690try691# This call can fail during editor initialization (as of codemirror 3.19, but not before).692cm.scrollIntoView(pos, info.clientHeight/2)693catch e694setTimeout((() => @set_cursor_center_focus(pos, tries-1)), 250)695cm.focus()696697disconnect_from_session: (cb) =>698# implement in a derived class if you need this699@syncdoc?.disconnect_from_session()700cb?()701702codemirrors: () =>703c = [@codemirror, @codemirror1]704return underscore.filter(c, ((x) -> x?))705706focused_codemirror: () =>707if @codemirror_with_last_focus?708return @codemirror_with_last_focus709else710return @codemirror711712action_key: (opts) =>713# opts ignored by default; worksheets use them....714@click_save_button()715716interrupt_key: () =>717# does nothing for generic editor, but important, e.g., for the sage worksheet editor.718719press_tab_key: (editor) =>720if editor.somethingSelected()721CodeMirror.commands.defaultTab(editor)722else723@tab_nothing_selected(editor)724725tab_nothing_selected: (editor) =>726if @opts.spaces_instead_of_tabs727editor.tab_as_space()728else729CodeMirror.commands.defaultTab(editor)730731init_edit_buttons: () =>732that = @733button_names = ['search', 'next', 'prev', 'replace', 'undo', 'redo', 'autoindent',734'shift-left', 'shift-right', 'split-view','increase-font', 'decrease-font', 'goto-line',735'copy', 'paste', 'vim-mode-toggle']736737if @opts.bindings != 'vim'738@element.find("a[href='#vim-mode-toggle']").remove()739740# if the file extension indicates that we know how to print it, show and enable the print button741if printing.can_print(@ext)742button_names.push('print')743else744@element.find('a[href="#print"]').remove()745746# sagews2pdf conversion747if @ext == 'sagews'748button_names.push('sagews2pdf')749button_names.push('sagews2ipynb')750else751@element.find('a[href="#sagews2pdf"]').remove()752@element.find('a[href="#sagews2ipynb"]').remove()753754for name in button_names755e = @element.find("a[href=\"##{name}\"]")756e.data('name', name).tooltip(delay:{ show: 500, hide: 100 }).click (event) ->757that.click_edit_button($(@).data('name'))758return false759760click_edit_button: (name) =>761cm = @codemirror_with_last_focus762if not cm?763cm = @codemirror764if not cm?765return766switch name767when 'search'768CodeMirror.commands.find(cm)769when 'next'770if cm._searchState?.query771CodeMirror.commands.findNext(cm)772else773CodeMirror.commands.goPageDown(cm)774cm.focus()775when 'prev'776if cm._searchState?.query777CodeMirror.commands.findPrev(cm)778else779CodeMirror.commands.goPageUp(cm)780cm.focus()781when 'replace'782CodeMirror.commands.replace(cm)783when 'undo'784cm.undo()785cm.focus()786when 'redo'787cm.redo()788cm.focus()789when 'split-view'790@toggle_split_view(cm)791when 'autoindent'792CodeMirror.commands.indentAuto(cm)793when 'shift-left'794cm.unindent_selection()795cm.focus()796when 'shift-right'797@press_tab_key(cm)798cm.focus()799when 'increase-font'800@change_font_size(cm, +1)801cm.focus()802when 'decrease-font'803@change_font_size(cm, -1)804cm.focus()805when 'goto-line'806@goto_line(cm)807when 'copy'808@copy(cm)809cm.focus()810when 'paste'811@paste(cm)812cm.focus()813when 'sagews2pdf'814@print(sagews2html = false)815when 'sagews2ipynb'816@convert_to_ipynb()817when 'print'818@print(sagews2html = true)819when 'vim-mode-toggle'820if @_vim_mode == 'visual'821CodeMirror.Vim.handleKey(cm, 'i')822else823CodeMirror.Vim.exitInsertMode(cm)824cm.focus()825826restore_font_size: () =>827# we set the font_size from local storage828# or fall back to the default from the account settings829for i, cm of @codemirrors()830size = @local_storage("font_size#{i}")831if size?832@set_font_size(cm, size)833else if @default_font_size?834@set_font_size(cm, @default_font_size)835836get_font_size: (cm) ->837if not cm?838return839elt = $(cm.getWrapperElement())840return elt.data('font-size') ? @default_font_size841842set_font_size: (cm, size) =>843if not cm?844return845if size > 1846elt = $(cm.getWrapperElement())847elt.css('font-size', size + 'px')848elt.data('font-size', size)849850change_font_size: (cm, delta) =>851if not cm?852return853#console.log("change_font_size #{cm.name}, #{delta}")854scroll_before = cm.getScrollInfo()855856elt = $(cm.getWrapperElement())857size = elt.data('font-size')858if not size?859s = elt.css('font-size')860size = parseInt(s.slice(0,s.length-2))861new_size = size + delta862@set_font_size(cm, new_size)863@local_storage("font_size#{cm.name}", new_size)864865# we have to do the scrollTo in the next render loop, since otherwise866# the getScrollInfo function below will return the sizing data about867# the cm instance before the above css font-size change has been rendered.868f = () =>869cm_refresh(cm)870scroll_after = cm.getScrollInfo()871x = (scroll_before.left / scroll_before.width) * scroll_after.width872y = (((scroll_before.top+scroll_before.clientHeight/2) / scroll_before.height) * scroll_after.height) - scroll_after.clientHeight/2873cm.scrollTo(x, y)874setTimeout(f, 0)875876toggle_split_view: (cm) =>877if not cm?878return879@_layout = (@_layout + 1) % 3880@local_storage("layout", @_layout)881@show()882if cm? and not feature.IS_TOUCH883if @_layout > 0884cm.focus()885else886# focus first editor since it is only one that is visible.887@codemirror.focus()888f = () =>889for x in @codemirrors()890x.scrollIntoView() # scroll the cursors back into view -- see https://github.com/sagemathinc/cocalc/issues/1044891setTimeout(f, 1) # wait until next loop after codemirror has laid itself out.892@emit 'toggle-split-view'893894goto_line: (cm) =>895if not cm?896return897focus = () =>898@focus()899cm.focus()900dialog = templates.find(".webapp-goto-line-dialog").clone()901dialog.modal('show')902dialog.find(".btn-close").off('click').click () ->903dialog.modal('hide')904setTimeout(focus, 50)905return false906input = dialog.find(".webapp-goto-line-input")907input.val(cm.getCursor().line+1) # +1 since line is 0-based908dialog.find(".webapp-goto-line-range").text("1-#{cm.lineCount()} or n%")909dialog.find(".webapp-goto-line-input").focus().select()910submit = () =>911dialog.modal('hide')912result = input.val().trim()913if result.length >= 1 and result[result.length-1] == '%'914line = Math.floor( cm.lineCount() * parseInt(result.slice(0,result.length-1)) / 100.0)915else916line = Math.min(parseInt(result)-1)917if line >= cm.lineCount()918line = cm.lineCount() - 1919if line <= 0920line = 0921pos = {line:line, ch:0}922cm.setCursor(pos)923info = cm.getScrollInfo()924cm.scrollIntoView(pos, info.clientHeight/2)925setTimeout(focus, 50)926dialog.find(".btn-submit").off('click').click(submit)927input.keydown (evt) =>928if evt.which == 13 # enter929submit()930return false931if evt.which == 27 # escape932setTimeout(focus, 50)933dialog.modal('hide')934return false935936copy: (cm) =>937if not cm?938return939copypaste.set_buffer(cm.getSelection())940941convert_to_ipynb: () =>942p = misc.path_split(@filename)943v = p.tail.split('.')944if v.length <= 1945ext = ''946base = p.tail947else948ext = v[v.length-1]949base = v.slice(0,v.length-1).join('.')950951if ext != 'sagews'952console.error("editor.print called on file with extension '#{ext}' but only supports 'sagews'.")953return954955async.series([956(cb) =>957@save(cb)958(cb) =>959webapp_client.exec960project_id : @project_id961command : "cc-sagews2ipynb"962args : [@filename]963err_on_exit : true964cb : (err, output) =>965if err966alert_message(type:"error", message:"Error occured converting '#{@filename}' -- #{err}")967else968path = base + '.ipynb'969if p.head970path = p.head + '/' + path971redux.getProjectActions(@project_id).open_file972path : path973foreground : true974])975976cut: (cm) =>977if not cm?978return979copypaste.set_buffer(cm.getSelection())980cm.replaceSelection('')981982paste: (cm) =>983if not cm?984return985cm.replaceSelection(copypaste.get_buffer())986987print: (sagews2html = true) =>988switch @ext989when 'sagews'990if sagews2html991@print_html()992else993@print_sagews()994when 'txt', 'csv'995print_button = @element.find('a[href="#print"]')996print_button.icon_spin(start:true, delay:0).addClass("disabled")997printing.Printer(@, @filename + '.pdf').print (err) ->998print_button.removeClass('disabled')999print_button.icon_spin(false)1000if err1001alert_message1002type : "error"1003message : "Printing error -- #{err}"10041005print_html: =>1006dialog = null1007d_content = null1008d_open = null1009d_download = null1010d_progress = _.noop1011output_fn = null # set this before showing the dialog10121013show_dialog = (cb) =>1014# this creates the dialog element and defines the action functions like d_progress1015dialog = $("""1016<div class="modal" tabindex="-1" role="dialog">1017<div class="modal-dialog" role="document">1018<div class="modal-content">1019<div class="modal-header">1020<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>1021<h4 class="modal-title">Print to HTML</h4>1022</div>1023<div class="modal-body">1024<div class="progress">1025<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">10260 %1027</div>1028</div>1029<div class="content" style="text-align: center;"></div>1030<div style="margin-top: 25px;">1031<p><b>More information</b></p>1032<p>1033This SageWS to HTML conversion transforms the current worksheet1034to a static HTML file.1035<br/>1036<a href="https://github.com/sagemathinc/cocalc/wiki/sagews2html" target='_blank'>Click here for more information</a>.1037</p>1038</div>1039</div>1040<div class="modal-footer">1041<button type="button" class="btn-download btn btn-primary disabled">Download</button>1042<button type="button" class="btn-open btn btn-success disabled">Open</button>1043<button type="button" class="btn-close btn btn-default" data-dismiss="modal">Close</button>1044</div>1045</div>1046</div>1047</div>1048""")1049d_content = dialog.find('.content')1050d_open = dialog.find('.btn-open')1051d_download = dialog.find('.btn-download')1052action = redux.getProjectActions(@project_id)1053d_progress = (p) ->1054pct = "#{Math.round(100 * p)}%"1055dialog.find(".progress-bar").css('width', pct).text(pct)1056dialog.find('.btn-close').click ->1057dialog.modal('hide')1058return false1059d_open.click =>1060action.download_file1061path : output_fn1062auto : false # open in new tab1063d_download.click =>1064action.download_file1065path : output_fn1066auto : true1067dialog.modal('show')1068cb()10691070convert = (cb) =>1071# initiates the actual conversion via printing.Printer ...1072switch @ext1073when 'sagews'1074output_fn = @filename + '.html'1075progress = (percent, mesg) =>1076d_content.text(mesg)1077d_progress(percent)1078progress = _.debounce(progress, 5)1079progress(.01, "Loading ...")1080done = (err) =>1081#console.log 'Printer.print_html is done: err = ', err1082if err1083progress(0, "Problem printing to HTML: #{err}")1084else1085progress(1, 'Printing finished.')1086# enable open & download buttons1087dialog.find('button.btn').removeClass('disabled')1088printing.Printer(@, output_fn).print(done, progress)1089cb(); return10901091# fallback1092cb("err -- unable to convert files with extension '@ext'")10931094async.series([show_dialog, convert], (err) =>1095if err1096msg = "problem printing -- #{err.message}"1097alert_message1098type : "error"1099message : msg1100dialog.content.text(msg)1101)11021103# WARNING: this "print" is actually for printing Sage worksheets, not arbitrary files.1104print_sagews: =>1105dialog = templates.find(".webapp-file-print-dialog").clone()1106dialog.processIcons()1107p = misc.path_split(@filename)1108v = p.tail.split('.')1109if v.length <= 11110ext = ''1111base = p.tail1112else1113ext = v[v.length-1]1114base = v.slice(0,v.length-1).join('.')11151116ext = ext.toLowerCase()1117if ext != 'sagews'1118console.error("editor.print called on file with extension '#{ext}' but only supports 'sagews'.")1119return11201121submit = () =>1122dialog.find(".webapp-file-printing-progress").show()1123dialog.find(".webapp-file-printing-link").hide()1124$print_tempdir = dialog.find(".smc-file-printing-tempdir")1125$print_tempdir.hide()1126is_subdir = dialog.find(".webapp-file-print-keepfiles").is(":checked")1127dialog.find(".btn-submit").icon_spin(start:true)1128pdf = undefined1129async.series([1130(cb) =>1131@save(cb)1132(cb) =>1133# get info from the UI and attempt to convert the sagews to pdf1134options =1135title : dialog.find(".webapp-file-print-title").text()1136author : dialog.find(".webapp-file-print-author").text()1137date : dialog.find(".webapp-file-print-date").text()1138contents : dialog.find(".webapp-file-print-contents").is(":checked")1139subdir : is_subdir1140base_url : require('./misc').BASE_URL # really is a base url (not base path)1141extra_data : misc.to_json(@syncdoc.print_to_pdf_data()) # avoid de/re-json'ing11421143printing.Printer(@, @filename + '.pdf').print1144project_id : @project_id1145path : @filename1146options : options1147cb : (err, _pdf) =>1148if err and not is_subdir1149cb(err)1150else1151pdf = _pdf1152cb()1153(cb) =>1154file_nonzero_size_cb(@project_id, pdf, cb)1155(cb) =>1156if is_subdir or not pdf?1157cb(); return1158# pdf file exists -- show it in the UI1159url = webapp_client.project_client.read_file1160project_id : @project_id1161path : pdf1162dialog.find(".webapp-file-printing-link").attr('href', url).text(pdf).show()1163cb()1164(cb) =>1165if not is_subdir1166cb(); return1167{join} = require('path')1168subdir_texfile = join(p.head, "#{base}-sagews2pdf", "tmp.tex")1169# check if generated tmp.tex exists and has nonzero size1170file_nonzero_size_cb @project_id, subdir_texfile, (err) =>1171if err1172cb("Unable to create directory of temporary Latex files. -- #{err}")1173return1174tempdir_link = $('<a>').text('Click to open temporary file')1175tempdir_link.click =>1176redux.getProjectActions(@project_id).open_file1177path : subdir_texfile1178foreground : true1179dialog.modal('hide')1180return false1181$print_tempdir.html(tempdir_link)1182$print_tempdir.show()1183cb()1184(cb) =>1185# if there is no subdirectory of temporary files, print generated pdf file1186if not is_subdir1187redux.getProjectActions(@project_id).print_file(path: pdf)1188cb()1189], (err) =>1190dialog.find(".btn-submit").icon_spin(false)1191dialog.find(".webapp-file-printing-progress").hide()1192if err1193alert_message(type:"error", message:"problem printing '#{p.tail}' -- #{err.message}")1194)1195return false11961197dialog.find(".webapp-file-print-filename").text(@filename)1198dialog.find(".webapp-file-print-title").text(base)1199dialog.find(".webapp-file-print-author").text(redux.getStore('account').get_fullname())1200dialog.find(".webapp-file-print-date").text((new Date()).toLocaleDateString())1201dialog.find(".btn-submit").click(submit)1202dialog.find(".btn-close").click(() -> dialog.modal('hide'); return false)1203if ext == "sagews"1204dialog.find(".webapp-file-options-sagews").show()1205dialog.modal('show')12061207init_save_button: () =>1208@save_button = @element.find("a[href=\"#save\"]").tooltip().click(@click_save_button)1209@save_button.find(".spinner").hide()12101211init_uncommitted_element: () =>1212@uncommitted_element = @element.find(".smc-uncommitted")12131214init_history_button: () =>1215if not @opts.public_access and @filename.slice(@filename.length-13) != '.sage-history'1216@history_button = @element.find(".webapp-editor-history-button")1217@history_button.click(@click_history_button)1218@history_button.show()1219@history_button.css1220display: 'inline-block' # this is needed due to subtleties of jQuery show().12211222click_save_button: () =>1223if @opts.read_only1224return1225if not @save? # not implemented...1226return1227if @_saving1228return1229@_saving = true1230@syncdoc?.delete_trailing_whitespace?() # only delete trailing whitespace on explicit save -- never on AUTOSAVE.1231@save_button.icon_spin(start:true, delay:8000)1232@save (err) =>1233# WARNING: As far as I can tell, this doesn't call FileEditor.save1234if err1235if redux.getProjectStore(@project_id).is_file_open(@filename) # only show error if file actually opened1236alert_message(type:"error", message:"Error saving '#{@filename}' (#{err}) -- (you might need to close and open this file or restart this project)")1237else1238@emit('saved')1239@save_button.icon_spin(false)1240@_saving = false1241return false12421243click_history_button: () =>1244redux.getProjectActions(@project_id).open_file1245path : misc.history_path(@filename)1246foreground : true12471248_get: () =>1249return @codemirror?.getValue()12501251_set: (content) =>1252if not @codemirror?1253# document is already closed and freed up.1254return1255{from} = @codemirror.getViewport()1256@codemirror.setValue(content)1257@codemirror.scrollIntoView(from)1258# even better -- fully restore cursors, if available in localStorage1259setTimeout((()=>@restore_cursor_position()),1) # do in next round, so that both editors get set by codemirror first (including the linked one)12601261# save/restore view state -- hooks used by React editor wrapper.1262save_view_state: =>1263state =1264scroll : (cm.getScrollInfo() for cm in @codemirrors())1265@_view_state = state1266return state12671268restore_view_state: (second_try) =>1269state = @_view_state1270if not state?1271return1272cms = @codemirrors()1273i = 01274for v in state.scroll1275cm = cms[i]1276if cm?1277cm.scrollTo(v.left, v.top)1278info = cm.getScrollInfo()1279# THIS IS HORRIBLE and SUCKS, but I can't understand what is going on sufficiently1280# well to remove this. Sometimes scrollTo fails (due to the document being reported as much1281# smaller than it is for a few ms) **and** it's then not possible to scroll,1282# so we just try again. See https://github.com/sagemathinc/cocalc/issues/13271283if not second_try and info.top != v.top1284# didn't work -- not fully visible; try again one time when rendering is presumably done.1285setTimeout((=>@restore_view_state(true)), 250)1286i += 112871288restore_cursor_position: () =>1289for i, cm of @codemirrors()1290if cm?1291pos = @local_storage("cursor#{cm.name}")1292if pos?1293cm.setCursor(pos)1294#console.log("#{@filename}: setting view #{cm.name} to cursor pos -- #{misc.to_json(pos)}")1295info = cm.getScrollInfo()1296try1297cm.scrollIntoView(pos, info.clientHeight/2)1298catch e1299#console.log("#{@filename}: failed to scroll view #{cm.name} into view -- #{e}")1300@codemirror?.focus()13011302# set background color of active line in editor based on background color (which depends on the theme)1303_style_active_line: () =>1304if not @opts.style_active_line1305return1306rgb = $(@codemirror.getWrapperElement()).css('background-color')1307v = (parseInt(x) for x in rgb.slice(4,rgb.length-1).split(','))1308amount = @opts.style_active_line1309for i in [0..2]1310if v[i] >= 1281311v[i] -= amount1312else1313v[i] += amount1314$("body").remove("#webapp-cm-activeline")1315$("body").append("<style id='webapp-cm-activeline' type=text/css>.CodeMirror-activeline{background:rgb(#{v[0]},#{v[1]},#{v[2]});}</style>") # this is a memory leak!13161317_show_codemirror_editors: (height) =>1318# console.log("_show_codemirror_editors: #{@_layout}")1319if not @codemirror?1320# already closed so can't show (in syncdoc, .codemirorr is deleted on close)1321return1322switch @_layout1323when 01324p = 11325when 11326p = @_layout1_split_pos ? 0.51327when 21328p = @_layout2_split_pos ? 0.513291330# Change the height of the *top* div that contain the editors; the bottom one then1331# uses of all remaining vertical height.1332if @_layout > 01333p = Math.max(MIN_SPLIT, Math.min(MAX_SPLIT, p))13341335# We set only the default size of the *first* div -- everything else expands accordingly.1336elt = @element.find(".webapp-editor-codemirror-input-container-layout-#{@_layout}").show()13371338if @_layout == 11339@element.find(".webapp-editor-resize-bar-layout-1").css(top:0)1340else if @_layout == 21341@element.find(".webapp-editor-resize-bar-layout-2").css(left:0)13421343c = elt.find(".webapp-editor-codemirror-input-box")1344if @_layout == 01345c.css('flex', 1) # use the full vertical height1346else1347c.css('flex-basis', "#{p*100}%")13481349if @_last_layout != @_layout1350# The layout has changed1351btn = @element.find('a[href="#split-view"]')13521353if @_last_layout?1354# Hide previous1355btn.find(".webapp-editor-layout-#{@_last_layout}").hide()1356@element.find(".webapp-editor-codemirror-input-container-layout-#{@_last_layout}").hide()13571358# Show current1359btn.find(".webapp-editor-layout-#{@_layout}").show()13601361# Put editors in their place -- in the div inside of each box1362elt.find(".webapp-editor-codemirror-input-box div").empty().append($(@codemirror.getWrapperElement()))1363elt.find(".webapp-editor-codemirror-input-box-1 div").empty().append($(@codemirror1.getWrapperElement()))13641365# Save for next time1366@_last_layout = @_layout13671368refresh = (cm) =>1369return if not cm?1370cm_refresh(cm)1371# See https://github.com/sagemathinc/cocalc/issues/1327#issuecomment-2654888721372setTimeout((=>cm_refresh(cm)), 1)13731374for cm in @codemirrors()1375refresh(cm)13761377@emit('show')13781379_show: (opts={}) =>1380# show the element that contains this editor1381#@element.show()1382# show the codemirror editors, resizing as needed1383@_show_codemirror_editors()13841385focus: () =>1386if not @codemirror?1387return1388@show()1389if not (IS_MOBILE or feature.IS_TOUCH)1390@codemirror_with_last_focus?.focus()13911392############1393# Editor button bar support code1394############1395textedit_command: (cm, cmd, args) =>1396# ATTN when adding more cases, also edit textedit_only_show_known_buttons1397switch cmd1398when "link"1399cm.insert_link(cb:() => @syncdoc?.sync())1400return false # don't return true or get an infinite recurse1401when "image"1402cm.insert_image(cb:() => @syncdoc?.sync())1403return false # don't return true or get an infinite recurse1404when "SpecialChar"1405cm.insert_special_char(cb:() => @syncdoc?.sync())1406return false # don't return true or get an infinite recurse1407else1408cm.edit_selection1409cmd : cmd1410args : args1411@syncdoc?.sync()1412# needed so that dropdown menu closes when clicked.1413return true14141415example_insert_handler: (insert) =>1416# insert : {lang: string, descr: string, code: string[]}1417{code, lang} = insert1418cm = @focused_codemirror()1419line = cm.getCursor().line1420# ATTN: to make this work properly, code and descr need to have a newline at the end (stripped by default)1421if insert.descr?1422@syncdoc?.insert_new_cell(line)1423# insert a "hidden" markdown cell and evaluate it1424cm.replaceRange("%md(hide=True)\n#{insert.descr}\n", {line : line+1, ch:0})1425@action_key(execute: true, advance:false, split:false)14261427# inserting one or more code cells1428for c in code1429line = cm.getCursor().line1430# next, we insert the code cell and prefix it with a mode change,1431# iff the mode is different from the current one1432@syncdoc?.insert_new_cell(line)1433cell = "#{c}\n"1434if lang != @_current_mode1435# special case: %sh for bash language1436if lang == 'bash' then lang = 'sh'1437cell = "%#{lang}\n#{cell}"1438cm.replaceRange(cell, {line : line+1, ch:0})1439# and we evaluate and sync all this, too…1440@action_key(execute: true, advance:false, split:false)1441@syncdoc?.sync()14421443# add a textedit toolbar to the editor1444init_sagews_edit_buttons: () =>1445if @opts.read_only # no editing button bar needed for read-only files1446return14471448if IS_MOBILE # no edit button bar on mobile either -- too big (for now at least)1449return14501451if not redux.getStore('account').get_editor_settings().extra_button_bar1452# explicitly disabled by user1453return14541455NAME_TO_MODE = {xml:'html', markdown:'md', mediawiki:'wiki'}1456for x in sagews_decorator_modes1457mode = x[0]1458name = x[1]1459v = name.split('-')1460if v.length > 11461name = v[1]1462NAME_TO_MODE[name] = "#{mode}"14631464name_to_mode = (name) ->1465n = NAME_TO_MODE[name]1466if n?1467return n1468else1469return "#{name}"14701471# add the text editing button bar1472e = @element.find(".webapp-editor-codemirror-textedit-buttons")1473@textedit_buttons = templates.find(".webapp-editor-textedit-buttonbar").clone().hide()1474e.append(@textedit_buttons).show()14751476# add the code editing button bar1477@codeedit_buttons = templates.find(".webapp-editor-codeedit-buttonbar").clone()1478e.append(@codeedit_buttons)14791480# the r-editing button bar1481@redit_buttons = templates.find(".webapp-editor-redit-buttonbar").clone()1482e.append(@redit_buttons)14831484# the Julia-editing button bar1485@julia_edit_buttons = templates.find(".webapp-editor-julia-edit-buttonbar").clone()1486e.append(@julia_edit_buttons)14871488# the sh-editing button bar1489@sh_edit_buttons = templates.find(".webapp-editor-sh-edit-buttonbar").clone()1490e.append(@sh_edit_buttons)14911492@cython_buttons = templates.find(".webapp-editor-cython-buttonbar").clone()1493e.append(@cython_buttons)14941495@fallback_buttons = templates.find(".webapp-editor-fallback-edit-buttonbar").clone()1496e.append(@fallback_buttons)14971498all_edit_buttons = [@textedit_buttons, @codeedit_buttons, @redit_buttons,1499@cython_buttons, @julia_edit_buttons, @sh_edit_buttons, @fallback_buttons]15001501# activite the buttons in the bar1502that = @1503edit_button_click = (e) ->1504e.preventDefault()1505args = $(this).data('args')1506cmd = $(this).attr('href').slice(1)1507if cmd == 'todo'1508return1509if args? and typeof(args) != 'object'1510args = "#{args}"1511if args.indexOf(',') != -11512args = args.split(',')1513return that.textedit_command(that.focused_codemirror(), cmd, args)15141515# FUTURE: activate color editing buttons -- for now just hide them1516@element.find(".sagews-output-editor-foreground-color-selector").hide()1517@element.find(".sagews-output-editor-background-color-selector").hide()15181519@fallback_buttons.find('a[href="#todo"]').click () =>1520bootbox.alert("<i class='fa fa-wrench' style='font-size: 18pt;margin-right: 1em;'></i> Button bar not yet implemented in <code>#{mode_display.text()}</code> cells.")1521return false15221523for edit_buttons in all_edit_buttons1524edit_buttons.find("a").click(edit_button_click)1525edit_buttons.find("*[title]").tooltip(TOOLTIP_DELAY)15261527@mode_display = mode_display = @element.find(".webapp-editor-codeedit-buttonbar-mode")1528@_current_mode = "sage"1529@mode_display.show()15301531# not all textedit buttons are known1532textedit_only_show_known_buttons = (name) =>1533EDIT_COMMANDS = require('./editors/editor-button-bar').commands1534default_mode = @focused_codemirror()?.get_edit_mode() ? 'sage'1535mode = sagews_canonical_mode(name, default_mode)1536#if DEBUG then console.log "textedit_only_show_known_buttons: mode #{name} → #{mode}"1537known_commands = misc.keys(EDIT_COMMANDS[mode] ? {})1538# see special cases in 'textedit_command' and codemirror/extensions: 'edit_selection'1539known_commands = known_commands.concat(['link', 'image', 'SpecialChar', 'font_size'])1540for button in @textedit_buttons.find('a')1541button = $(button)1542cmd = button.attr('href').slice(1)1543# in theory, this should also be done for html&md, but there are many more special cases1544# therefore we just make sure they're all activated again1545button.toggle((mode != 'tex') or (cmd in known_commands))15461547set_mode_display = (name) =>1548#console.log("set_mode_display: #{name}")1549if name?1550mode = name_to_mode(name)1551else1552mode = ""1553mode_display.text("%" + mode)1554@_current_mode = mode15551556show_edit_buttons = (which_one, name) =>1557for edit_buttons in all_edit_buttons1558edit_buttons.toggle(edit_buttons == which_one)1559if which_one == @textedit_buttons1560textedit_only_show_known_buttons(name)1561set_mode_display(name)15621563# this is deprecated1564@element.find('.webapp-editor-codeedit-buttonbar-assistant').hide()15651566# The code below changes the bar at the top depending on where the cursor1567# is located. We only change the edit bar if the cursor hasn't moved for1568# a while, to be more efficient, avoid noise, and be less annoying to the user.1569# Replaced by http://underscorejs.org/#debounce1570#bar_timeout = undefined1571#f = () =>1572# if bar_timeout?1573# clearTimeout(bar_timeout)1574# bar_timeout = setTimeout(update_context_sensitive_bar, 250)15751576update_context_sensitive_bar = () =>1577cm = @focused_codemirror()1578if not cm?1579return1580pos = cm.getCursor()1581name = cm.getModeAt(pos).name1582#console.log("update_context_sensitive_bar, pos=#{misc.to_json(pos)}, name=#{name}")1583if name in ['xml', 'stex', 'markdown', 'mediawiki']1584show_edit_buttons(@textedit_buttons, name)1585else if name == "r"1586show_edit_buttons(@redit_buttons, name)1587else if name == "julia"1588show_edit_buttons(@julia_edit_buttons, name)1589else if name == "cython" # doesn't work yet, since name=python still1590show_edit_buttons(@cython_buttons, name)1591else if name == "python" # doesn't work yet, since name=python still1592show_edit_buttons(@codeedit_buttons, "sage")1593else if name == "shell"1594show_edit_buttons(@sh_edit_buttons, name)1595else1596show_edit_buttons(@fallback_buttons, name)15971598for cm in @codemirrors()1599cm.on('cursorActivity', _.debounce(update_context_sensitive_bar, 250))16001601update_context_sensitive_bar()1602@element.find(".webapp-editor-codemirror-textedit-buttons").katex({preProcess:true})160316041605exports.codemirror_editor = (project_id, filename, extra_opts) ->1606return new CodeMirrorEditor(project_id, filename, "", extra_opts)16071608codemirror_session_editor = exports.codemirror_session_editor = (project_id, filename, extra_opts) ->1609#console.log("codemirror_session_editor '#{filename}'")1610ext = filename_extension_notilde(filename).toLowerCase()16111612E = new CodeMirrorEditor(project_id, filename, "", extra_opts)1613# Enhance the editor with synchronized session capabilities.1614opts =1615cursor_interval : E.opts.cursor_interval1616sync_interval : E.opts.sync_interval16171618switch ext1619when "sagews"1620# temporary.1621opts =1622cursor_interval : 20001623sync_interval : 2501624E.syncdoc = new (sagews.SynchronizedWorksheet)(E, opts)1625E.action_key = E.syncdoc.action1626E.interrupt_key = E.syncdoc.interrupt1627E.tab_nothing_selected = () => E.syncdoc.introspect()1628when "sage-history"1629# no syncdoc1630else1631E.syncdoc = new (syncdoc.SynchronizedDocument2)(E, opts)16321633E.save = E.syncdoc?.save1634return E163516361637class FileEditorWrapper extends FileEditor1638constructor: (project_id, filename, content, opts) ->1639super(project_id, filename)1640@content = content1641@opts = opts1642@init_wrapped(@project_id, @filename, @content, @opts)16431644init_wrapped: () =>1645# Define @element and @wrapped in derived class1646throw Error('must define in derived class')16471648save: (cb) =>1649if @wrapped?.save?1650@wrapped.save(cb)1651else1652cb?()16531654has_unsaved_changes: (val) =>1655return @wrapped?.has_unsaved_changes?(val)16561657has_uncommitted_changes: (val) =>1658return @wrapped?.has_uncommitted_changes?(val)16591660_get: () =>1661# FUTURE1662return 'history saving not yet implemented'16631664_set: (content) =>1665# FUTURE ???16661667focus: () =>16681669terminate_session: () =>16701671disconnect_from_session: () =>1672@wrapped?.destroy?()16731674remove: () =>1675super()1676@wrapped?.destroy?()1677delete @filename; delete @content; delete @opts16781679show: () =>1680if not @is_active()1681return1682if not @element?1683return1684@element.show()16851686if IS_MOBILE1687@element.css(position:'relative')16881689@wrapped?.show?()16901691hide: () =>1692@element?.hide()1693@wrapped?.hide?()1694169516961697exports.register_nonreact_editors = ->16981699# Make non-react editors available in react rewrite1700reg = require('./editors/react-wrapper').register_nonreact_editor17011702# wrapper for registering private and public editors1703exports.register = register = (is_public, cls, extensions) ->1704icon = file_icon_class(extensions[0])1705reg1706ext : extensions1707is_public : is_public1708icon : icon1709f : (project_id, path, opts) ->1710e = new cls(project_id, path, undefined, opts)1711if not e.ext?1712console.error('You have to call super(@project_id, @filename) in the constructor to properly initialize this FileEditor instance.')1713return e17141715# Editing Sage worksheets1716reg1717ext : 'sagews'1718f : (project_id, path, opts) -> codemirror_session_editor(project_id, path, opts)1719is_public : false1720172117221723# See https://github.com/sagemathinc/cocalc/issues/35381724cm_refresh = (cm) ->1725if not cm?1726return1727try1728cm.refresh()1729catch err1730console.warn("cm refresh err", err)1731173217331734