Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/editor.coffee
Views: 687
#########################################################################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# Ensure the console jquery plugin is available52require('./console')5354# SMELL: undo doing the import below -- just use misc.[stuff] is more readable.55{copy, trunc, from_json, to_json, keys, defaults, required, filename_extension, filename_extension_notilde,56len, path_split, uuid} = require('@cocalc/util/misc')5758syncdoc = require('./syncdoc')59sagews = require('./sagews/sagews')60printing = require('./printing')6162{file_nonzero_size} = require('./project/utils')6364copypaste = require('./copy-paste-buffer')6566extra_alt_keys = (extraKeys, editor, opts) ->67misc.merge extraKeys,68"Shift-Alt-L" : (cm) => cm.align_assignments()69'Alt-Z' : (cm) => cm.undo()70'Shift-Alt-Z' : (cm) => cm.redo()71'Alt-A' : (cm) => cm.execCommand('selectAll')72'Shift-Alt-A' : (cm) => cm.execCommand('selectAll')73'Alt-K' : (cm) => cm.execCommand('killLine')74'Alt-D' : (cm) => cm.execCommand('selectNextOccurrence')75'Alt-F' : (cm) => cm.execCommand('find')76'Shift-Alt-F' : (cm) => cm.execCommand('replace')77'Shift-Alt-R' : (cm) => cm.execCommand('replaceAll')78'Shift-Alt-D' : (cm) => cm.execCommand('duplicateLine')79'Alt-G' : (cm) => cm.execCommand('findNext')80'Shift-Alt-G' : (cm) => cm.execCommand('findPrev')81'Alt-Up' : (cm) => cm.execCommand('goPageUp')82'Alt-Down' : (cm) => cm.execCommand('goPageDown')83'Alt-K' : (cm) => cm.execCommand('goPageUp')84'Alt-J' : (cm) => cm.execCommand('goPageDown')85'Alt-P' : (cm) => cm.execCommand('goLineUp')86'Alt-N' : (cm) => cm.execCommand('goLineDown')8788if editor?.goto_line?89extraKeys['Alt-L'] = (cm) => editor.goto_line(cm)90if editor?.toggle_split_view?91extraKeys['Alt-I'] = (cm) => editor.toggle_split_view(cm)92if editor?.copy?93extraKeys['Alt-C'] = (cm) => editor.copy(cm) # gets overwritten for vim mode, of course94else95extraKeys['Alt-C'] = (cm) => copypaste.set_buffer(cm.getSelection())9697if editor?.cut?98extraKeys['Alt-X'] = (cm) => editor.cut(cm)99else100extraKeys['Alt-X'] = (cm) =>101copypaste.set_buffer(cm.getSelection())102cm.replaceSelection('')103if editor?.paste?104extraKeys['Alt-V'] = (cm) => editor.paste(cm)105else106extraKeys['Alt-V'] = (cm) => cm.replaceSelection(copypaste.get_buffer())107if editor?.click_save_button?108extraKeys['Alt-S'] = (cm) => editor.click_save_button()109else if editor?.save?110extraKeys['Alt-S'] = (cm) => editor.save()111112if opts.bindings == 'vim'113# An additional key to get to visual mode in vim (added for ipad Smart Keyboard)114extraKeys["Alt-C"] = (cm) =>115CodeMirror.Vim.exitInsertMode(cm)116extraKeys["Alt-F"] = (cm) =>117cm.execCommand('goPageDown')118extraKeys["Alt-B"] = (cm) =>119cm.execCommand('goPageUp')120121122{file_associations, VIDEO_EXTS} = require('./file-associations')123124file_nonzero_size_cb = (project_id, path, cb) =>125try126if not await file_nonzero_size(project_id, path)127cb("Unable to convert file to PDF")128else129cb()130catch err131cb(err)132133134initialize_new_file_type_list = () ->135file_types_so_far = {}136v = misc.keys(file_associations)137v.sort()138f = (elt, ext, exclude) ->139if not ext140return141data = file_associations[ext]142if exclude and data.exclude_from_menu143return144if data.name? and not file_types_so_far[data.name]145file_types_so_far[data.name] = true146e = $("<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>")147elt.append(e)148149elt = $(".smc-new-file-type-list")150for ext in v151f(elt, ext, true)152153elt = $(".smc-mini-new-file-type-list")154file_types_so_far = {}155for ext in ['sagews', 'term', 'ipynb', 'tex', 'md', 'tasks', 'course', 'sage', 'py']156f(elt, ext)157elt.append($("<li class='divider'></li><li><a href='#new-folder'><i style='width: 18px;' class='fa fa-folder'></i> <span>Folder </span></a></li>"))158159elt.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>"))160161initialize_new_file_type_list()162163exports.file_icon_class = file_icon_class = (ext) ->164assoc = exports.file_options('x.' + ext)165return assoc.icon166167# This defines a bunch of custom modes and gets some info about special case of sagews168{sagews_decorator_modes} = require('./codemirror/custom-modes')169170exports.file_options = require("./editor-tmp").file_options171172# old "local storage" code was here, now moved to TS173editor_local_storage = require('./editor-local-storage')174exports.local_storage_delete = editor_local_storage.local_storage_delete175exports.local_storage = editor_local_storage.local_storage176local_storage = exports.local_storage # used below177178###############################################179# Abstract base class for editors (not exports.Editor)180###############################################181# Derived classes must:182# (1) implement the _get and _set methods183# (2) show/hide/remove184#185# Events ensure that *all* users editor the same file see the same186# thing (synchronized).187#188189class FileEditor extends EventEmitter190# ATTN it is crucial to call this constructor in subclasses via super(@project_id, @filename)191constructor: (project_id, filename) ->192super()193@project_id = project_id194@filename = filename195@ext = misc.filename_extension_notilde(@filename)?.toLowerCase()196@_show = underscore.debounce(@_show, 50)197198is_active: () =>199misc.tab_to_path(redux.getProjectStore(@project_id).get('active_project_tab')) == @filename200201# call it, to set the @default_font_size from the account settings202init_font_size: () =>203@default_font_size = redux.getStore('account').get('font_size')204205val: (content) =>206if not content?207# If content not defined, returns current value.208return @_get()209else210# If content is defined, sets value.211@_set(content)212213# has_unsaved_changes() returns the state, where true means that214# there are unsaved changed. To set the state, do215# has_unsaved_changes(true or false).216has_unsaved_changes: (val) =>217if not val?218return @_has_unsaved_changes219else220if not @_has_unsaved_changes? or @_has_unsaved_changes != val221if val222@save_button.removeClass('disabled')223else224@_when_had_no_unsaved_changes = new Date() # when we last knew for a fact there are no unsaved changes225@save_button.addClass('disabled')226@_has_unsaved_changes = val227228# committed means "not saved to the database/server", whereas save above229# means "saved to *disk*".230has_uncommitted_changes: (val) =>231if not val?232return @_has_uncommitted_changes233else234@_has_uncommitted_changes = val235if val236if not @_show_uncommitted_warning_timeout?237# We have not already started a timer, so start one -- if we do not hear otherwise, show238# the warning in 30s.239@_show_uncommitted_warning_timeout = setTimeout((()=>@_show_uncommitted_warning()), 30000)240else241if @_show_uncommitted_warning_timeout?242clearTimeout(@_show_uncommitted_warning_timeout)243delete @_show_uncommitted_warning_timeout244@uncommitted_element?.hide()245246_show_uncommitted_warning: () =>247delete @_show_uncommitted_warning_timeout248@uncommitted_element?.show()249250focus: () => # FUTURE in derived class (???)251252_get: () =>253console.warn("Incomplete: editor -- needs to implement _get in derived class")254255_set: (content) =>256console.warn("Incomplete: editor -- needs to implement _set in derived class")257258restore_cursor_position: () =>259# implement in a derived class if you need this260261disconnect_from_session: (cb) =>262# implement in a derived class if you need this263264local_storage: (key, value) =>265return local_storage(@project_id, @filename, key, value)266267show: (opts) =>268if not opts?269if @_last_show_opts?270opts = @_last_show_opts271else272opts = {}273@_last_show_opts = opts274275# only re-render the editor if it is active. that's crucial, because e.g. the autosave276# of latex triggers a build, which in turn calls @show to update itself. that would cause277# the latex editor to be visible despite not being the active editor.278if not @is_active?()279return280281@element.show()282# if above line reveals it, give it a bit time to do the layout first283@_show(opts) # critical -- also do an initial layout! Otherwise get a horrible messed up animation effect.284setTimeout((=> @_show(opts)), 10)285if DEBUG286window?.smc?.doc = @ # useful for debugging...287288_show: (opts={}) =>289# define in derived class290291hide: () =>292#@element?.hide()293294remove: () =>295@syncdoc?.close()296@element?.remove()297@removeAllListeners()298299terminate_session: () =>300# If some backend session on a remote machine is serving this session, terminate it.301302exports.FileEditor = FileEditor303304###############################################305# Codemirror-based File Editor306307# - 'saved' : when the file is successfully saved by the user308# - 'show' :309# - 'toggle-split-view' :310###############################################311class CodeMirrorEditor extends FileEditor312constructor: (project_id, filename, content, opts) ->313super(project_id, filename)314315editor_settings = redux.getStore('account').get_editor_settings()316opts = @opts = defaults opts,317mode : undefined318geometry : undefined # (default=full screen);319read_only : false320delete_trailing_whitespace: editor_settings.strip_trailing_whitespace # delete on save321show_trailing_whitespace : editor_settings.show_trailing_whitespace322allow_javascript_eval : true # if false, the one use of eval isn't allowed.323line_numbers : editor_settings.line_numbers324first_line_number : editor_settings.first_line_number325indent_unit : editor_settings.indent_unit326tab_size : editor_settings.tab_size327smart_indent : editor_settings.smart_indent328electric_chars : editor_settings.electric_chars329undo_depth : editor_settings.undo_depth # no longer relevant, since done via sync system330match_brackets : editor_settings.match_brackets331code_folding : editor_settings.code_folding332auto_close_brackets : editor_settings.auto_close_brackets333match_xml_tags : editor_settings.match_xml_tags334auto_close_xml_tags : editor_settings.auto_close_xml_tags335line_wrapping : editor_settings.line_wrapping336spaces_instead_of_tabs : editor_settings.spaces_instead_of_tabs337style_active_line : 15 # editor_settings.style_active_line # (a number between 0 and 127)338bindings : editor_settings.bindings # 'standard', 'vim', or 'emacs'339theme : editor_settings.theme340track_revisions : editor_settings.track_revisions341public_access : false342latex_editor : false343344# I'm making the times below very small for now. If we have to adjust these to reduce load, due to lack345# of capacity, then we will. Or, due to lack of optimization (e.g., for big documents). These parameters346# below would break editing a huge file right now, due to slowness of applying a patch to a codemirror editor.347348cursor_interval : 1000 # minimum time (in ms) between sending cursor position info to hub -- used in sync version349sync_interval : 500 # minimum time (in ms) between synchronizing text with hub. -- used in sync version below350351completions_size : 20 # for tab completions (when applicable, e.g., for sage sessions)352353#console.log("mode =", opts.mode)354355@element = templates.find(".webapp-editor-codemirror").clone()356357@element.data('editor', @)358359@init_save_button()360@init_uncommitted_element()361@init_history_button()362@init_edit_buttons()363364@init_file_actions()365366filename = @filename367if filename.length > 30368filename = "…" + filename.slice(filename.length-30)369370# not really needed due to highlighted tab; annoying.371#@element.find(".webapp-editor-codemirror-filename").text(filename)372373@show_exec_warning = redux.getStore('account').getIn(['editor_settings', 'show_exec_warning']) ? true374if @show_exec_warning and @ext in ['py', 'r', 'sage', 'f90']375msg = "<strong>INFO:</strong> you can only run <code>*.#{@ext}</code> files in a terminal or create a worksheet/notebook. <a href='#'>Close</a>"376msg_el = @element.find('.webapp-editor-codemirror-message')377msg_el.html(msg)378msg_el.find('a').click ->379msg_el.hide()380redux.getTable('account').set(editor_settings:{show_exec_warning:false})381382@_video_is_on = @local_storage("video_is_on")383if not @_video_is_on?384@_video_is_on = false385386extraKeys =387"Alt-Enter" : (editor) => @action_key(execute: true, advance:false, split:false)388"Cmd-Enter" : (editor) => @action_key(execute: true, advance:false, split:false)389"Ctrl-Enter" : (editor) => @action_key(execute: true, advance:true, split:true)390"Ctrl-;" : (editor) => @action_key(split:true, execute:false, advance:false)391"Cmd-;" : (editor) => @action_key(split:true, execute:false, advance:false)392"Ctrl-\\" : (editor) => @action_key(execute:false, toggle_input:true)393#"Cmd-x" : (editor) => @action_key(execute:false, toggle_input:true)394"Shift-Ctrl-\\" : (editor) => @action_key(execute:false, toggle_output:true)395#"Shift-Cmd-y" : (editor) => @action_key(execute:false, toggle_output:true)396397"Cmd-S" : (editor) => @click_save_button()398"Alt-S" : (editor) => @click_save_button()399400"Ctrl-L" : (editor) => @goto_line(editor)401"Cmd-L" : (editor) => @goto_line(editor)402403"Shift-Ctrl-I" : (editor) => @toggle_split_view(editor)404"Shift-Cmd-I" : (editor) => @toggle_split_view(editor)405406"Shift-Cmd-L" : (editor) => editor.align_assignments()407"Shift-Ctrl-L" : (editor) => editor.align_assignments()408409"Shift-Ctrl-." : (editor) => @change_font_size(editor, +1)410"Shift-Ctrl-," : (editor) => @change_font_size(editor, -1)411412"Shift-Cmd-." : (editor) => @change_font_size(editor, +1)413"Shift-Cmd-," : (editor) => @change_font_size(editor, -1)414415"Shift-Tab" : (editor) => editor.unindent_selection()416417"Ctrl-'" : "indentAuto"418"Cmd-'" : "indentAuto"419420"Cmd-/" : "toggleComment"421"Ctrl-/" : "toggleComment" # shortcut chosen by jupyter project (undocumented)422423"Tab" : (editor) => @press_tab_key(editor)424"Shift-Ctrl-C" : (editor) => @interrupt_key()425426"Ctrl-Space" : "autocomplete"427"Alt-Space": "autocomplete"428429if feature.IS_TOUCH430# Better more external keyboard friendly shortcuts, motivated by iPad.431extra_alt_keys(extraKeys, @, opts)432433if opts.match_xml_tags434extraKeys['Ctrl-J'] = "toMatchingTag"435436if opts.bindings != 'emacs'437# Emacs uses control s for find.438extraKeys["Ctrl-S"] = (editor) => @click_save_button()439440# FUTURE: We will replace this by a general framework...441if misc.filename_extension_notilde(filename).toLowerCase() == "sagews"442evaluate_key = redux.getStore('account').get('evaluate_key').toLowerCase()443if evaluate_key == "enter"444evaluate_key = "Enter"445else446evaluate_key = "Shift-Enter"447extraKeys[evaluate_key] = (editor) => @action_key(execute: true, advance:true, split:false)448else449extraKeys["Shift-Enter"] = =>450alert_message451type : "error"452message : "You can only evaluate code in a file that ends with the extension 'sagews' or 'ipynb'. Create a Sage Worksheet or Jupyter notebook instead."453454# Layouts:455# 0 - one single editor456# 1 - two editors, one on top of the other457# 2 - two editors, one next to the other458459if IS_MOBILE460@_layout = 0461else462@_layout = @local_storage("layout") ? 0 # WARNING/UGLY: used by syncdoc.coffee and sagews.coffee !463if @_layout not in [0, 1, 2]464# IMPORTANT: If this were anything other than what is listed, the user465# would never be able to open tex files. So it's important that this be valid.466@_layout = 0467@_last_layout = undefined468469if feature.isMobile.Android()470# see https://github.com/sragemathinc/smc/issues/1360471opts.style_active_line = false472473make_editor = (node) =>474options =475firstLineNumber : opts.first_line_number476autofocus : false477mode : {name:opts.mode, globalVars: true}478lineNumbers : opts.line_numbers479showTrailingSpace : opts.show_trailing_whitespace480indentUnit : opts.indent_unit481tabSize : opts.tab_size482smartIndent : opts.smart_indent483electricChars : opts.electric_chars484undoDepth : opts.undo_depth485matchBrackets : opts.match_brackets486autoCloseBrackets : opts.auto_close_brackets and (misc.filename_extension_notilde(filename) not in ['hs', 'lhs']) #972487autoCloseTags : opts.auto_close_xml_tags488lineWrapping : opts.line_wrapping489readOnly : opts.read_only490styleActiveLine : opts.style_active_line491indentWithTabs : not opts.spaces_instead_of_tabs492showCursorWhenSelecting : true493extraKeys : extraKeys494cursorScrollMargin : 6495viewportMargin : Infinity # larger than the default of 10 specifically so *sage worksheets* (which are the only thing that uses this)496# don't feel jumpy when re-rendering output.497# NOTE that in cocalc right now, no remaining non-sagews editors use this code.498# with images, even using a viewport of 300 causes major jumpiness problems with images.499# See https://github.com/sagemathinc/cocalc/issues/7654500# Browser caching may have changed in newer browsers as well making 300 feel jumpy in "modern times".501502if opts.match_xml_tags503options.matchTags = {bothTags: true}504505if opts.code_folding506extraKeys["Ctrl-Q"] = (cm) -> cm.foldCodeSelectionAware()507extraKeys["Alt-Q"] = (cm) -> cm.foldCodeSelectionAware()508options.foldGutter = true509options.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"]510511if opts.latex_editor512options.gutters ?= []513options.gutters.push("Codemirror-latex-errors")514515if opts.bindings? and opts.bindings != "standard"516options.keyMap = opts.bindings517#cursorBlinkRate: 1000518519if opts.theme? and opts.theme != "standard"520options.theme = opts.theme521522cm = CodeMirror.fromTextArea(node, options)523cm.save = () => @click_save_button()524525# The Codemirror themes impose their own weird fonts, but most users want whatever526# they've configured as "monospace" in their browser. So we force that back:527e = $(cm.getWrapperElement())528e.attr('style', e.attr('style') + '; height:100%; font-family:monospace !important;')529# see http://stackoverflow.com/questions/2655925/apply-important-css-style-using-jquery530531if opts.bindings == 'vim'532# annoying due to api change in vim mode533cm.setOption("vimMode", true)534535return cm536537elt = @element.find(".webapp-editor-textarea-0"); elt.text(content)538539@codemirror = make_editor(elt[0])540@codemirror.name = '0'541#window.cm = @codemirror542543elt1 = @element.find(".webapp-editor-textarea-1")544545@codemirror1 = make_editor(elt1[0])546@codemirror1.name = '1'547548buf = @codemirror.linkedDoc({sharedHist: true})549@codemirror1.swapDoc(buf)550551@codemirror.on 'focus', () =>552@codemirror_with_last_focus = @codemirror553554@codemirror1.on 'focus', () =>555@codemirror_with_last_focus = @codemirror1556557if @opts.bindings == 'vim'558@_vim_mode = 'visual'559@codemirror.on 'vim-mode-change', (obj) =>560if obj.mode == 'normal'561@_vim_mode = 'visual'562@element.find("a[href='#vim-mode-toggle']").text('esc')563else564@_vim_mode = 'insert'565@element.find("a[href='#vim-mode-toggle']").text('i')566567if feature.IS_TOUCH568# ugly hack so more usable on touch...569@element.find(".webapp-editor-resize-bar-layout-1").height('12px')570@element.find(".webapp-editor-resize-bar-layout-2").width('12px')571572@init_font_size() # get the @default_font_size573@restore_font_size()574575@init_draggable_splits()576577if opts.read_only578@set_readonly_ui()579580if misc.filename_extension(@filename)?.toLowerCase() == 'sagews'581@init_sagews_edit_buttons()582583@snippets_dialog = null584# Render all icons using React.585@element.processIcons()586587programmatical_goto_line: (line) =>588cm = @codemirror_with_last_focus589return if not cm?590pos = {line:line-1, ch:0}591info = cm.getScrollInfo()592cm.scrollIntoView(pos, info.clientHeight/2)593594get_users_cursors: (account_id) =>595return @syncdoc?.get_users_cursors(account_id)596597init_file_actions: () =>598if not @element?599return600dom_node = @element.find('.smc-editor-file-info-dropdown')[0]601require('./editors/file-info-dropdown').render_file_info_dropdown(@filename, @project_id, dom_node, @opts.public_access)602603init_draggable_splits: () =>604@_layout1_split_pos = @local_storage("layout1_split_pos")605@_layout2_split_pos = @local_storage("layout2_split_pos")606607layout1_bar = @element.find(".webapp-editor-resize-bar-layout-1")608layout1_bar.draggable609axis : 'y'610containment : @element611zIndex : 10612start : drag_start_iframe_disable613stop : (event, ui) =>614drag_stop_iframe_enable()615# compute the position of bar as a number from 0 to 1, with616# 0 being at top (left), 1 at bottom (right), and .5 right in the middle617e = @element.find(".webapp-editor-codemirror-input-container-layout-1")618top = e.offset().top619ht = e.height()620p = layout1_bar.offset().top + layout1_bar.height()/2621@_layout1_split_pos = (p - top) / ht622@local_storage("layout1_split_pos", @_layout1_split_pos)623# redraw, which uses split info624@show()625626layout2_bar = @element.find(".webapp-editor-resize-bar-layout-2")627layout2_bar.draggable628axis : 'x'629containment : @element630zIndex : 100631start : drag_start_iframe_disable632stop : (event, ui) =>633drag_stop_iframe_enable()634# compute the position of bar as a number from 0 to 1, with635# 0 being at top (left), 1 at bottom (right), and .5 right in the middle636e = @element.find(".webapp-editor-codemirror-input-container-layout-2")637left = e.offset().left638width = e.width()639p = layout2_bar.offset().left640@_layout2_split_pos = (p - left) / width641@local_storage("layout2_split_pos", @_layout2_split_pos)642# redraw, which uses split info643@show()644645hide_content: () =>646@element.find(".webapp-editor-codemirror-content").hide()647648show_content: () =>649@hide_startup_message()650@element.find(".webapp-editor-codemirror-content").show()651for cm in @codemirrors()652cm_refresh(cm)653654hide_startup_message: () =>655@element.find(".webapp-editor-codemirror-startup-message").hide()656657show_startup_message: (mesg, type='info') =>658@hide_content()659if typeof(mesg) != 'string'660mesg = JSON.stringify(mesg)661e = @element.find(".webapp-editor-codemirror-startup-message").show().text(mesg)662for t in ['success', 'info', 'warning', 'danger']663e.removeClass("alert-#{t}")664e.addClass("alert-#{type}")665666is_active: () =>667return @codemirror? and misc.tab_to_path(redux.getProjectStore(@project_id).get('active_project_tab')) == @filename668669set_theme: (theme) =>670# Change the editor theme after the editor has been created671for cm in @codemirrors()672cm.setOption('theme', theme)673@opts.theme = theme674675# add something visual to the UI to suggest that the file is read only676set_readonly_ui: (readonly=true) =>677@opts.read_only = readonly678@element.find(".webapp-editor-write-only").toggle(!readonly)679@element.find(".webapp-editor-read-only").toggle(readonly)680for cm in @codemirrors()681cm.setOption('readOnly', readonly)682683set_cursor_center_focus: (pos, tries=5) =>684if tries <= 0685return686cm = @codemirror_with_last_focus687if not cm?688cm = @codemirror689if not cm?690return691cm.setCursor(pos)692info = cm.getScrollInfo()693try694# This call can fail during editor initialization (as of codemirror 3.19, but not before).695cm.scrollIntoView(pos, info.clientHeight/2)696catch e697setTimeout((() => @set_cursor_center_focus(pos, tries-1)), 250)698cm.focus()699700disconnect_from_session: (cb) =>701# implement in a derived class if you need this702@syncdoc?.disconnect_from_session()703cb?()704705codemirrors: () =>706c = [@codemirror, @codemirror1]707return underscore.filter(c, ((x) -> x?))708709focused_codemirror: () =>710if @codemirror_with_last_focus?711return @codemirror_with_last_focus712else713return @codemirror714715action_key: (opts) =>716# opts ignored by default; worksheets use them....717@click_save_button()718719interrupt_key: () =>720# does nothing for generic editor, but important, e.g., for the sage worksheet editor.721722press_tab_key: (editor) =>723if editor.somethingSelected()724CodeMirror.commands.defaultTab(editor)725else726@tab_nothing_selected(editor)727728tab_nothing_selected: (editor) =>729if @opts.spaces_instead_of_tabs730editor.tab_as_space()731else732CodeMirror.commands.defaultTab(editor)733734init_edit_buttons: () =>735that = @736button_names = ['search', 'next', 'prev', 'replace', 'undo', 'redo', 'autoindent',737'shift-left', 'shift-right', 'split-view','increase-font', 'decrease-font', 'goto-line',738'copy', 'paste', 'vim-mode-toggle']739740if @opts.bindings != 'vim'741@element.find("a[href='#vim-mode-toggle']").remove()742743# if the file extension indicates that we know how to print it, show and enable the print button744if printing.can_print(@ext)745button_names.push('print')746else747@element.find('a[href="#print"]').remove()748749# sagews2pdf conversion750if @ext == 'sagews'751button_names.push('sagews2pdf')752button_names.push('sagews2ipynb')753else754@element.find('a[href="#sagews2pdf"]').remove()755@element.find('a[href="#sagews2ipynb"]').remove()756757for name in button_names758e = @element.find("a[href=\"##{name}\"]")759e.data('name', name).tooltip(delay:{ show: 500, hide: 100 }).click (event) ->760that.click_edit_button($(@).data('name'))761return false762763click_edit_button: (name) =>764cm = @codemirror_with_last_focus765if not cm?766cm = @codemirror767if not cm?768return769switch name770when 'search'771CodeMirror.commands.find(cm)772when 'next'773if cm._searchState?.query774CodeMirror.commands.findNext(cm)775else776CodeMirror.commands.goPageDown(cm)777cm.focus()778when 'prev'779if cm._searchState?.query780CodeMirror.commands.findPrev(cm)781else782CodeMirror.commands.goPageUp(cm)783cm.focus()784when 'replace'785CodeMirror.commands.replace(cm)786when 'undo'787cm.undo()788cm.focus()789when 'redo'790cm.redo()791cm.focus()792when 'split-view'793@toggle_split_view(cm)794when 'autoindent'795CodeMirror.commands.indentAuto(cm)796when 'shift-left'797cm.unindent_selection()798cm.focus()799when 'shift-right'800@press_tab_key(cm)801cm.focus()802when 'increase-font'803@change_font_size(cm, +1)804cm.focus()805when 'decrease-font'806@change_font_size(cm, -1)807cm.focus()808when 'goto-line'809@goto_line(cm)810when 'copy'811@copy(cm)812cm.focus()813when 'paste'814@paste(cm)815cm.focus()816when 'sagews2pdf'817@print(sagews2html = false)818when 'sagews2ipynb'819@convert_to_ipynb()820when 'print'821@print(sagews2html = true)822when 'vim-mode-toggle'823if @_vim_mode == 'visual'824CodeMirror.Vim.handleKey(cm, 'i')825else826CodeMirror.Vim.exitInsertMode(cm)827cm.focus()828829restore_font_size: () =>830# we set the font_size from local storage831# or fall back to the default from the account settings832for i, cm of @codemirrors()833size = @local_storage("font_size#{i}")834if size?835@set_font_size(cm, size)836else if @default_font_size?837@set_font_size(cm, @default_font_size)838839get_font_size: (cm) ->840if not cm?841return842elt = $(cm.getWrapperElement())843return elt.data('font-size') ? @default_font_size844845set_font_size: (cm, size) =>846if not cm?847return848if size > 1849elt = $(cm.getWrapperElement())850elt.css('font-size', size + 'px')851elt.data('font-size', size)852853change_font_size: (cm, delta) =>854if not cm?855return856#console.log("change_font_size #{cm.name}, #{delta}")857scroll_before = cm.getScrollInfo()858859elt = $(cm.getWrapperElement())860size = elt.data('font-size')861if not size?862s = elt.css('font-size')863size = parseInt(s.slice(0,s.length-2))864new_size = size + delta865@set_font_size(cm, new_size)866@local_storage("font_size#{cm.name}", new_size)867868# we have to do the scrollTo in the next render loop, since otherwise869# the getScrollInfo function below will return the sizing data about870# the cm instance before the above css font-size change has been rendered.871f = () =>872cm_refresh(cm)873scroll_after = cm.getScrollInfo()874x = (scroll_before.left / scroll_before.width) * scroll_after.width875y = (((scroll_before.top+scroll_before.clientHeight/2) / scroll_before.height) * scroll_after.height) - scroll_after.clientHeight/2876cm.scrollTo(x, y)877setTimeout(f, 0)878879toggle_split_view: (cm) =>880if not cm?881return882@_layout = (@_layout + 1) % 3883@local_storage("layout", @_layout)884@show()885if cm? and not feature.IS_TOUCH886if @_layout > 0887cm.focus()888else889# focus first editor since it is only one that is visible.890@codemirror.focus()891f = () =>892for x in @codemirrors()893x.scrollIntoView() # scroll the cursors back into view -- see https://github.com/sagemathinc/cocalc/issues/1044894setTimeout(f, 1) # wait until next loop after codemirror has laid itself out.895@emit 'toggle-split-view'896897goto_line: (cm) =>898if not cm?899return900focus = () =>901@focus()902cm.focus()903dialog = templates.find(".webapp-goto-line-dialog").clone()904dialog.modal('show')905dialog.find(".btn-close").off('click').click () ->906dialog.modal('hide')907setTimeout(focus, 50)908return false909input = dialog.find(".webapp-goto-line-input")910input.val(cm.getCursor().line+1) # +1 since line is 0-based911dialog.find(".webapp-goto-line-range").text("1-#{cm.lineCount()} or n%")912dialog.find(".webapp-goto-line-input").focus().select()913submit = () =>914dialog.modal('hide')915result = input.val().trim()916if result.length >= 1 and result[result.length-1] == '%'917line = Math.floor( cm.lineCount() * parseInt(result.slice(0,result.length-1)) / 100.0)918else919line = Math.min(parseInt(result)-1)920if line >= cm.lineCount()921line = cm.lineCount() - 1922if line <= 0923line = 0924pos = {line:line, ch:0}925cm.setCursor(pos)926info = cm.getScrollInfo()927cm.scrollIntoView(pos, info.clientHeight/2)928setTimeout(focus, 50)929dialog.find(".btn-submit").off('click').click(submit)930input.keydown (evt) =>931if evt.which == 13 # enter932submit()933return false934if evt.which == 27 # escape935setTimeout(focus, 50)936dialog.modal('hide')937return false938939copy: (cm) =>940if not cm?941return942copypaste.set_buffer(cm.getSelection())943944convert_to_ipynb: () =>945p = misc.path_split(@filename)946v = p.tail.split('.')947if v.length <= 1948ext = ''949base = p.tail950else951ext = v[v.length-1]952base = v.slice(0,v.length-1).join('.')953954if ext != 'sagews'955console.error("editor.print called on file with extension '#{ext}' but only supports 'sagews'.")956return957958async.series([959(cb) =>960@save(cb)961(cb) =>962webapp_client.exec963project_id : @project_id964command : "cc-sagews2ipynb"965args : [@filename]966err_on_exit : true967cb : (err, output) =>968if err969alert_message(type:"error", message:"Error occured converting '#{@filename}' -- #{err}")970else971path = base + '.ipynb'972if p.head973path = p.head + '/' + path974redux.getProjectActions(@project_id).open_file975path : path976foreground : true977])978979cut: (cm) =>980if not cm?981return982copypaste.set_buffer(cm.getSelection())983cm.replaceSelection('')984985paste: (cm) =>986if not cm?987return988cm.replaceSelection(copypaste.get_buffer())989990print: (sagews2html = true) =>991switch @ext992when 'sagews'993if sagews2html994@print_html()995else996@print_sagews()997when 'txt', 'csv'998print_button = @element.find('a[href="#print"]')999print_button.icon_spin(start:true, delay:0).addClass("disabled")1000printing.Printer(@, @filename + '.pdf').print (err) ->1001print_button.removeClass('disabled')1002print_button.icon_spin(false)1003if err1004alert_message1005type : "error"1006message : "Printing error -- #{err}"10071008print_html: =>1009dialog = null1010d_content = null1011d_open = null1012d_download = null1013d_progress = _.noop1014output_fn = null # set this before showing the dialog10151016show_dialog = (cb) =>1017# this creates the dialog element and defines the action functions like d_progress1018dialog = $("""1019<div class="modal" tabindex="-1" role="dialog">1020<div class="modal-dialog" role="document">1021<div class="modal-content">1022<div class="modal-header">1023<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>1024<h4 class="modal-title">Print to HTML</h4>1025</div>1026<div class="modal-body">1027<div class="progress">1028<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">10290 %1030</div>1031</div>1032<div class="content" style="text-align: center;"></div>1033<div style="margin-top: 25px;">1034<p><b>More information</b></p>1035<p>1036This SageWS to HTML conversion transforms the current worksheet1037to a static HTML file.1038<br/>1039<a href="https://github.com/sagemathinc/cocalc/wiki/sagews2html" target='_blank'>Click here for more information</a>.1040</p>1041</div>1042</div>1043<div class="modal-footer">1044<button type="button" class="btn-download btn btn-primary disabled">Download</button>1045<button type="button" class="btn-open btn btn-success disabled">Open</button>1046<button type="button" class="btn-close btn btn-default" data-dismiss="modal">Close</button>1047</div>1048</div>1049</div>1050</div>1051""")1052d_content = dialog.find('.content')1053d_open = dialog.find('.btn-open')1054d_download = dialog.find('.btn-download')1055action = redux.getProjectActions(@project_id)1056d_progress = (p) ->1057pct = "#{Math.round(100 * p)}%"1058dialog.find(".progress-bar").css('width', pct).text(pct)1059dialog.find('.btn-close').click ->1060dialog.modal('hide')1061return false1062d_open.click =>1063action.download_file1064path : output_fn1065auto : false # open in new tab1066d_download.click =>1067action.download_file1068path : output_fn1069auto : true1070dialog.modal('show')1071cb()10721073convert = (cb) =>1074# initiates the actual conversion via printing.Printer ...1075switch @ext1076when 'sagews'1077output_fn = @filename + '.html'1078progress = (percent, mesg) =>1079d_content.text(mesg)1080d_progress(percent)1081progress = _.debounce(progress, 5)1082progress(.01, "Loading ...")1083done = (err) =>1084#console.log 'Printer.print_html is done: err = ', err1085if err1086progress(0, "Problem printing to HTML: #{err}")1087else1088progress(1, 'Printing finished.')1089# enable open & download buttons1090dialog.find('button.btn').removeClass('disabled')1091printing.Printer(@, output_fn).print(done, progress)1092cb(); return10931094# fallback1095cb("err -- unable to convert files with extension '@ext'")10961097async.series([show_dialog, convert], (err) =>1098if err1099msg = "problem printing -- #{misc.to_json(err)}"1100alert_message1101type : "error"1102message : msg1103dialog.content.text(msg)1104)11051106# WARNING: this "print" is actually for printing Sage worksheets, not arbitrary files.1107print_sagews: =>1108dialog = templates.find(".webapp-file-print-dialog").clone()1109dialog.processIcons()1110p = misc.path_split(@filename)1111v = p.tail.split('.')1112if v.length <= 11113ext = ''1114base = p.tail1115else1116ext = v[v.length-1]1117base = v.slice(0,v.length-1).join('.')11181119ext = ext.toLowerCase()1120if ext != 'sagews'1121console.error("editor.print called on file with extension '#{ext}' but only supports 'sagews'.")1122return11231124submit = () =>1125dialog.find(".webapp-file-printing-progress").show()1126dialog.find(".webapp-file-printing-link").hide()1127$print_tempdir = dialog.find(".smc-file-printing-tempdir")1128$print_tempdir.hide()1129is_subdir = dialog.find(".webapp-file-print-keepfiles").is(":checked")1130dialog.find(".btn-submit").icon_spin(start:true)1131pdf = undefined1132async.series([1133(cb) =>1134@save(cb)1135(cb) =>1136# get info from the UI and attempt to convert the sagews to pdf1137options =1138title : dialog.find(".webapp-file-print-title").text()1139author : dialog.find(".webapp-file-print-author").text()1140date : dialog.find(".webapp-file-print-date").text()1141contents : dialog.find(".webapp-file-print-contents").is(":checked")1142subdir : is_subdir1143base_url : require('./misc').BASE_URL # really is a base url (not base path)1144extra_data : misc.to_json(@syncdoc.print_to_pdf_data()) # avoid de/re-json'ing11451146printing.Printer(@, @filename + '.pdf').print1147project_id : @project_id1148path : @filename1149options : options1150cb : (err, _pdf) =>1151if err and not is_subdir1152cb(err)1153else1154pdf = _pdf1155cb()1156(cb) =>1157file_nonzero_size_cb(@project_id, pdf, cb)1158(cb) =>1159if is_subdir or not pdf?1160cb(); return1161# pdf file exists -- show it in the UI1162url = webapp_client.project_client.read_file1163project_id : @project_id1164path : pdf1165dialog.find(".webapp-file-printing-link").attr('href', url).text(pdf).show()1166cb()1167(cb) =>1168if not is_subdir1169cb(); return1170{join} = require('path')1171subdir_texfile = join(p.head, "#{base}-sagews2pdf", "tmp.tex")1172# check if generated tmp.tex exists and has nonzero size1173file_nonzero_size_cb @project_id, subdir_texfile, (err) =>1174if err1175cb("Unable to create directory of temporary Latex files. -- #{err}")1176return1177tempdir_link = $('<a>').text('Click to open temporary file')1178tempdir_link.click =>1179redux.getProjectActions(@project_id).open_file1180path : subdir_texfile1181foreground : true1182dialog.modal('hide')1183return false1184$print_tempdir.html(tempdir_link)1185$print_tempdir.show()1186cb()1187(cb) =>1188# if there is no subdirectory of temporary files, print generated pdf file1189if not is_subdir1190redux.getProjectActions(@project_id).print_file(path: pdf)1191cb()1192], (err) =>1193dialog.find(".btn-submit").icon_spin(false)1194dialog.find(".webapp-file-printing-progress").hide()1195if err1196alert_message(type:"error", message:"problem printing '#{p.tail}' -- #{misc.to_json(err)}")1197)1198return false11991200dialog.find(".webapp-file-print-filename").text(@filename)1201dialog.find(".webapp-file-print-title").text(base)1202dialog.find(".webapp-file-print-author").text(redux.getStore('account').get_fullname())1203dialog.find(".webapp-file-print-date").text((new Date()).toLocaleDateString())1204dialog.find(".btn-submit").click(submit)1205dialog.find(".btn-close").click(() -> dialog.modal('hide'); return false)1206if ext == "sagews"1207dialog.find(".webapp-file-options-sagews").show()1208dialog.modal('show')12091210init_save_button: () =>1211@save_button = @element.find("a[href=\"#save\"]").tooltip().click(@click_save_button)1212@save_button.find(".spinner").hide()12131214init_uncommitted_element: () =>1215@uncommitted_element = @element.find(".smc-uncommitted")12161217init_history_button: () =>1218if not @opts.public_access and @filename.slice(@filename.length-13) != '.sage-history'1219@history_button = @element.find(".webapp-editor-history-button")1220@history_button.click(@click_history_button)1221@history_button.show()1222@history_button.css1223display: 'inline-block' # this is needed due to subtleties of jQuery show().12241225click_save_button: () =>1226if @opts.read_only1227return1228if not @save? # not implemented...1229return1230if @_saving1231return1232@_saving = true1233@syncdoc?.delete_trailing_whitespace?() # only delete trailing whitespace on explicit save -- never on AUTOSAVE.1234@save_button.icon_spin(start:true, delay:8000)1235@save (err) =>1236# WARNING: As far as I can tell, this doesn't call FileEditor.save1237if err1238if redux.getProjectStore(@project_id).is_file_open(@filename) # only show error if file actually opened1239alert_message(type:"error", message:"Error saving '#{@filename}' (#{err}) -- (you might need to close and open this file or restart this project)")1240else1241@emit('saved')1242@save_button.icon_spin(false)1243@_saving = false1244return false12451246click_history_button: () =>1247redux.getProjectActions(@project_id).open_file1248path : misc.history_path(@filename)1249foreground : true12501251_get: () =>1252return @codemirror?.getValue()12531254_set: (content) =>1255if not @codemirror?1256# document is already closed and freed up.1257return1258{from} = @codemirror.getViewport()1259@codemirror.setValue(content)1260@codemirror.scrollIntoView(from)1261# even better -- fully restore cursors, if available in localStorage1262setTimeout((()=>@restore_cursor_position()),1) # do in next round, so that both editors get set by codemirror first (including the linked one)12631264# save/restore view state -- hooks used by React editor wrapper.1265save_view_state: =>1266state =1267scroll : (cm.getScrollInfo() for cm in @codemirrors())1268@_view_state = state1269return state12701271restore_view_state: (second_try) =>1272state = @_view_state1273if not state?1274return1275cms = @codemirrors()1276i = 01277for v in state.scroll1278cm = cms[i]1279if cm?1280cm.scrollTo(v.left, v.top)1281info = cm.getScrollInfo()1282# THIS IS HORRIBLE and SUCKS, but I can't understand what is going on sufficiently1283# well to remove this. Sometimes scrollTo fails (due to the document being reported as much1284# smaller than it is for a few ms) **and** it's then not possible to scroll,1285# so we just try again. See https://github.com/sagemathinc/cocalc/issues/13271286if not second_try and info.top != v.top1287# didn't work -- not fully visible; try again one time when rendering is presumably done.1288setTimeout((=>@restore_view_state(true)), 250)1289i += 112901291restore_cursor_position: () =>1292for i, cm of @codemirrors()1293if cm?1294pos = @local_storage("cursor#{cm.name}")1295if pos?1296cm.setCursor(pos)1297#console.log("#{@filename}: setting view #{cm.name} to cursor pos -- #{misc.to_json(pos)}")1298info = cm.getScrollInfo()1299try1300cm.scrollIntoView(pos, info.clientHeight/2)1301catch e1302#console.log("#{@filename}: failed to scroll view #{cm.name} into view -- #{e}")1303@codemirror?.focus()13041305# set background color of active line in editor based on background color (which depends on the theme)1306_style_active_line: () =>1307if not @opts.style_active_line1308return1309rgb = $(@codemirror.getWrapperElement()).css('background-color')1310v = (parseInt(x) for x in rgb.slice(4,rgb.length-1).split(','))1311amount = @opts.style_active_line1312for i in [0..2]1313if v[i] >= 1281314v[i] -= amount1315else1316v[i] += amount1317$("body").remove("#webapp-cm-activeline")1318$("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!13191320_show_codemirror_editors: (height) =>1321# console.log("_show_codemirror_editors: #{@_layout}")1322if not @codemirror?1323# already closed so can't show (in syncdoc, .codemirorr is deleted on close)1324return1325switch @_layout1326when 01327p = 11328when 11329p = @_layout1_split_pos ? 0.51330when 21331p = @_layout2_split_pos ? 0.513321333# Change the height of the *top* div that contain the editors; the bottom one then1334# uses of all remaining vertical height.1335if @_layout > 01336p = Math.max(MIN_SPLIT, Math.min(MAX_SPLIT, p))13371338# We set only the default size of the *first* div -- everything else expands accordingly.1339elt = @element.find(".webapp-editor-codemirror-input-container-layout-#{@_layout}").show()13401341if @_layout == 11342@element.find(".webapp-editor-resize-bar-layout-1").css(top:0)1343else if @_layout == 21344@element.find(".webapp-editor-resize-bar-layout-2").css(left:0)13451346c = elt.find(".webapp-editor-codemirror-input-box")1347if @_layout == 01348c.css('flex', 1) # use the full vertical height1349else1350c.css('flex-basis', "#{p*100}%")13511352if @_last_layout != @_layout1353# The layout has changed1354btn = @element.find('a[href="#split-view"]')13551356if @_last_layout?1357# Hide previous1358btn.find(".webapp-editor-layout-#{@_last_layout}").hide()1359@element.find(".webapp-editor-codemirror-input-container-layout-#{@_last_layout}").hide()13601361# Show current1362btn.find(".webapp-editor-layout-#{@_layout}").show()13631364# Put editors in their place -- in the div inside of each box1365elt.find(".webapp-editor-codemirror-input-box div").empty().append($(@codemirror.getWrapperElement()))1366elt.find(".webapp-editor-codemirror-input-box-1 div").empty().append($(@codemirror1.getWrapperElement()))13671368# Save for next time1369@_last_layout = @_layout13701371refresh = (cm) =>1372return if not cm?1373cm_refresh(cm)1374# See https://github.com/sagemathinc/cocalc/issues/1327#issuecomment-2654888721375setTimeout((=>cm_refresh(cm)), 1)13761377for cm in @codemirrors()1378refresh(cm)13791380@emit('show')13811382_show: (opts={}) =>1383# show the element that contains this editor1384#@element.show()1385# show the codemirror editors, resizing as needed1386@_show_codemirror_editors()13871388focus: () =>1389if not @codemirror?1390return1391@show()1392if not (IS_MOBILE or feature.IS_TOUCH)1393@codemirror_with_last_focus?.focus()13941395############1396# Editor button bar support code1397############1398textedit_command: (cm, cmd, args) =>1399# ATTN when adding more cases, also edit textedit_only_show_known_buttons1400switch cmd1401when "link"1402cm.insert_link(cb:() => @syncdoc?.sync())1403return false # don't return true or get an infinite recurse1404when "image"1405cm.insert_image(cb:() => @syncdoc?.sync())1406return false # don't return true or get an infinite recurse1407when "SpecialChar"1408cm.insert_special_char(cb:() => @syncdoc?.sync())1409return false # don't return true or get an infinite recurse1410else1411cm.edit_selection1412cmd : cmd1413args : args1414@syncdoc?.sync()1415# needed so that dropdown menu closes when clicked.1416return true14171418example_insert_handler: (insert) =>1419# insert : {lang: string, descr: string, code: string[]}1420{code, lang} = insert1421cm = @focused_codemirror()1422line = cm.getCursor().line1423# ATTN: to make this work properly, code and descr need to have a newline at the end (stripped by default)1424if insert.descr?1425@syncdoc?.insert_new_cell(line)1426# insert a "hidden" markdown cell and evaluate it1427cm.replaceRange("%md(hide=True)\n#{insert.descr}\n", {line : line+1, ch:0})1428@action_key(execute: true, advance:false, split:false)14291430# inserting one or more code cells1431for c in code1432line = cm.getCursor().line1433# next, we insert the code cell and prefix it with a mode change,1434# iff the mode is different from the current one1435@syncdoc?.insert_new_cell(line)1436cell = "#{c}\n"1437if lang != @_current_mode1438# special case: %sh for bash language1439if lang == 'bash' then lang = 'sh'1440cell = "%#{lang}\n#{cell}"1441cm.replaceRange(cell, {line : line+1, ch:0})1442# and we evaluate and sync all this, too…1443@action_key(execute: true, advance:false, split:false)1444@syncdoc?.sync()14451446# add a textedit toolbar to the editor1447init_sagews_edit_buttons: () =>1448if @opts.read_only # no editing button bar needed for read-only files1449return14501451if IS_MOBILE # no edit button bar on mobile either -- too big (for now at least)1452return14531454if not redux.getStore('account').get_editor_settings().extra_button_bar1455# explicitly disabled by user1456return14571458NAME_TO_MODE = {xml:'html', markdown:'md', mediawiki:'wiki'}1459for x in sagews_decorator_modes1460mode = x[0]1461name = x[1]1462v = name.split('-')1463if v.length > 11464name = v[1]1465NAME_TO_MODE[name] = "#{mode}"14661467name_to_mode = (name) ->1468n = NAME_TO_MODE[name]1469if n?1470return n1471else1472return "#{name}"14731474# add the text editing button bar1475e = @element.find(".webapp-editor-codemirror-textedit-buttons")1476@textedit_buttons = templates.find(".webapp-editor-textedit-buttonbar").clone().hide()1477e.append(@textedit_buttons).show()14781479# add the code editing button bar1480@codeedit_buttons = templates.find(".webapp-editor-codeedit-buttonbar").clone()1481e.append(@codeedit_buttons)14821483# the r-editing button bar1484@redit_buttons = templates.find(".webapp-editor-redit-buttonbar").clone()1485e.append(@redit_buttons)14861487# the Julia-editing button bar1488@julia_edit_buttons = templates.find(".webapp-editor-julia-edit-buttonbar").clone()1489e.append(@julia_edit_buttons)14901491# the sh-editing button bar1492@sh_edit_buttons = templates.find(".webapp-editor-sh-edit-buttonbar").clone()1493e.append(@sh_edit_buttons)14941495@cython_buttons = templates.find(".webapp-editor-cython-buttonbar").clone()1496e.append(@cython_buttons)14971498@fallback_buttons = templates.find(".webapp-editor-fallback-edit-buttonbar").clone()1499e.append(@fallback_buttons)15001501all_edit_buttons = [@textedit_buttons, @codeedit_buttons, @redit_buttons,1502@cython_buttons, @julia_edit_buttons, @sh_edit_buttons, @fallback_buttons]15031504# activite the buttons in the bar1505that = @1506edit_button_click = (e) ->1507e.preventDefault()1508args = $(this).data('args')1509cmd = $(this).attr('href').slice(1)1510if cmd == 'todo'1511return1512if args? and typeof(args) != 'object'1513args = "#{args}"1514if args.indexOf(',') != -11515args = args.split(',')1516return that.textedit_command(that.focused_codemirror(), cmd, args)15171518# FUTURE: activate color editing buttons -- for now just hide them1519@element.find(".sagews-output-editor-foreground-color-selector").hide()1520@element.find(".sagews-output-editor-background-color-selector").hide()15211522@fallback_buttons.find('a[href="#todo"]').click () =>1523bootbox.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.")1524return false15251526for edit_buttons in all_edit_buttons1527edit_buttons.find("a").click(edit_button_click)1528edit_buttons.find("*[title]").tooltip(TOOLTIP_DELAY)15291530@mode_display = mode_display = @element.find(".webapp-editor-codeedit-buttonbar-mode")1531@_current_mode = "sage"1532@mode_display.show()15331534# not all textedit buttons are known1535textedit_only_show_known_buttons = (name) =>1536EDIT_COMMANDS = require('./editors/editor-button-bar').commands1537default_mode = @focused_codemirror()?.get_edit_mode() ? 'sage'1538mode = sagews_canonical_mode(name, default_mode)1539#if DEBUG then console.log "textedit_only_show_known_buttons: mode #{name} → #{mode}"1540known_commands = misc.keys(EDIT_COMMANDS[mode] ? {})1541# see special cases in 'textedit_command' and codemirror/extensions: 'edit_selection'1542known_commands = known_commands.concat(['link', 'image', 'SpecialChar', 'font_size'])1543for button in @textedit_buttons.find('a')1544button = $(button)1545cmd = button.attr('href').slice(1)1546# in theory, this should also be done for html&md, but there are many more special cases1547# therefore we just make sure they're all activated again1548button.toggle((mode != 'tex') or (cmd in known_commands))15491550set_mode_display = (name) =>1551#console.log("set_mode_display: #{name}")1552if name?1553mode = name_to_mode(name)1554else1555mode = ""1556mode_display.text("%" + mode)1557@_current_mode = mode15581559show_edit_buttons = (which_one, name) =>1560for edit_buttons in all_edit_buttons1561edit_buttons.toggle(edit_buttons == which_one)1562if which_one == @textedit_buttons1563textedit_only_show_known_buttons(name)1564set_mode_display(name)15651566# this is deprecated1567@element.find('.webapp-editor-codeedit-buttonbar-assistant').hide()15681569# The code below changes the bar at the top depending on where the cursor1570# is located. We only change the edit bar if the cursor hasn't moved for1571# a while, to be more efficient, avoid noise, and be less annoying to the user.1572# Replaced by http://underscorejs.org/#debounce1573#bar_timeout = undefined1574#f = () =>1575# if bar_timeout?1576# clearTimeout(bar_timeout)1577# bar_timeout = setTimeout(update_context_sensitive_bar, 250)15781579update_context_sensitive_bar = () =>1580cm = @focused_codemirror()1581if not cm?1582return1583pos = cm.getCursor()1584name = cm.getModeAt(pos).name1585#console.log("update_context_sensitive_bar, pos=#{misc.to_json(pos)}, name=#{name}")1586if name in ['xml', 'stex', 'markdown', 'mediawiki']1587show_edit_buttons(@textedit_buttons, name)1588else if name == "r"1589show_edit_buttons(@redit_buttons, name)1590else if name == "julia"1591show_edit_buttons(@julia_edit_buttons, name)1592else if name == "cython" # doesn't work yet, since name=python still1593show_edit_buttons(@cython_buttons, name)1594else if name == "python" # doesn't work yet, since name=python still1595show_edit_buttons(@codeedit_buttons, "sage")1596else if name == "shell"1597show_edit_buttons(@sh_edit_buttons, name)1598else1599show_edit_buttons(@fallback_buttons, name)16001601for cm in @codemirrors()1602cm.on('cursorActivity', _.debounce(update_context_sensitive_bar, 250))16031604update_context_sensitive_bar()1605@element.find(".webapp-editor-codemirror-textedit-buttons").katex({preProcess:true})160616071608exports.codemirror_editor = (project_id, filename, extra_opts) ->1609return new CodeMirrorEditor(project_id, filename, "", extra_opts)16101611codemirror_session_editor = exports.codemirror_session_editor = (project_id, filename, extra_opts) ->1612#console.log("codemirror_session_editor '#{filename}'")1613ext = filename_extension_notilde(filename).toLowerCase()16141615E = new CodeMirrorEditor(project_id, filename, "", extra_opts)1616# Enhance the editor with synchronized session capabilities.1617opts =1618cursor_interval : E.opts.cursor_interval1619sync_interval : E.opts.sync_interval16201621switch ext1622when "sagews"1623# temporary.1624opts =1625cursor_interval : 20001626sync_interval : 2501627E.syncdoc = new (sagews.SynchronizedWorksheet)(E, opts)1628E.action_key = E.syncdoc.action1629E.interrupt_key = E.syncdoc.interrupt1630E.tab_nothing_selected = () => E.syncdoc.introspect()1631when "sage-history"1632# no syncdoc1633else1634E.syncdoc = new (syncdoc.SynchronizedDocument2)(E, opts)16351636E.save = E.syncdoc?.save1637return E16381639class Terminal extends FileEditor1640constructor: (project_id, filename, content, opts) ->1641super(project_id, filename)1642@element = $("<div>").hide()1643elt = @element.webapp_console1644title : "Terminal"1645filename : @filename1646project_id : @project_id1647path : @filename1648editor : @1649@console = elt.data("console")1650@console.is_hidden = true1651@element = @console.element1652@console.blur()16531654_get: => # FUTURE ??1655return @opts.session_uuid ? ''16561657_set: (content) => # FUTURE ??16581659save: =>1660# DO nothing -- a no-op for now1661# FUTURE: Add notion of history1662cb?()16631664focus: =>1665@console?.is_hidden = false1666@console?.focus()16671668blur: =>1669@console?.is_hidden = false1670@console?.blur()16711672terminate_session: () =>16731674remove: =>1675@element.webapp_console(false)1676super()16771678hide: =>1679@console?.is_hidden = true1680@console?.blur()16811682_show: () =>1683@console?.is_hidden = false1684@console?.resize_terminal()1685168616871688class FileEditorWrapper extends FileEditor1689constructor: (project_id, filename, content, opts) ->1690super(project_id, filename)1691@content = content1692@opts = opts1693@init_wrapped(@project_id, @filename, @content, @opts)16941695init_wrapped: () =>1696# Define @element and @wrapped in derived class1697throw Error('must define in derived class')16981699save: (cb) =>1700if @wrapped?.save?1701@wrapped.save(cb)1702else1703cb?()17041705has_unsaved_changes: (val) =>1706return @wrapped?.has_unsaved_changes?(val)17071708has_uncommitted_changes: (val) =>1709return @wrapped?.has_uncommitted_changes?(val)17101711_get: () =>1712# FUTURE1713return 'history saving not yet implemented'17141715_set: (content) =>1716# FUTURE ???17171718focus: () =>17191720terminate_session: () =>17211722disconnect_from_session: () =>1723@wrapped?.destroy?()17241725remove: () =>1726super()1727@wrapped?.destroy?()1728delete @filename; delete @content; delete @opts17291730show: () =>1731if not @is_active()1732return1733if not @element?1734return1735@element.show()17361737if IS_MOBILE1738@element.css(position:'relative')17391740@wrapped?.show?()17411742hide: () =>1743@element?.hide()1744@wrapped?.hide?()1745174617471748exports.register_nonreact_editors = ->17491750# Make non-react editors available in react rewrite1751reg = require('./editors/react-wrapper').register_nonreact_editor17521753# wrapper for registering private and public editors1754exports.register = register = (is_public, cls, extensions) ->1755icon = file_icon_class(extensions[0])1756reg1757ext : extensions1758is_public : is_public1759icon : icon1760f : (project_id, path, opts) ->1761e = new cls(project_id, path, undefined, opts)1762if not e.ext?1763console.error('You have to call super(@project_id, @filename) in the constructor to properly initialize this FileEditor instance.')1764return e17651766if feature.IS_TOUCH1767register(false, Terminal, ['term', 'sage-term'])17681769# Editing Sage worksheets1770reg1771ext : 'sagews'1772f : (project_id, path, opts) -> codemirror_session_editor(project_id, path, opts)1773is_public : false1774177517761777# See https://github.com/sagemathinc/cocalc/issues/35381778cm_refresh = (cm) ->1779if not cm?1780return1781try1782cm.refresh()1783catch err1784console.warn("cm refresh err", err)1785178617871788