Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
| Download
Views: 39550
1
###############################################################################
2
#
3
# CoCalc: Collaborative Calculation in the Cloud
4
#
5
# Copyright (C) 2014--2016, SageMath, Inc.
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#
20
###############################################################################
21
22
$ = window.$
23
24
# Editor files in a project
25
# Show button labels if there are at most this many file tabs opened.
26
# This is in exports so that an elite user could customize this by doing, e.g.,
27
# require('./editor').SHOW_BUTTON_LABELS=0
28
exports.SHOW_BUTTON_LABELS = 4
29
30
exports.MIN_SPLIT = MIN_SPLIT = 0.02
31
exports.MAX_SPLIT = MAX_SPLIT = 0.98 # maximum pane split proportion for editing
32
33
TOOLTIP_DELAY = delay: {show: 500, hide: 100}
34
35
async = require('async')
36
37
message = require('smc-util/message')
38
39
{redux} = require('./smc-react')
40
41
_ = underscore = require('underscore')
42
43
{webapp_client} = require('./webapp_client')
44
{EventEmitter} = require('events')
45
{alert_message} = require('./alerts')
46
{project_tasks} = require('./project_tasks')
47
48
feature = require('./feature')
49
IS_MOBILE = feature.IS_MOBILE
50
51
misc = require('smc-util/misc')
52
misc_page = require('./misc_page')
53
54
# Ensure CodeMirror is available and configured
55
require('./codemirror/codemirror')
56
require('./codemirror/multiplex')
57
58
# Ensure the console jquery plugin is available
59
require('./console')
60
61
# SMELL: undo doing the import below -- just use misc.[stuff] is more readable.
62
{copy, trunc, from_json, to_json, keys, defaults, required, filename_extension, filename_extension_notilde,
63
len, path_split, uuid} = require('smc-util/misc')
64
65
syncdoc = require('./syncdoc')
66
sagews = require('./sagews')
67
printing = require('./printing')
68
69
copypaste = require('./copy-paste-buffer')
70
{extra_alt_keys} = require('mobile/codemirror')
71
72
codemirror_associations =
73
c : 'text/x-c'
74
'c++' : 'text/x-c++src'
75
cql : 'text/x-sql'
76
cpp : 'text/x-c++src'
77
cc : 'text/x-c++src'
78
tcc : 'text/x-c++src'
79
conf : 'nginx' # should really have a list of different types that end in .conf and autodetect based on heuristics, letting user change.
80
csharp : 'text/x-csharp'
81
'c#' : 'text/x-csharp'
82
clj : 'text/x-clojure'
83
cljs : 'text/x-clojure'
84
cljc : 'text/x-clojure'
85
edn : 'text/x-clojure'
86
elm : 'text/x-elm'
87
cjsx : 'text/cjsx'
88
coffee : 'coffeescript'
89
css : 'css'
90
diff : 'text/x-diff'
91
dtd : 'application/xml-dtd'
92
e : 'text/x-eiffel'
93
ecl : 'ecl'
94
f : 'text/x-fortran' # https://github.com/mgaitan/CodeMirror/tree/be73b866e7381da6336b258f4aa75fb455623338/mode/fortran
95
f90 : 'text/x-fortran'
96
f95 : 'text/x-fortran'
97
h : 'text/x-c++hdr'
98
hpp : 'text/x-c++hdr'
99
hs : 'text/x-haskell'
100
lhs : 'text/x-haskell'
101
html : 'htmlmixed'
102
jade : 'text/x-pug'
103
java : 'text/x-java'
104
jl : 'text/x-julia'
105
js : 'javascript'
106
jsx : 'jsx'
107
json : 'javascript'
108
lua : 'lua'
109
m : 'text/x-octave'
110
md : 'gfm2'
111
ml : 'text/x-ocaml'
112
mysql : 'text/x-sql'
113
patch : 'text/x-diff'
114
gp : 'text/pari'
115
go : 'text/x-go'
116
pari : 'text/pari'
117
php : 'php'
118
pl : 'text/x-perl'
119
pug : 'text/x-pug'
120
py : 'python'
121
pyx : 'python'
122
r : 'r'
123
rmd : 'gfm2'
124
rnw : 'stex2'
125
rst : 'rst'
126
rb : 'text/x-ruby'
127
ru : 'text/x-ruby'
128
sage : 'python'
129
sagews : 'sagews'
130
scala : 'text/x-scala'
131
scm : 'text/x-scheme'
132
sh : 'shell'
133
spyx : 'python'
134
sql : 'text/x-sql'
135
ss : 'text/x-scheme'
136
sty : 'stex2'
137
txt : 'text'
138
tex : 'stex2'
139
ts : 'application/typescript'
140
toml : 'text/x-toml'
141
bib : 'stex'
142
bbl : 'stex'
143
xml : 'xml'
144
xsl : 'xsl'
145
yaml : 'yaml'
146
'' : 'text'
147
148
file_associations = exports.file_associations = {}
149
for ext, mode of codemirror_associations
150
name = mode
151
i = name.indexOf('x-')
152
if i != -1
153
name = name.slice(i+2)
154
name = name.replace('src','')
155
icon = switch mode
156
when 'python'
157
'cc-icon-python'
158
when 'coffeescript'
159
'fa-coffee'
160
else
161
'fa-file-code-o'
162
if ext in ['r', 'rmd']
163
icon = 'cc-icon-r'
164
file_associations[ext] =
165
editor : 'codemirror'
166
binary : false
167
icon : icon
168
opts : {mode:mode}
169
name : name
170
171
# noext = means file with no extension but the given name.
172
file_associations['noext-Dockerfile'] =
173
editor : 'codemirror'
174
binary : false
175
icon : 'fa-ship'
176
opts : {mode:'dockerfile', indent_unit:2, tab_size:2}
177
name : 'Dockerfile'
178
179
file_associations['tex'] =
180
editor : 'latex'
181
icon : 'cc-icon-tex-file'
182
opts : {mode:'stex2', indent_unit:4, tab_size:4}
183
name : "LaTeX"
184
#file_associations['tex'] = # WARNING: only for TESTING!!!
185
# editor : 'html-md'
186
# icon : 'fa-file-code-o'
187
# opts : {indent_unit:4, tab_size:4, mode:'stex2'}
188
189
file_associations['rnw'] =
190
editor : 'latex'
191
icon : 'cc-icon-tex-file'
192
opts : {mode:'stex2', indent_unit:4, tab_size:4}
193
name : "R/knitr LaTeX"
194
195
file_associations['html'] =
196
editor : 'html-md'
197
icon : 'fa-file-code-o'
198
opts : {indent_unit:4, tab_size:4, mode:'htmlmixed'}
199
name : "html"
200
201
file_associations['md'] =
202
editor : 'html-md'
203
icon : 'cc-icon-markdown'
204
opts : {indent_unit:4, tab_size:4, mode:'gfm2'}
205
name : "markdown"
206
207
file_associations['rmd'] =
208
editor : 'html-md'
209
icon : 'cc-icon-r'
210
opts : {indent_unit:4, tab_size:4, mode:'gfm2'}
211
name : "Rmd"
212
213
file_associations['java'] =
214
editor : 'html-md'
215
icon : 'fa-file-code-o'
216
opts : {indent_unit:4, tab_size:4, mode:'text/x-java'}
217
name : "Java"
218
219
file_associations['rst'] =
220
editor : 'html-md'
221
icon : 'fa-file-code-o'
222
opts : {indent_unit:4, tab_size:4, mode:'rst'}
223
name : "ReST"
224
225
file_associations['mediawiki'] = file_associations['wiki'] =
226
editor : 'html-md'
227
icon : 'fa-file-code-o'
228
opts : {indent_unit:4, tab_size:4, mode:'mediawiki'}
229
name : "MediaWiki"
230
231
file_associations['sass'] =
232
editor : 'codemirror'
233
icon : 'fa-file-code-o'
234
opts : {mode:'text/x-sass', indent_unit:2, tab_size:2}
235
name : "SASS"
236
237
file_associations['css'] =
238
editor : 'codemirror'
239
icon : 'fa-file-code-o'
240
opts : {mode:'css', indent_unit:4, tab_size:4}
241
name : "CSS"
242
243
for m in ['noext-makefile', 'noext-Makefile', 'noext-GNUmakefile', 'make', 'build']
244
file_associations[m] =
245
editor : 'codemirror'
246
icon : 'fa-cogs'
247
opts : {mode:'makefile', indent_unit:4, tab_size:4, spaces_instead_of_tabs: false}
248
name : "Makefile"
249
250
file_associations['term'] =
251
editor : 'terminal'
252
icon : 'fa-terminal'
253
opts : {}
254
name : "Terminal"
255
256
file_associations['ipynb'] =
257
editor : 'ipynb'
258
icon : 'cc-icon-ipynb'
259
opts : {}
260
name : "Jupyter Notebook"
261
262
for ext in ['png', 'jpg', 'jpeg', 'gif', 'svg']
263
file_associations[ext] =
264
editor : 'media'
265
icon : 'fa-file-image-o'
266
opts : {}
267
name : ext
268
binary : true
269
exclude_from_menu : true
270
271
VIDEO_EXTS = ['webm', 'mp4', 'avi', 'mkv']
272
for ext in VIDEO_EXTS
273
file_associations[ext] =
274
editor : 'media'
275
icon : 'fa-file-video-o'
276
opts : {}
277
name : ext
278
binary : true
279
exclude_from_menu : true
280
281
file_associations['pdf'] =
282
editor : 'pdf'
283
icon : 'fa-file-pdf-o'
284
opts : {}
285
name : 'pdf'
286
binary : true
287
exclude_from_menu : true
288
289
file_associations['tasks'] =
290
editor : 'tasks'
291
icon : 'fa-tasks'
292
opts : {}
293
name : 'task list'
294
295
file_associations['course'] =
296
editor : 'course'
297
icon : 'fa-graduation-cap'
298
opts : {}
299
name : 'course'
300
301
file_associations['sage-chat'] =
302
editor : 'chat'
303
icon : 'fa-comment'
304
opts : {}
305
name : 'chat'
306
307
file_associations['sage-git'] =
308
editor : 'git'
309
icon : 'fa-git-square'
310
opts : {}
311
name : 'git'
312
313
file_associations['sage-template'] =
314
editor : 'template'
315
icon : 'fa-clone'
316
opts : {}
317
name : 'template'
318
319
file_associations['sage-history'] =
320
editor : 'history'
321
icon : 'fa-history'
322
opts : {}
323
name : 'sage history'
324
exclude_from_menu : true
325
326
# For tar, see http://en.wikipedia.org/wiki/Tar_%28computing%29
327
archive_association =
328
editor : 'archive'
329
icon : 'fa-file-archive-o'
330
opts : {}
331
name : 'archive'
332
333
for ext in 'zip gz bz2 z lz xz lzma tgz tbz tbz2 tb2 taz tz tlz txz lzip'.split(' ')
334
file_associations[ext] = archive_association
335
336
file_associations['sage'].name = "sage code"
337
file_associations['sage'].icon = 'cc-icon-sagemath-bold'
338
339
file_associations['sagews'].name = "sage worksheet"
340
file_associations['sagews'].exclude_from_menu = true
341
file_associations['sagews'].icon = 'cc-icon-sagemath-file'
342
343
initialize_new_file_type_list = () ->
344
file_types_so_far = {}
345
v = misc.keys(file_associations)
346
v.sort()
347
f = (elt, ext, exclude) ->
348
if not ext
349
return
350
data = file_associations[ext]
351
if exclude and data.exclude_from_menu
352
return
353
if data.name? and not file_types_so_far[data.name]
354
file_types_so_far[data.name] = true
355
e = $("<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>")
356
elt.append(e)
357
358
elt = $(".smc-new-file-type-list")
359
for ext in v
360
f(elt, ext, true)
361
362
elt = $(".smc-mini-new-file-type-list")
363
file_types_so_far = {}
364
for ext in ['sagews', 'term', 'ipynb', 'tex', 'md', 'tasks', 'course', 'sage', 'py']
365
f(elt, ext)
366
elt.append($("<li class='divider'></li><li><a href='#new-folder'><i style='width: 18px;' class='fa fa-folder'></i> <span>Folder </span></a></li>"))
367
368
elt.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>"))
369
370
initialize_new_file_type_list()
371
372
exports.file_icon_class = file_icon_class = (ext) ->
373
assoc = exports.file_options('x.' + ext)
374
return assoc.icon
375
376
# Multiplex'd worksheet mode
377
378
{MARKERS} = require('smc-util/sagews')
379
380
exports.sagews_decorator_modes = sagews_decorator_modes = [
381
['cjsx' , 'text/cjsx'],
382
['coffeescript', 'coffeescript'],
383
['cython' , 'cython'],
384
['file' , 'text'],
385
['fortran' , 'text/x-fortran'],
386
['html' , 'htmlmixed'],
387
['javascript' , 'javascript'],
388
['java' , 'text/x-java'], # !! more specific name must be first!!!! (java vs javascript!)
389
['latex' , 'stex']
390
['lisp' , 'ecl'],
391
['md' , 'gfm2'],
392
['gp' , 'text/pari'],
393
['go' , 'text/x-go']
394
['perl' , 'text/x-perl'],
395
['python3' , 'python'],
396
['python' , 'python'],
397
['ruby' , 'text/x-ruby'], # !! more specific name must be first or get mismatch!
398
['r' , 'r'],
399
['sage' , 'python'],
400
['script' , 'shell'],
401
['sh' , 'shell'],
402
['julia' , 'text/x-julia'],
403
['wiki' , 'mediawiki'],
404
['mediawiki' , 'mediawiki']
405
]
406
407
# Called immediately below. It's just nice putting this code in a function.
408
define_codemirror_sagews_mode = () ->
409
410
# not using these two gfm2 and htmlmixed2 modes, with their sub-latex mode, since
411
# detection of math isn't good enough. e.g., \$ causes math mode and $ doesn't seem to... \$500 and $\sin(x)$.
412
CodeMirror.defineMode "gfm2", (config) ->
413
options = []
414
for x in [['$$','$$'], ['$','$'], ['\\[','\\]'], ['\\(','\\)']]
415
options.push
416
open : x[0]
417
close : x[1]
418
mode : CodeMirror.getMode(config, 'stex')
419
return CodeMirror.multiplexingMode(CodeMirror.getMode(config, "gfm"), options...)
420
421
CodeMirror.defineMode "htmlmixed2", (config) ->
422
options = []
423
for x in [['$$','$$'], ['$','$'], ['\\[','\\]'], ['\\(','\\)']]
424
options.push
425
open : x[0]
426
close : x[1]
427
mode : CodeMirror.getMode(config, mode)
428
return CodeMirror.multiplexingMode(CodeMirror.getMode(config, "htmlmixed"), options...)
429
430
CodeMirror.defineMode "stex2", (config) ->
431
options = []
432
for x in ['sagesilent', 'sageblock']
433
options.push
434
open : "\\begin{#{x}}"
435
close : "\\end{#{x}}"
436
mode : CodeMirror.getMode(config, 'sagews')
437
options.push
438
open : "\\sage{"
439
close : "}"
440
mode : CodeMirror.getMode(config, 'sagews')
441
return CodeMirror.multiplexingMode(CodeMirror.getMode(config, "stex"), options...)
442
443
CodeMirror.defineMode "cython", (config) ->
444
# FUTURE: need to figure out how to do this so that the name
445
# of the mode is cython
446
return CodeMirror.multiplexingMode(CodeMirror.getMode(config, "python"))
447
448
CodeMirror.defineMode "sagews", (config) ->
449
options = []
450
close = new RegExp("[#{MARKERS.output}#{MARKERS.cell}]")
451
for x in sagews_decorator_modes
452
# NOTE: very important to close on both MARKERS.output *and* MARKERS.cell,
453
# rather than just MARKERS.cell, or it will try to
454
# highlight the *hidden* output message line, which can
455
# be *enormous*, and could take a very very long time, but is
456
# a complete waste, since we never see that markup.
457
options.push
458
open : "%" + x[0]
459
start : true # must be at beginning of line
460
close : close
461
mode : CodeMirror.getMode(config, x[1])
462
463
return CodeMirror.smc_multiplexing_mode(CodeMirror.getMode(config, "python"), options...)
464
465
###
466
# ATTN: if that's ever going to be re-activated again,
467
# this needs to be require("script!...") in the spirit of webpack
468
$.get '/static/codemirror-extra/data/sage-completions.txt', (data) ->
469
s = data.split('\n')
470
sagews_hint = (editor) ->
471
console.log("sagews_hint")
472
cur = editor.getCursor()
473
token = editor.getTokenAt(cur)
474
console.log(token)
475
t = token.string
476
completions = (a for a in s when a.slice(0,t.length) == t)
477
ans =
478
list : completions,
479
from : CodeMirror.Pos(cur.line, token.start)
480
to : CodeMirror.Pos(cur.line, token.end)
481
CodeMirror.registerHelper("hint", "sagews", sagews_hint)
482
###
483
484
# Initialize all of the codemirror modes and extensions, since the editor may need them.
485
# OPTIMIZATION: defer this until we actually open a document that actually relies on codemirror.
486
# (one step at a time!)
487
define_codemirror_sagews_mode()
488
misc_page.define_codemirror_extensions()
489
490
# Given a text file (defined by content), try to guess
491
# what the extension should be.
492
guess_file_extension_type = (content) ->
493
content = $.trim(content)
494
i = content.indexOf('\n')
495
first_line = content.slice(0,i).toLowerCase()
496
if first_line.slice(0,2) == '#!'
497
# A script. What kind?
498
if first_line.indexOf('python') != -1
499
return 'py'
500
if first_line.indexOf('bash') != -1 or first_line.indexOf('sh') != -1
501
return 'sh'
502
if first_line.indexOf('html') != -1
503
return 'html'
504
if first_line.indexOf('/*') != -1 or first_line.indexOf('//') != -1 # kind of a stretch
505
return 'c++'
506
return undefined
507
508
exports.file_options = (filename, content) -> # content may be undefined
509
ext = misc.filename_extension_notilde(filename)?.toLowerCase()
510
if not ext? and content? # no recognized extension, but have contents
511
ext = guess_file_extension_type(content)
512
if ext == ''
513
x = file_associations["noext-#{misc.path_split(filename).tail}"]
514
else
515
x = file_associations[ext]
516
if not x?
517
x = file_associations['']
518
# Don't use the icon for this fallback, to give the icon selection below a chance to work;
519
# we do this so new react editors work. All this code will go away someday.
520
delete x.icon
521
if not x.icon?
522
# Use the new react editor icons first, if they exist...
523
icon = require('./project_file').icon(ext)
524
if icon?
525
x.icon = 'fa-' + icon
526
else
527
x.icon = 'fa-file-code-o'
528
return x
529
530
SEP = "\uFE10"
531
532
_local_storage_prefix = (project_id, filename, key) ->
533
s = project_id
534
if filename?
535
s += filename + SEP
536
if key?
537
s += key
538
return s
539
#
540
# Set or get something about a project from local storage:
541
#
542
# local_storage(project_id): returns everything known about this project.
543
# local_storage(project_id, filename): get everything about given filename in project
544
# local_storage(project_id, filename, key): get value of key for given filename in project
545
# local_storage(project_id, filename, key, value): set value of key
546
#
547
# In all cases, returns undefined if localStorage is not supported in this browser.
548
#
549
550
if misc.has_local_storage()
551
local_storage_delete = exports.local_storage_delete = (project_id, filename, key) ->
552
storage = window.localStorage
553
if storage?
554
prefix = _local_storage_prefix(project_id, filename, key)
555
n = prefix.length
556
for k, v of storage
557
if k.slice(0,n) == prefix
558
delete storage[k]
559
560
local_storage = exports.local_storage = (project_id, filename, key, value) ->
561
storage = window.localStorage
562
if storage?
563
prefix = _local_storage_prefix(project_id, filename, key)
564
n = prefix.length
565
if filename?
566
if key?
567
if value?
568
storage[prefix] = misc.to_json(value)
569
else
570
x = storage[prefix]
571
if not x?
572
return x
573
else
574
return misc.from_json(x)
575
else
576
# Everything about a given filename
577
obj = {}
578
for k, v of storage
579
if k.slice(0,n) == prefix
580
obj[k.split(SEP)[1]] = v
581
return obj
582
else
583
# Everything about project
584
obj = {}
585
for k, v of storage
586
if k.slice(0,n) == prefix
587
x = k.slice(n)
588
z = x.split(SEP)
589
filename = z[0]
590
key = z[1]
591
if not obj[filename]?
592
obj[filename] = {}
593
obj[filename][key] = v
594
return obj
595
else
596
# no-op fallback
597
console.warn("cursor saving won't work due to lack of localStorage")
598
local_storage_delete = local_storage = () ->
599
600
templates = $("#webapp-editor-templates")
601
602
###############################################
603
# Abstract base class for editors (not exports.Editor)
604
###############################################
605
# Derived classes must:
606
# (1) implement the _get and _set methods
607
# (2) show/hide/remove
608
#
609
# Events ensure that *all* users editor the same file see the same
610
# thing (synchronized).
611
#
612
613
class FileEditor extends EventEmitter
614
# ATTN it is crucial to call this constructor in subclasses via super(@project_id, @filename)
615
constructor: (@project_id, @filename) ->
616
@ext = misc.filename_extension_notilde(@filename)?.toLowerCase()
617
@_show = underscore.debounce(@_show, 50)
618
619
is_active: () =>
620
misc.tab_to_path(redux.getProjectStore(@project_id).get('active_project_tab')) == @filename
621
622
# call it, to set the @default_font_size from the account settings
623
init_font_size: () =>
624
@default_font_size = redux.getStore('account').get('font_size')
625
626
val: (content) =>
627
if not content?
628
# If content not defined, returns current value.
629
return @_get()
630
else
631
# If content is defined, sets value.
632
@_set(content)
633
634
# has_unsaved_changes() returns the state, where true means that
635
# there are unsaved changed. To set the state, do
636
# has_unsaved_changes(true or false).
637
has_unsaved_changes: (val) =>
638
if not val?
639
return @_has_unsaved_changes
640
else
641
if not @_has_unsaved_changes? or @_has_unsaved_changes != val
642
if val
643
@save_button.removeClass('disabled')
644
else
645
@_when_had_no_unsaved_changes = new Date() # when we last knew for a fact there are no unsaved changes
646
@save_button.addClass('disabled')
647
@_has_unsaved_changes = val
648
649
# committed means "not saved to the database/server", whereas save above
650
# means "saved to *disk*".
651
has_uncommitted_changes: (val) =>
652
if not val?
653
return @_has_uncommitted_changes
654
else
655
@_has_uncommitted_changes = val
656
if val
657
if not @_show_uncommitted_warning_timeout?
658
# We have not already started a timer, so start one -- if we do not hear otherwise, show
659
# the warning in 30s.
660
@_show_uncommitted_warning_timeout = setTimeout((()=>@_show_uncommitted_warning()), 30000)
661
else
662
if @_show_uncommitted_warning_timeout?
663
clearTimeout(@_show_uncommitted_warning_timeout)
664
delete @_show_uncommitted_warning_timeout
665
@uncommitted_element?.hide()
666
667
_show_uncommitted_warning: () =>
668
delete @_show_uncommitted_warning_timeout
669
@uncommitted_element?.show()
670
671
focus: () => # FUTURE in derived class (???)
672
673
_get: () =>
674
console.warn("Incomplete: editor -- needs to implement _get in derived class")
675
676
_set: (content) =>
677
console.warn("Incomplete: editor -- needs to implement _set in derived class")
678
679
restore_cursor_position: () =>
680
# implement in a derived class if you need this
681
682
disconnect_from_session: (cb) =>
683
# implement in a derived class if you need this
684
685
local_storage: (key, value) =>
686
return local_storage(@project_id, @filename, key, value)
687
688
show: (opts) =>
689
if not opts?
690
if @_last_show_opts?
691
opts = @_last_show_opts
692
else
693
opts = {}
694
@_last_show_opts = opts
695
696
# only re-render the editor if it is active. that's crucial, because e.g. the autosave
697
# of latex triggers a build, which in turn calls @show to update itself. that would cause
698
# the latex editor to be visible despite not being the active editor.
699
if not @is_active?()
700
return
701
702
@element.show()
703
# if above line reveals it, give it a bit time to do the layout first
704
@_show(opts) # critical -- also do an intial layout! Otherwise get a horrible messed up animation effect.
705
setTimeout((=> @_show(opts)), 10)
706
if DEBUG
707
window?.smc?.doc = @ # useful for debugging...
708
709
_show: (opts={}) =>
710
# define in derived class
711
712
hide: () =>
713
#@element?.hide()
714
715
remove: () =>
716
@syncdoc?.close()
717
@element?.remove()
718
@removeAllListeners()
719
720
terminate_session: () =>
721
# If some backend session on a remote machine is serving this session, terminate it.
722
723
exports.FileEditor = FileEditor
724
725
###############################################
726
# Codemirror-based File Editor
727
728
# - 'saved' : when the file is successfully saved by the user
729
# - 'show' :
730
# - 'toggle-split-view' :
731
###############################################
732
class CodeMirrorEditor extends FileEditor
733
constructor: (@project_id, @filename, content, opts) ->
734
super(@project_id, @filename)
735
editor_settings = redux.getStore('account').get_editor_settings()
736
opts = @opts = defaults opts,
737
mode : undefined
738
geometry : undefined # (default=full screen);
739
read_only : false
740
delete_trailing_whitespace: editor_settings.strip_trailing_whitespace # delete on save
741
show_trailing_whitespace : editor_settings.show_trailing_whitespace
742
allow_javascript_eval : true # if false, the one use of eval isn't allowed.
743
line_numbers : editor_settings.line_numbers
744
first_line_number : editor_settings.first_line_number
745
indent_unit : editor_settings.indent_unit
746
tab_size : editor_settings.tab_size
747
smart_indent : editor_settings.smart_indent
748
electric_chars : editor_settings.electric_chars
749
undo_depth : editor_settings.undo_depth # no longer relevant, since done via sync system
750
match_brackets : editor_settings.match_brackets
751
code_folding : editor_settings.code_folding
752
auto_close_brackets : editor_settings.auto_close_brackets
753
match_xml_tags : editor_settings.match_xml_tags
754
auto_close_xml_tags : editor_settings.auto_close_xml_tags
755
line_wrapping : editor_settings.line_wrapping
756
spaces_instead_of_tabs : editor_settings.spaces_instead_of_tabs
757
style_active_line : 15 # editor_settings.style_active_line # (a number between 0 and 127)
758
bindings : editor_settings.bindings # 'standard', 'vim', or 'emacs'
759
theme : editor_settings.theme
760
track_revisions : editor_settings.track_revisions
761
public_access : false
762
latex_editor : false
763
764
# I'm making the times below very small for now. If we have to adjust these to reduce load, due to lack
765
# of capacity, then we will. Or, due to lack of optimization (e.g., for big documents). These parameters
766
# below would break editing a huge file right now, due to slowness of applying a patch to a codemirror editor.
767
768
cursor_interval : 1000 # minimum time (in ms) between sending cursor position info to hub -- used in sync version
769
sync_interval : 500 # minimum time (in ms) between synchronizing text with hub. -- used in sync version below
770
771
completions_size : 20 # for tab completions (when applicable, e.g., for sage sessions)
772
773
#console.log("mode =", opts.mode)
774
775
@element = templates.find(".webapp-editor-codemirror").clone()
776
777
@element.data('editor', @)
778
779
@init_save_button()
780
@init_uncommitted_element()
781
@init_history_button()
782
@init_edit_buttons()
783
784
@init_file_actions()
785
786
filename = @filename
787
if filename.length > 30
788
filename = "…" + filename.slice(filename.length-30)
789
790
# not really needed due to highlighted tab; annoying.
791
#@element.find(".webapp-editor-codemirror-filename").text(filename)
792
793
@_video_is_on = @local_storage("video_is_on")
794
if not @_video_is_on?
795
@_video_is_on = false
796
797
extraKeys =
798
"Alt-Enter" : (editor) => @action_key(execute: true, advance:false, split:false)
799
"Cmd-Enter" : (editor) => @action_key(execute: true, advance:false, split:false)
800
"Ctrl-Enter" : (editor) => @action_key(execute: true, advance:true, split:true)
801
"Ctrl-;" : (editor) => @action_key(split:true, execute:false, advance:false)
802
"Cmd-;" : (editor) => @action_key(split:true, execute:false, advance:false)
803
"Ctrl-\\" : (editor) => @action_key(execute:false, toggle_input:true)
804
#"Cmd-x" : (editor) => @action_key(execute:false, toggle_input:true)
805
"Shift-Ctrl-\\" : (editor) => @action_key(execute:false, toggle_output:true)
806
#"Shift-Cmd-y" : (editor) => @action_key(execute:false, toggle_output:true)
807
808
"Cmd-S" : (editor) => @click_save_button()
809
"Alt-S" : (editor) => @click_save_button()
810
811
"Ctrl-L" : (editor) => @goto_line(editor)
812
"Cmd-L" : (editor) => @goto_line(editor)
813
814
"Ctrl-I" : (editor) => @toggle_split_view(editor)
815
"Cmd-I" : (editor) => @toggle_split_view(editor)
816
817
"Shift-Cmd-L" : (editor) => editor.align_assignments()
818
"Shift-Ctrl-L" : (editor) => editor.align_assignments()
819
820
"Shift-Ctrl-." : (editor) => @change_font_size(editor, +1)
821
"Shift-Ctrl-," : (editor) => @change_font_size(editor, -1)
822
823
"Shift-Cmd-." : (editor) => @change_font_size(editor, +1)
824
"Shift-Cmd-," : (editor) => @change_font_size(editor, -1)
825
826
"Shift-Tab" : (editor) => editor.unindent_selection()
827
828
"Ctrl-'" : "indentAuto"
829
"Cmd-'" : "indentAuto"
830
831
"Cmd-/" : "toggleComment"
832
"Ctrl-/" : "toggleComment" # shortcut chosen by jupyter project (undocumented)
833
834
"Tab" : (editor) => @press_tab_key(editor)
835
"Shift-Ctrl-C" : (editor) => @interrupt_key()
836
837
"Ctrl-Space" : "autocomplete"
838
839
if feature.IS_TOUCH
840
# Better more external keyboard friendly shortcuts, motivated by iPad.
841
extra_alt_keys(extraKeys, @, opts)
842
843
if opts.match_xml_tags
844
extraKeys['Ctrl-J'] = "toMatchingTag"
845
846
if opts.bindings != 'emacs'
847
# Emacs uses control s for find.
848
extraKeys["Ctrl-S"] = (editor) => @click_save_button()
849
850
# FUTURE: We will replace this by a general framework...
851
if misc.filename_extension_notilde(filename).toLowerCase() == "sagews"
852
evaluate_key = redux.getStore('account').get('evaluate_key').toLowerCase()
853
if evaluate_key == "enter"
854
evaluate_key = "Enter"
855
else
856
evaluate_key = "Shift-Enter"
857
extraKeys[evaluate_key] = (editor) => @action_key(execute: true, advance:true, split:false)
858
else
859
extraKeys["Shift-Enter"] = =>
860
alert_message
861
type : "error"
862
message : "You can only evaluate code in a file that ends with the extension 'sagews'. Create a Sage Worksheet instead."
863
864
# Layouts:
865
# 0 - one single editor
866
# 1 - two editors, one on top of the other
867
# 2 - two editors, one next to the other
868
869
if IS_MOBILE
870
@_layout = 0
871
else
872
@_layout = @local_storage("layout") ? 0 # WARNING/UGLY: used by syncdoc.coffee and sagews.coffee !
873
if @_layout not in [0, 1, 2]
874
# IMPORTANT: If this were anything other than what is listed, the user
875
# would never be able to open tex files. So it's important that this be valid.
876
@_layout = 0
877
@_last_layout = undefined
878
879
if feature.isMobile.Android()
880
# see https://github.com/sragemathinc/smc/issues/1360
881
opts.style_active_line = false
882
883
make_editor = (node) =>
884
options =
885
firstLineNumber : opts.first_line_number
886
autofocus : false
887
mode : {name:opts.mode, globalVars: true}
888
lineNumbers : opts.line_numbers
889
showTrailingSpace : opts.show_trailing_whitespace
890
indentUnit : opts.indent_unit
891
tabSize : opts.tab_size
892
smartIndent : opts.smart_indent
893
electricChars : opts.electric_chars
894
undoDepth : opts.undo_depth
895
matchBrackets : opts.match_brackets
896
autoCloseBrackets : opts.auto_close_brackets and (misc.filename_extension_notilde(filename) not in ['hs', 'lhs']) #972
897
autoCloseTags : opts.auto_close_xml_tags
898
lineWrapping : opts.line_wrapping
899
readOnly : opts.read_only
900
styleActiveLine : opts.style_active_line
901
indentWithTabs : not opts.spaces_instead_of_tabs
902
showCursorWhenSelecting : true
903
extraKeys : extraKeys
904
cursorScrollMargin : 6
905
viewportMargin : 10
906
907
if opts.match_xml_tags
908
options.matchTags = {bothTags: true}
909
910
if opts.code_folding
911
extraKeys["Ctrl-Q"] = (cm) -> cm.foldCodeSelectionAware()
912
extraKeys["Alt-Q"] = (cm) -> cm.foldCodeSelectionAware()
913
options.foldGutter = true
914
options.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"]
915
916
if opts.latex_editor
917
options.gutters ?= []
918
options.gutters.push("Codemirror-latex-errors")
919
920
if opts.bindings? and opts.bindings != "standard"
921
options.keyMap = opts.bindings
922
#cursorBlinkRate: 1000
923
924
if opts.theme? and opts.theme != "standard"
925
options.theme = opts.theme
926
927
cm = CodeMirror.fromTextArea(node, options)
928
cm.save = () => @click_save_button()
929
930
# The Codemirror themes impose their own weird fonts, but most users want whatever
931
# they've configured as "monospace" in their browser. So we force that back:
932
e = $(cm.getWrapperElement())
933
e.attr('style', e.attr('style') + '; height:100%; font-family:monospace !important;')
934
# see http://stackoverflow.com/questions/2655925/apply-important-css-style-using-jquery
935
936
if opts.bindings == 'vim'
937
# annoying due to api change in vim mode
938
cm.setOption("vimMode", true)
939
940
return cm
941
942
elt = @element.find(".webapp-editor-textarea-0"); elt.text(content)
943
944
@codemirror = make_editor(elt[0])
945
@codemirror.name = '0'
946
#window.cm = @codemirror
947
948
elt1 = @element.find(".webapp-editor-textarea-1")
949
950
@codemirror1 = make_editor(elt1[0])
951
@codemirror1.name = '1'
952
953
buf = @codemirror.linkedDoc({sharedHist: true})
954
@codemirror1.swapDoc(buf)
955
956
@codemirror.on 'focus', () =>
957
@codemirror_with_last_focus = @codemirror
958
959
@codemirror1.on 'focus', () =>
960
@codemirror_with_last_focus = @codemirror1
961
962
if @opts.bindings == 'vim'
963
@_vim_mode = 'visual'
964
@codemirror.on 'vim-mode-change', (obj) =>
965
if obj.mode == 'normal'
966
@_vim_mode = 'visual'
967
@element.find("a[href='#vim-mode-toggle']").text('esc')
968
else
969
@_vim_mode = 'insert'
970
@element.find("a[href='#vim-mode-toggle']").text('i')
971
972
if feature.IS_TOUCH
973
# ugly hack so more usable on touch...
974
@element.find(".webapp-editor-resize-bar-layout-1").height('12px')
975
@element.find(".webapp-editor-resize-bar-layout-2").width('12px')
976
977
@init_font_size() # get the @default_font_size
978
@restore_font_size()
979
980
@init_draggable_splits()
981
982
if opts.read_only
983
@set_readonly_ui()
984
985
if misc.filename_extension(@filename)?.toLowerCase() == 'sagews'
986
@init_sagews_edit_buttons()
987
988
@examples_dialog = null
989
990
programmatical_goto_line: (line) =>
991
cm = @codemirror_with_last_focus
992
pos = {line:line-1, ch:0}
993
info = cm.getScrollInfo()
994
cm.scrollIntoView(pos, info.clientHeight/2)
995
996
get_users_cursors: (account_id) =>
997
return @syncdoc?.get_users_cursors(account_id)
998
999
init_file_actions: () =>
1000
if not @element?
1001
return
1002
actions = redux.getProjectActions(@project_id)
1003
dom_node = @element.find('.smc-editor-file-info-dropdown')[0]
1004
require('./r_misc').render_file_info_dropdown(@filename, actions, dom_node, @opts.public_access)
1005
1006
init_draggable_splits: () =>
1007
@_layout1_split_pos = @local_storage("layout1_split_pos")
1008
@_layout2_split_pos = @local_storage("layout2_split_pos")
1009
1010
layout1_bar = @element.find(".webapp-editor-resize-bar-layout-1")
1011
layout1_bar.draggable
1012
axis : 'y'
1013
containment : @element
1014
zIndex : 10
1015
start : misc_page.drag_start_iframe_disable
1016
stop : (event, ui) =>
1017
misc_page.drag_stop_iframe_enable()
1018
# compute the position of bar as a number from 0 to 1, with
1019
# 0 being at top (left), 1 at bottom (right), and .5 right in the middle
1020
e = @element.find(".webapp-editor-codemirror-input-container-layout-1")
1021
top = e.offset().top
1022
ht = e.height()
1023
p = layout1_bar.offset().top + layout1_bar.height()/2
1024
@_layout1_split_pos = (p - top) / ht
1025
@local_storage("layout1_split_pos", @_layout1_split_pos)
1026
# redraw, which uses split info
1027
@show()
1028
1029
layout2_bar = @element.find(".webapp-editor-resize-bar-layout-2")
1030
layout2_bar.draggable
1031
axis : 'x'
1032
containment : @element
1033
zIndex : 100
1034
start : misc_page.drag_start_iframe_disable
1035
stop : (event, ui) =>
1036
misc_page.drag_stop_iframe_enable()
1037
# compute the position of bar as a number from 0 to 1, with
1038
# 0 being at top (left), 1 at bottom (right), and .5 right in the middle
1039
e = @element.find(".webapp-editor-codemirror-input-container-layout-2")
1040
left = e.offset().left
1041
width = e.width()
1042
p = layout2_bar.offset().left
1043
@_layout2_split_pos = (p - left) / width
1044
@local_storage("layout2_split_pos", @_layout2_split_pos)
1045
# redraw, which uses split info
1046
@show()
1047
1048
hide_content: () =>
1049
@element.find(".webapp-editor-codemirror-content").hide()
1050
1051
show_content: () =>
1052
@hide_startup_message()
1053
@element.find(".webapp-editor-codemirror-content").show()
1054
for cm in @codemirrors()
1055
cm.refresh()
1056
1057
hide_startup_message: () =>
1058
@element.find(".webapp-editor-codemirror-startup-message").hide()
1059
1060
show_startup_message: (mesg, type='info') =>
1061
@hide_content()
1062
if typeof(mesg) != 'string'
1063
mesg = JSON.stringify(mesg)
1064
e = @element.find(".webapp-editor-codemirror-startup-message").show().text(mesg)
1065
for t in ['success', 'info', 'warning', 'danger']
1066
e.removeClass("alert-#{t}")
1067
e.addClass("alert-#{type}")
1068
1069
is_active: () =>
1070
return @codemirror? and misc.tab_to_path(redux.getProjectStore(@project_id).get('active_project_tab')) == @filename
1071
1072
set_theme: (theme) =>
1073
# Change the editor theme after the editor has been created
1074
for cm in @codemirrors()
1075
cm.setOption('theme', theme)
1076
@opts.theme = theme
1077
1078
# add something visual to the UI to suggest that the file is read only
1079
set_readonly_ui: (readonly=true) =>
1080
@opts.read_only = readonly
1081
@element.find(".webapp-editor-write-only").toggle(!readonly)
1082
@element.find(".webapp-editor-read-only").toggle(readonly)
1083
for cm in @codemirrors()
1084
cm.setOption('readOnly', readonly)
1085
1086
set_cursor_center_focus: (pos, tries=5) =>
1087
if tries <= 0
1088
return
1089
cm = @codemirror_with_last_focus
1090
if not cm?
1091
cm = @codemirror
1092
if not cm?
1093
return
1094
cm.setCursor(pos)
1095
info = cm.getScrollInfo()
1096
try
1097
# This call can fail during editor initialization (as of codemirror 3.19, but not before).
1098
cm.scrollIntoView(pos, info.clientHeight/2)
1099
catch e
1100
setTimeout((() => @set_cursor_center_focus(pos, tries-1)), 250)
1101
cm.focus()
1102
1103
disconnect_from_session: (cb) =>
1104
# implement in a derived class if you need this
1105
@syncdoc?.disconnect_from_session()
1106
cb?()
1107
1108
codemirrors: () =>
1109
c = [@codemirror, @codemirror1]
1110
return underscore.filter(c, ((x) -> x?))
1111
1112
focused_codemirror: () =>
1113
if @codemirror_with_last_focus?
1114
return @codemirror_with_last_focus
1115
else
1116
return @codemirror
1117
1118
action_key: (opts) =>
1119
# opts ignored by default; worksheets use them....
1120
@click_save_button()
1121
1122
interrupt_key: () =>
1123
# does nothing for generic editor, but important, e.g., for the sage worksheet editor.
1124
1125
press_tab_key: (editor) =>
1126
if editor.somethingSelected()
1127
CodeMirror.commands.defaultTab(editor)
1128
else
1129
@tab_nothing_selected(editor)
1130
1131
tab_nothing_selected: (editor) =>
1132
if @opts.spaces_instead_of_tabs
1133
editor.tab_as_space()
1134
else
1135
CodeMirror.commands.defaultTab(editor)
1136
1137
init_edit_buttons: () =>
1138
that = @
1139
button_names = ['search', 'next', 'prev', 'replace', 'undo', 'redo', 'autoindent',
1140
'shift-left', 'shift-right', 'split-view','increase-font', 'decrease-font', 'goto-line',
1141
'copy', 'paste', 'vim-mode-toggle']
1142
1143
if @opts.bindings != 'vim'
1144
@element.find("a[href='#vim-mode-toggle']").remove()
1145
1146
# if the file extension indicates that we know how to print it, show and enable the print button
1147
if printing.can_print(@ext)
1148
button_names.push('print')
1149
else
1150
@element.find('a[href="#print"]').remove()
1151
1152
# sagews2pdf conversion
1153
if @ext == 'sagews'
1154
button_names.push('sagews2pdf')
1155
button_names.push('sagews2ipynb')
1156
else
1157
@element.find('a[href="#sagews2pdf"]').remove()
1158
@element.find('a[href="#sagews2ipynb"]').remove()
1159
1160
for name in button_names
1161
e = @element.find("a[href=\"##{name}\"]")
1162
e.data('name', name).tooltip(delay:{ show: 500, hide: 100 }).click (event) ->
1163
that.click_edit_button($(@).data('name'))
1164
return false
1165
1166
click_edit_button: (name) =>
1167
cm = @codemirror_with_last_focus
1168
if not cm?
1169
cm = @codemirror
1170
if not cm?
1171
return
1172
switch name
1173
when 'search'
1174
CodeMirror.commands.find(cm)
1175
when 'next'
1176
if cm._searchState?.query
1177
CodeMirror.commands.findNext(cm)
1178
else
1179
CodeMirror.commands.goPageDown(cm)
1180
cm.focus()
1181
when 'prev'
1182
if cm._searchState?.query
1183
CodeMirror.commands.findPrev(cm)
1184
else
1185
CodeMirror.commands.goPageUp(cm)
1186
cm.focus()
1187
when 'replace'
1188
CodeMirror.commands.replace(cm)
1189
when 'undo'
1190
cm.undo()
1191
cm.focus()
1192
when 'redo'
1193
cm.redo()
1194
cm.focus()
1195
when 'split-view'
1196
@toggle_split_view(cm)
1197
when 'autoindent'
1198
CodeMirror.commands.indentAuto(cm)
1199
when 'shift-left'
1200
cm.unindent_selection()
1201
cm.focus()
1202
when 'shift-right'
1203
@press_tab_key(cm)
1204
cm.focus()
1205
when 'increase-font'
1206
@change_font_size(cm, +1)
1207
cm.focus()
1208
when 'decrease-font'
1209
@change_font_size(cm, -1)
1210
cm.focus()
1211
when 'goto-line'
1212
@goto_line(cm)
1213
when 'copy'
1214
@copy(cm)
1215
cm.focus()
1216
when 'paste'
1217
@paste(cm)
1218
cm.focus()
1219
when 'sagews2pdf'
1220
@print(sagews2html = false)
1221
when 'sagews2ipynb'
1222
@convert_to_ipynb()
1223
when 'print'
1224
@print(sagews2html = true)
1225
when 'vim-mode-toggle'
1226
if @_vim_mode == 'visual'
1227
CodeMirror.Vim.handleKey(cm, 'i')
1228
else
1229
CodeMirror.Vim.exitInsertMode(cm)
1230
cm.focus()
1231
1232
restore_font_size: () =>
1233
# we set the font_size from local storage
1234
# or fall back to the default from the account settings
1235
for i, cm of @codemirrors()
1236
size = @local_storage("font_size#{i}")
1237
if size?
1238
@set_font_size(cm, size)
1239
else if @default_font_size?
1240
@set_font_size(cm, @default_font_size)
1241
1242
get_font_size: (cm) ->
1243
if not cm?
1244
return
1245
elt = $(cm.getWrapperElement())
1246
return elt.data('font-size') ? @default_font_size
1247
1248
set_font_size: (cm, size) =>
1249
if not cm?
1250
return
1251
if size > 1
1252
elt = $(cm.getWrapperElement())
1253
elt.css('font-size', size + 'px')
1254
elt.data('font-size', size)
1255
1256
change_font_size: (cm, delta) =>
1257
if not cm?
1258
return
1259
#console.log("change_font_size #{cm.name}, #{delta}")
1260
scroll_before = cm.getScrollInfo()
1261
1262
elt = $(cm.getWrapperElement())
1263
size = elt.data('font-size')
1264
if not size?
1265
s = elt.css('font-size')
1266
size = parseInt(s.slice(0,s.length-2))
1267
new_size = size + delta
1268
@set_font_size(cm, new_size)
1269
@local_storage("font_size#{cm.name}", new_size)
1270
1271
# we have to do the scrollTo in the next render loop, since otherwise
1272
# the getScrollInfo function below will return the sizing data about
1273
# the cm instance before the above css font-size change has been rendered.
1274
f = () =>
1275
cm.refresh()
1276
scroll_after = cm.getScrollInfo()
1277
x = (scroll_before.left / scroll_before.width) * scroll_after.width
1278
y = (((scroll_before.top+scroll_before.clientHeight/2) / scroll_before.height) * scroll_after.height) - scroll_after.clientHeight/2
1279
cm.scrollTo(x, y)
1280
setTimeout(f, 0)
1281
1282
toggle_split_view: (cm) =>
1283
if not cm?
1284
return
1285
@_layout = (@_layout + 1) % 3
1286
@local_storage("layout", @_layout)
1287
@show()
1288
if cm? and not feature.IS_TOUCH
1289
if @_layout > 0
1290
cm.focus()
1291
else
1292
# focus first editor since it is only one that is visible.
1293
@codemirror.focus()
1294
f = () =>
1295
for x in @codemirrors()
1296
x.scrollIntoView() # scroll the cursors back into view -- see https://github.com/sagemathinc/cocalc/issues/1044
1297
setTimeout(f, 1) # wait until next loop after codemirror has laid itself out.
1298
@emit 'toggle-split-view'
1299
1300
goto_line: (cm) =>
1301
if not cm?
1302
return
1303
focus = () =>
1304
@focus()
1305
cm.focus()
1306
dialog = templates.find(".webapp-goto-line-dialog").clone()
1307
dialog.modal('show')
1308
dialog.find(".btn-close").off('click').click () ->
1309
dialog.modal('hide')
1310
setTimeout(focus, 50)
1311
return false
1312
input = dialog.find(".webapp-goto-line-input")
1313
input.val(cm.getCursor().line+1) # +1 since line is 0-based
1314
dialog.find(".webapp-goto-line-range").text("1-#{cm.lineCount()} or n%")
1315
dialog.find(".webapp-goto-line-input").focus().select()
1316
submit = () =>
1317
dialog.modal('hide')
1318
result = input.val().trim()
1319
if result.length >= 1 and result[result.length-1] == '%'
1320
line = Math.floor( cm.lineCount() * parseInt(result.slice(0,result.length-1)) / 100.0)
1321
else
1322
line = Math.min(parseInt(result)-1)
1323
if line >= cm.lineCount()
1324
line = cm.lineCount() - 1
1325
if line <= 0
1326
line = 0
1327
pos = {line:line, ch:0}
1328
cm.setCursor(pos)
1329
info = cm.getScrollInfo()
1330
cm.scrollIntoView(pos, info.clientHeight/2)
1331
setTimeout(focus, 50)
1332
dialog.find(".btn-submit").off('click').click(submit)
1333
input.keydown (evt) =>
1334
if evt.which == 13 # enter
1335
submit()
1336
return false
1337
if evt.which == 27 # escape
1338
setTimeout(focus, 50)
1339
dialog.modal('hide')
1340
return false
1341
1342
copy: (cm) =>
1343
if not cm?
1344
return
1345
copypaste.set_buffer(cm.getSelection())
1346
1347
convert_to_ipynb: () =>
1348
p = misc.path_split(@filename)
1349
v = p.tail.split('.')
1350
if v.length <= 1
1351
ext = ''
1352
base = p.tail
1353
else
1354
ext = v[v.length-1]
1355
base = v.slice(0,v.length-1).join('.')
1356
1357
if ext != 'sagews'
1358
console.error("editor.print called on file with extension '#{ext}' but only supports 'sagews'.")
1359
return
1360
1361
async.series([
1362
(cb) =>
1363
@save(cb)
1364
(cb) =>
1365
webapp_client.exec
1366
project_id : @project_id
1367
command : "smc-sagews2ipynb #{@filename}"
1368
bash : true
1369
err_on_exit : false
1370
cb : (err, output) =>
1371
if err
1372
alert_message(type:"error", message:"Error occured converting #{@filename}")
1373
else
1374
redux.getProjectActions(@project_id).open_file
1375
path : base + '.ipynb'
1376
foreground : true
1377
])
1378
1379
cut: (cm) =>
1380
if not cm?
1381
return
1382
copypaste.set_buffer(cm.getSelection())
1383
cm.replaceSelection('')
1384
1385
paste: (cm) =>
1386
if not cm?
1387
return
1388
cm.replaceSelection(copypaste.get_buffer())
1389
1390
print: (sagews2html = true) =>
1391
switch @ext
1392
when 'sagews'
1393
if sagews2html
1394
@print_html()
1395
else
1396
@print_sagews()
1397
when 'txt', 'csv'
1398
print_button = @element.find('a[href="#print"]')
1399
print_button.icon_spin(start:true, delay:0).addClass("disabled")
1400
printing.Printer(@, @filename + '.pdf').print (err) ->
1401
print_button.removeClass('disabled')
1402
print_button.icon_spin(false)
1403
if err
1404
alert_message
1405
type : "error"
1406
message : "Printing error -- #{err}"
1407
1408
print_html: =>
1409
dialog = null
1410
d_content = null
1411
d_open = null
1412
d_download = null
1413
d_progress = _.noop
1414
output_fn = null # set this before showing the dialog
1415
1416
show_dialog = (cb) =>
1417
# this creates the dialog element and defines the action functions like d_progress
1418
dialog = $("""
1419
<div class="modal" tabindex="-1" role="dialog">
1420
<div class="modal-dialog" role="document">
1421
<div class="modal-content">
1422
<div class="modal-header">
1423
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
1424
<h4 class="modal-title">Print to HTML</h4>
1425
</div>
1426
<div class="modal-body">
1427
<div class="progress">
1428
<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
1429
0 %
1430
</div>
1431
</div>
1432
<div class="content" style="text-align: center;"></div>
1433
<div style="margin-top: 25px;">
1434
<p><b>More information</b></p>
1435
<p>
1436
This SageWS to HTML conversion transforms the current worksheet
1437
to a static HTML file.
1438
<br/>
1439
<a href="https://github.com/sagemathinc/cocalc/wiki/sagews2html" target='_blank'>Click here for more information</a>.
1440
</p>
1441
</div>
1442
</div>
1443
<div class="modal-footer">
1444
<button type="button" class="btn-download btn btn-primary disabled">Download</button>
1445
<button type="button" class="btn-open btn btn-success disabled">Open</button>
1446
<button type="button" class="btn-close btn btn-default" data-dismiss="modal">Close</button>
1447
</div>
1448
</div>
1449
</div>
1450
</div>
1451
""")
1452
d_content = dialog.find('.content')
1453
d_open = dialog.find('.btn-open')
1454
d_download = dialog.find('.btn-download')
1455
action = redux.getProjectActions(@project_id)
1456
d_progress = (p) ->
1457
pct = "#{Math.round(100 * p)}%"
1458
dialog.find(".progress-bar").css('width', pct).text(pct)
1459
dialog.find('.btn-close').click ->
1460
dialog.modal('hide')
1461
return false
1462
d_open.click =>
1463
action.download_file
1464
path : output_fn
1465
auto : false # open in new tab
1466
d_download.click =>
1467
action.download_file
1468
path : output_fn
1469
auto : true
1470
dialog.modal('show')
1471
cb()
1472
1473
convert = (cb) =>
1474
# initiates the actual conversion via printing.Printer ...
1475
switch @ext
1476
when 'sagews'
1477
output_fn = @filename + '.html'
1478
progress = (percent, mesg) =>
1479
d_content.text(mesg)
1480
d_progress(percent)
1481
progress = _.debounce(progress, 5)
1482
progress(.01, "Loading ...")
1483
done = (err) =>
1484
#console.log 'Printer.print_html is done: err = ', err
1485
if err
1486
progress(0, "Problem printing to HTML: #{err}")
1487
else
1488
progress(1, 'Printing finished.')
1489
# enable open & download buttons
1490
dialog.find('button.btn').removeClass('disabled')
1491
printing.Printer(@, output_fn).print(done, progress)
1492
cb(); return
1493
1494
# fallback
1495
cb("err -- unable to convert files with extension '@ext'")
1496
1497
async.series([show_dialog, convert], (err) =>
1498
if err
1499
msg = "problem printing -- #{misc.to_json(err)}"
1500
alert_message
1501
type : "error"
1502
message : msg
1503
dialog.content.text(msg)
1504
)
1505
1506
# WARNING: this "print" is actually for printing Sage worksheets, not arbitrary files.
1507
print_sagews: =>
1508
dialog = templates.find(".webapp-file-print-dialog").clone()
1509
p = misc.path_split(@filename)
1510
v = p.tail.split('.')
1511
if v.length <= 1
1512
ext = ''
1513
base = p.tail
1514
else
1515
ext = v[v.length-1]
1516
base = v.slice(0,v.length-1).join('.')
1517
1518
ext = ext.toLowerCase()
1519
if ext != 'sagews'
1520
console.error("editor.print called on file with extension '#{ext}' but only supports 'sagews'.")
1521
return
1522
1523
submit = () =>
1524
dialog.find(".webapp-file-printing-progress").show()
1525
dialog.find(".webapp-file-printing-link").hide()
1526
$print_tempdir = dialog.find(".smc-file-printing-tempdir")
1527
$print_tempdir.hide()
1528
is_subdir = dialog.find(".webapp-file-print-keepfiles").is(":checked")
1529
dialog.find(".btn-submit").icon_spin(start:true)
1530
pdf = undefined
1531
async.series([
1532
(cb) =>
1533
@save(cb)
1534
(cb) =>
1535
# get info from the UI and attempt to convert the sagews to pdf
1536
options =
1537
title : dialog.find(".webapp-file-print-title").text()
1538
author : dialog.find(".webapp-file-print-author").text()
1539
date : dialog.find(".webapp-file-print-date").text()
1540
contents : dialog.find(".webapp-file-print-contents").is(":checked")
1541
subdir : is_subdir
1542
base_url : require('./misc_page').BASE_URL
1543
extra_data : misc.to_json(@syncdoc.print_to_pdf_data()) # avoid de/re-json'ing
1544
1545
printing.Printer(@, @filename + '.pdf').print
1546
project_id : @project_id
1547
path : @filename
1548
options : options
1549
cb : (err, _pdf) =>
1550
if err and not is_subdir
1551
cb(err)
1552
else
1553
pdf = _pdf
1554
cb()
1555
(cb) =>
1556
if is_subdir or not pdf?
1557
cb(); return
1558
# does the pdf file exist?
1559
project_tasks(@project_id).file_nonzero_size
1560
path : pdf
1561
cb : (err) =>
1562
if err
1563
err_msg = 'Unable to convert file to PDF. '
1564
if not is_subdir
1565
err_msg += "Enable 'Keep generated files in a sub-directory...' and check for Latex errors."
1566
cb(err_msg)
1567
else
1568
cb()
1569
(cb) =>
1570
if is_subdir or not pdf?
1571
cb(); return
1572
# pdf file exists -- show it in the UI
1573
url = webapp_client.read_file_from_project
1574
project_id : @project_id
1575
path : pdf
1576
dialog.find(".webapp-file-printing-link").attr('href', url).text(pdf).show()
1577
cb()
1578
(cb) =>
1579
if not is_subdir
1580
cb(); return
1581
{join} = require('path')
1582
subdir_texfile = join(p.head, "#{base}-sagews2pdf", "tmp.tex")
1583
# check if generated tmp.tex exists and has nonzero size
1584
project_tasks(@project_id).file_nonzero_size
1585
path : subdir_texfile
1586
cb : (err) =>
1587
if err
1588
cb('Unable to create directory of temporary Latex files.')
1589
else
1590
tempdir_link = $('<a>').text('Click to open temporary file')
1591
tempdir_link.click =>
1592
redux.getProjectActions(@project_id).open_file
1593
path : subdir_texfile
1594
foreground : true
1595
dialog.modal('hide')
1596
return false
1597
$print_tempdir.html(tempdir_link)
1598
$print_tempdir.show()
1599
cb()
1600
(cb) =>
1601
# if there is no subdirectory of temporary files, print generated pdf file
1602
if not is_subdir
1603
redux.getProjectActions(@project_id).print_file(path: pdf)
1604
cb()
1605
], (err) =>
1606
dialog.find(".btn-submit").icon_spin(false)
1607
dialog.find(".webapp-file-printing-progress").hide()
1608
if err
1609
alert_message(type:"error", message:"problem printing '#{p.tail}' -- #{misc.to_json(err)}")
1610
)
1611
return false
1612
1613
dialog.find(".webapp-file-print-filename").text(@filename)
1614
dialog.find(".webapp-file-print-title").text(base)
1615
dialog.find(".webapp-file-print-author").text(redux.getStore('account').get_fullname())
1616
dialog.find(".webapp-file-print-date").text((new Date()).toLocaleDateString())
1617
dialog.find(".btn-submit").click(submit)
1618
dialog.find(".btn-close").click(() -> dialog.modal('hide'); return false)
1619
if ext == "sagews"
1620
dialog.find(".webapp-file-options-sagews").show()
1621
dialog.modal('show')
1622
1623
init_save_button: () =>
1624
@save_button = @element.find("a[href=\"#save\"]").tooltip().click(@click_save_button)
1625
@save_button.find(".spinner").hide()
1626
1627
init_uncommitted_element: () =>
1628
@uncommitted_element = @element.find(".smc-uncommitted")
1629
1630
init_history_button: () =>
1631
if not @opts.public_access and @filename.slice(@filename.length-13) != '.sage-history'
1632
@history_button = @element.find(".webapp-editor-history-button")
1633
@history_button.click(@click_history_button)
1634
@history_button.show()
1635
@history_button.css
1636
display: 'inline-block' # this is needed due to subtleties of jQuery show().
1637
1638
click_save_button: () =>
1639
if @opts.read_only
1640
return
1641
if not @save? # not implemented...
1642
return
1643
if @_saving
1644
return
1645
@_saving = true
1646
@save_button.icon_spin(start:true, delay:8000)
1647
@save (err) =>
1648
# WARNING: As far as I can tell, this doesn't call FileEditor.save
1649
if err
1650
if redux.getProjectStore(@project_id).is_file_open(@filename) # only show error if file actually opened
1651
alert_message(type:"error", message:"Error saving '#{@filename}' (#{err}) -- (you might need to close and open this file or restart this project)")
1652
else
1653
@emit('saved')
1654
@save_button.icon_spin(false)
1655
@_saving = false
1656
return false
1657
1658
click_history_button: () =>
1659
redux.getProjectActions(@project_id).open_file
1660
path : misc.history_path(@filename)
1661
foreground : true
1662
1663
_get: () =>
1664
return @codemirror?.getValue()
1665
1666
_set: (content) =>
1667
if not @codemirror?
1668
# document is already closed and freed up.
1669
return
1670
{from} = @codemirror.getViewport()
1671
@codemirror.setValue(content)
1672
@codemirror.scrollIntoView(from)
1673
# even better -- fully restore cursors, if available in localStorage
1674
setTimeout((()=>@restore_cursor_position()),1) # do in next round, so that both editors get set by codemirror first (including the linked one)
1675
1676
# save/restore view state -- hooks used by React editor wrapper.
1677
save_view_state: =>
1678
state =
1679
scroll : (cm.getScrollInfo() for cm in @codemirrors())
1680
@_view_state = state
1681
return state
1682
1683
restore_view_state: (second_try) =>
1684
state = @_view_state
1685
if not state?
1686
return
1687
cms = @codemirrors()
1688
i = 0
1689
for v in state.scroll
1690
cm = cms[i]
1691
if cm?
1692
cm.scrollTo(v.left, v.top)
1693
info = cm.getScrollInfo()
1694
# THIS IS HORRIBLE and SUCKS, but I can't understand what is going on sufficiently
1695
# well to remove this. Sometimes scrollTo fails (due to the document being reported as much
1696
# smaller than it is for a few ms) **and** it's then not possible to scroll,
1697
# so we just try again. See https://github.com/sagemathinc/cocalc/issues/1327
1698
if not second_try and info.top != v.top
1699
# didn't work -- not fully visible; try again one time when rendering is presumably done.
1700
setTimeout((=>@restore_view_state(true)), 250)
1701
i += 1
1702
1703
restore_cursor_position: () =>
1704
for i, cm of @codemirrors()
1705
if cm?
1706
pos = @local_storage("cursor#{cm.name}")
1707
if pos?
1708
cm.setCursor(pos)
1709
#console.log("#{@filename}: setting view #{cm.name} to cursor pos -- #{misc.to_json(pos)}")
1710
info = cm.getScrollInfo()
1711
try
1712
cm.scrollIntoView(pos, info.clientHeight/2)
1713
catch e
1714
#console.log("#{@filename}: failed to scroll view #{cm.name} into view -- #{e}")
1715
@codemirror?.focus()
1716
1717
# set background color of active line in editor based on background color (which depends on the theme)
1718
_style_active_line: () =>
1719
if not @opts.style_active_line
1720
return
1721
rgb = $(@codemirror.getWrapperElement()).css('background-color')
1722
v = (parseInt(x) for x in rgb.slice(4,rgb.length-1).split(','))
1723
amount = @opts.style_active_line
1724
for i in [0..2]
1725
if v[i] >= 128
1726
v[i] -= amount
1727
else
1728
v[i] += amount
1729
$("body").remove("#webapp-cm-activeline")
1730
$("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!
1731
1732
_show_codemirror_editors: (height) =>
1733
# console.log("_show_codemirror_editors: #{@_layout}")
1734
switch @_layout
1735
when 0
1736
p = 1
1737
when 1
1738
p = @_layout1_split_pos ? 0.5
1739
when 2
1740
p = @_layout2_split_pos ? 0.5
1741
1742
# Change the height of the *top* div that contain the editors; the bottom one then
1743
# uses of all remaining vertical height.
1744
if @_layout > 0
1745
p = Math.max(MIN_SPLIT, Math.min(MAX_SPLIT, p))
1746
1747
# We set only the default size of the *first* div -- everything else expands accordingly.
1748
elt = @element.find(".webapp-editor-codemirror-input-container-layout-#{@_layout}").show()
1749
1750
if @_layout == 1
1751
@element.find(".webapp-editor-resize-bar-layout-1").css(top:0)
1752
else if @_layout == 2
1753
@element.find(".webapp-editor-resize-bar-layout-2").css(left:0)
1754
1755
c = elt.find(".webapp-editor-codemirror-input-box")
1756
if @_layout == 0
1757
c.css('flex', 1) # use the full vertical height
1758
else
1759
c.css('flex-basis', "#{p*100}%")
1760
1761
if @_last_layout != @_layout
1762
# The layout has changed
1763
btn = @element.find('a[href="#split-view"]')
1764
1765
if @_last_layout?
1766
# Hide previous
1767
btn.find(".webapp-editor-layout-#{@_last_layout}").hide()
1768
@element.find(".webapp-editor-codemirror-input-container-layout-#{@_last_layout}").hide()
1769
1770
# Show current
1771
btn.find(".webapp-editor-layout-#{@_layout}").show()
1772
1773
# Put editors in their place -- in the div inside of each box
1774
elt.find(".webapp-editor-codemirror-input-box div").empty().append($(@codemirror.getWrapperElement()))
1775
elt.find(".webapp-editor-codemirror-input-box-1 div").empty().append($(@codemirror1.getWrapperElement()))
1776
1777
# Save for next time
1778
@_last_layout = @_layout
1779
1780
# Workaround a major and annoying bug in Safari:
1781
# https://github.com/philipwalton/flexbugs/issues/132
1782
if $.browser.safari and @_layout == 1
1783
# This is only needed for the "split via a horizontal line" layout, since
1784
# the flex layout with column direction is broken on Safari.
1785
@element.find(".webapp-editor-codemirror-input-container-layout-#{@_layout}").make_height_defined()
1786
1787
refresh = (cm) =>
1788
return if not cm?
1789
cm.refresh()
1790
# See https://github.com/sagemathinc/cocalc/issues/1327#issuecomment-265488872
1791
setTimeout((=>cm.refresh()), 1)
1792
1793
for cm in @codemirrors()
1794
refresh(cm)
1795
1796
@emit('show')
1797
1798
_show: (opts={}) =>
1799
# show the element that contains this editor
1800
#@element.show()
1801
# show the codemirror editors, resizing as needed
1802
@_show_codemirror_editors()
1803
1804
focus: () =>
1805
if not @codemirror?
1806
return
1807
@show()
1808
if not (IS_MOBILE or feature.IS_TOUCH)
1809
@codemirror_with_last_focus?.focus()
1810
1811
############
1812
# Editor button bar support code
1813
############
1814
textedit_command: (cm, cmd, args) =>
1815
# ATTN when adding more cases, also edit textedit_only_show_known_buttons
1816
switch cmd
1817
when "link"
1818
cm.insert_link(cb:() => @syncdoc?.sync())
1819
return false # don't return true or get an infinite recurse
1820
when "image"
1821
cm.insert_image(cb:() => @syncdoc?.sync())
1822
return false # don't return true or get an infinite recurse
1823
when "SpecialChar"
1824
cm.insert_special_char(cb:() => @syncdoc?.sync())
1825
return false # don't return true or get an infinite recurse
1826
else
1827
cm.edit_selection
1828
cmd : cmd
1829
args : args
1830
@syncdoc?.sync()
1831
# needed so that dropdown menu closes when clicked.
1832
return true
1833
1834
examples_dialog_handler: () =>
1835
# @examples_dialog is this ExampleActions object
1836
if not @examples_dialog?
1837
$target = @mode_display.parent().find('.react-target')
1838
{render_examples_dialog} = require('./examples')
1839
@examples_dialog = render_examples_dialog($target[0], @project_id, @filename, lang = @_current_mode, cb = @example_insert_handler)
1840
else
1841
@examples_dialog.show(lang = @_current_mode)
1842
1843
example_insert_handler: (insert) =>
1844
code = insert.code
1845
lang = insert.lang
1846
cm = @focused_codemirror()
1847
line = cm.getCursor().line
1848
# console.log "example insert:", lang, code, insert.descr
1849
if insert.descr?
1850
@syncdoc?.insert_new_cell(line)
1851
cm.replaceRange("%md\n#{insert.descr}", {line : line+1, ch:0})
1852
@action_key(execute: true, advance:false, split:false)
1853
line = cm.getCursor().line
1854
@syncdoc?.insert_new_cell(line)
1855
cell = code
1856
if lang != @_current_mode
1857
cell = "%#{lang}\n#{cell}"
1858
cm.replaceRange(cell, {line : line+1, ch:0})
1859
@action_key(execute: true, advance:false, split:false)
1860
@syncdoc?.sync()
1861
1862
# add a textedit toolbar to the editor
1863
init_sagews_edit_buttons: () =>
1864
if @opts.read_only # no editing button bar needed for read-only files
1865
return
1866
1867
if IS_MOBILE # no edit button bar on mobile either -- too big (for now at least)
1868
return
1869
1870
if not redux.getStore('account').get_editor_settings().extra_button_bar
1871
# explicitly disabled by user
1872
return
1873
1874
NAME_TO_MODE = {xml:'html', markdown:'md', mediawiki:'wiki'}
1875
for x in sagews_decorator_modes
1876
mode = x[0]
1877
name = x[1]
1878
v = name.split('-')
1879
if v.length > 1
1880
name = v[1]
1881
NAME_TO_MODE[name] = "#{mode}"
1882
1883
name_to_mode = (name) ->
1884
n = NAME_TO_MODE[name]
1885
if n?
1886
return n
1887
else
1888
return "#{name}"
1889
1890
# add the text editing button bar
1891
e = @element.find(".webapp-editor-codemirror-textedit-buttons")
1892
@textedit_buttons = templates.find(".webapp-editor-textedit-buttonbar").clone().hide()
1893
e.append(@textedit_buttons).show()
1894
1895
# add the code editing button bar
1896
@codeedit_buttons = templates.find(".webapp-editor-codeedit-buttonbar").clone()
1897
e.append(@codeedit_buttons)
1898
1899
# the r-editing button bar
1900
@redit_buttons = templates.find(".webapp-editor-redit-buttonbar").clone()
1901
e.append(@redit_buttons)
1902
1903
# the Julia-editing button bar
1904
@julia_edit_buttons = templates.find(".webapp-editor-julia-edit-buttonbar").clone()
1905
e.append(@julia_edit_buttons)
1906
1907
# the sh-editing button bar
1908
@sh_edit_buttons = templates.find(".webapp-editor-sh-edit-buttonbar").clone()
1909
e.append(@sh_edit_buttons)
1910
1911
@cython_buttons = templates.find(".webapp-editor-cython-buttonbar").clone()
1912
e.append(@cython_buttons)
1913
1914
@fallback_buttons = templates.find(".webapp-editor-fallback-edit-buttonbar").clone()
1915
e.append(@fallback_buttons)
1916
1917
all_edit_buttons = [@textedit_buttons, @codeedit_buttons, @redit_buttons,
1918
@cython_buttons, @julia_edit_buttons, @sh_edit_buttons, @fallback_buttons]
1919
1920
# activite the buttons in the bar
1921
that = @
1922
edit_button_click = (e) ->
1923
e.preventDefault()
1924
args = $(this).data('args')
1925
cmd = $(this).attr('href').slice(1)
1926
if cmd == 'todo'
1927
return
1928
if args? and typeof(args) != 'object'
1929
args = "#{args}"
1930
if args.indexOf(',') != -1
1931
args = args.split(',')
1932
return that.textedit_command(that.focused_codemirror(), cmd, args)
1933
1934
# FUTURE: activate color editing buttons -- for now just hide them
1935
@element.find(".sagews-output-editor-foreground-color-selector").hide()
1936
@element.find(".sagews-output-editor-background-color-selector").hide()
1937
1938
@fallback_buttons.find('a[href="#todo"]').click () =>
1939
bootbox.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.")
1940
return false
1941
1942
for edit_buttons in all_edit_buttons
1943
edit_buttons.find("a").click(edit_button_click)
1944
edit_buttons.find("*[title]").tooltip(TOOLTIP_DELAY)
1945
1946
@mode_display = mode_display = @element.find(".webapp-editor-codeedit-buttonbar-mode")
1947
@_current_mode = "sage"
1948
@mode_display.show()
1949
1950
# not all textedit buttons are known
1951
textedit_only_show_known_buttons = (name) =>
1952
EDIT_COMMANDS = require('./buttonbar').commands
1953
{sagews_canonical_mode} = require('./misc_page')
1954
default_mode = @focused_codemirror()?.get_edit_mode() ? 'sage'
1955
mode = sagews_canonical_mode(name, default_mode)
1956
#if DEBUG then console.log "textedit_only_show_known_buttons: mode #{name} → #{mode}"
1957
known_commands = misc.keys(EDIT_COMMANDS[mode] ? {})
1958
# see special cases in 'textedit_command' and misc_page: 'edit_selection'
1959
known_commands = known_commands.concat(['link', 'image', 'SpecialChar', 'font_size'])
1960
for button in @textedit_buttons.find('a')
1961
button = $(button)
1962
cmd = button.attr('href').slice(1)
1963
# in theory, this should also be done for html&md, but there are many more special cases
1964
# therefore we just make sure they're all activated again
1965
button.toggle((mode != 'tex') or (cmd in known_commands))
1966
1967
set_mode_display = (name) =>
1968
#console.log("set_mode_display: #{name}")
1969
if name?
1970
mode = name_to_mode(name)
1971
else
1972
mode = ""
1973
mode_display.text("%" + mode)
1974
@_current_mode = mode
1975
1976
show_edit_buttons = (which_one, name) =>
1977
for edit_buttons in all_edit_buttons
1978
edit_buttons.toggle(edit_buttons == which_one)
1979
if which_one == @textedit_buttons
1980
textedit_only_show_known_buttons(name)
1981
set_mode_display(name)
1982
1983
mode_display.click(@examples_dialog_handler)
1984
1985
# The code below changes the bar at the top depending on where the cursor
1986
# is located. We only change the edit bar if the cursor hasn't moved for
1987
# a while, to be more efficient, avoid noise, and be less annoying to the user.
1988
# Replaced by http://underscorejs.org/#debounce
1989
#bar_timeout = undefined
1990
#f = () =>
1991
# if bar_timeout?
1992
# clearTimeout(bar_timeout)
1993
# bar_timeout = setTimeout(update_context_sensitive_bar, 250)
1994
1995
update_context_sensitive_bar = () =>
1996
cm = @focused_codemirror()
1997
if not cm?
1998
return
1999
pos = cm.getCursor()
2000
name = cm.getModeAt(pos).name
2001
#console.log("update_context_sensitive_bar, pos=#{misc.to_json(pos)}, name=#{name}")
2002
if name in ['xml', 'stex', 'markdown', 'mediawiki']
2003
show_edit_buttons(@textedit_buttons, name)
2004
else if name == "r"
2005
show_edit_buttons(@redit_buttons, name)
2006
else if name == "julia"
2007
show_edit_buttons(@julia_edit_buttons, name)
2008
else if name == "cython" # doesn't work yet, since name=python still
2009
show_edit_buttons(@cython_buttons, name)
2010
else if name == "python" # doesn't work yet, since name=python still
2011
show_edit_buttons(@codeedit_buttons, "sage")
2012
else if name == "shell"
2013
show_edit_buttons(@sh_edit_buttons, name)
2014
else
2015
show_edit_buttons(@fallback_buttons, name)
2016
2017
for cm in @codemirrors()
2018
cm.on('cursorActivity', _.debounce(update_context_sensitive_bar, 250))
2019
2020
update_context_sensitive_bar()
2021
@element.find(".webapp-editor-codemirror-textedit-buttons").mathjax()
2022
2023
2024
codemirror_session_editor = exports.codemirror_session_editor = (project_id, filename, extra_opts) ->
2025
#console.log("codemirror_session_editor '#{filename}'")
2026
ext = filename_extension_notilde(filename).toLowerCase()
2027
2028
E = new CodeMirrorEditor(project_id, filename, "", extra_opts)
2029
# Enhance the editor with synchronized session capabilities.
2030
opts =
2031
cursor_interval : E.opts.cursor_interval
2032
sync_interval : E.opts.sync_interval
2033
2034
switch ext
2035
when "sagews"
2036
# temporary.
2037
opts =
2038
cursor_interval : 2000
2039
sync_interval : 250
2040
E.syncdoc = new (sagews.SynchronizedWorksheet)(E, opts)
2041
E.action_key = E.syncdoc.action
2042
E.interrupt_key = E.syncdoc.interrupt
2043
E.tab_nothing_selected = () => E.syncdoc.introspect()
2044
when "sage-history"
2045
# no syncdoc
2046
else
2047
E.syncdoc = new (syncdoc.SynchronizedDocument2)(E, opts)
2048
2049
E.save = E.syncdoc?.save
2050
return E
2051
2052
class Terminal extends FileEditor
2053
constructor: (@project_id, @filename, content, opts) ->
2054
super(@project_id, @filename)
2055
@element = $("<div>").hide()
2056
elt = @element.webapp_console
2057
title : "Terminal"
2058
filename : @filename
2059
project_id : @project_id
2060
path : @filename
2061
editor : @
2062
@console = elt.data("console")
2063
@element = @console.element
2064
webapp_client.read_text_file_from_project
2065
project_id : @project_id
2066
path : @filename
2067
cb : (err, result) =>
2068
if err
2069
alert_message(type:"error", message: "Error connecting to console server -- #{err}")
2070
else
2071
# New session or connect to session
2072
if result.content? and result.content.length < 36
2073
# empty/corrupted -- messed up by bug in early version of SMC...
2074
delete result.content
2075
@opts = defaults opts,
2076
session_uuid : result.content
2077
@connect_to_server()
2078
2079
connect_to_server: (cb) =>
2080
mesg =
2081
timeout : 30 # just for making the connection; not the timeout of the session itself!
2082
type : 'console'
2083
project_id : @project_id
2084
cb : (err, session) =>
2085
if err
2086
alert_message(type:'error', message:err)
2087
cb?(err)
2088
else
2089
if @element.is(":visible")
2090
@show()
2091
@console.set_session(session)
2092
@opts.session_uuid = session.session_uuid
2093
webapp_client.write_text_file_to_project
2094
project_id : @project_id
2095
path : @filename
2096
content : session.session_uuid
2097
cb : cb
2098
2099
path = misc.path_split(@filename).head
2100
mesg.params = {command:'bash', rows:@opts.rows, cols:@opts.cols, path:path, filename:@filename}
2101
if @opts.session_uuid?
2102
mesg.session_uuid = @opts.session_uuid
2103
webapp_client.connect_to_session(mesg)
2104
else
2105
webapp_client.new_session(mesg)
2106
2107
2108
_get: => # FUTURE ??
2109
return @opts.session_uuid ? ''
2110
2111
_set: (content) => # FUTURE ??
2112
2113
save: =>
2114
# DO nothing -- a no-op for now
2115
# FUTURE: Add notion of history
2116
cb?()
2117
2118
focus: =>
2119
@console?.focus()
2120
2121
blur: =>
2122
@console?.blur()
2123
2124
terminate_session: () =>
2125
2126
remove: =>
2127
@element.webapp_console(false)
2128
super()
2129
2130
hide: =>
2131
@console?.blur()
2132
2133
_show: () =>
2134
@console?.resize()
2135
2136
class Media extends FileEditor
2137
constructor: (@project_id, @filename, url, @opts) ->
2138
super(@project_id, @filename)
2139
@mode = if @ext in VIDEO_EXTS then 'video' else 'image'
2140
@element = templates.find(".webapp-editor-image").clone()
2141
@element.find(".webapp-editor-image-title").text(@filename)
2142
2143
refresh = @element.find('a[href="#refresh"]')
2144
refresh.click () =>
2145
refresh.icon_spin(true)
2146
@update (err) =>
2147
refresh.icon_spin(false)
2148
return false
2149
2150
@element.find('a[href="#close"]').click () =>
2151
return false
2152
2153
if url?
2154
@element.find(".webapp-editor-image-container").find("span").hide()
2155
@set_src(url)
2156
else
2157
@update()
2158
2159
set_src: (src) =>
2160
switch @mode
2161
when 'image'
2162
@element.find("img").attr('src', src)
2163
@element.find('video').hide()
2164
when 'video'
2165
@element.find('img').hide()
2166
@element.find('video').attr('src', src).show()
2167
2168
update: (cb) =>
2169
@element.find('a[href="#refresh"]').icon_spin(start:true)
2170
webapp_client.read_file_from_project
2171
project_id : @project_id
2172
timeout : 30
2173
path : @filename
2174
cb : (err, mesg) =>
2175
@element.find('a[href="#refresh"]').icon_spin(false)
2176
@element.find(".webapp-editor-image-container").find("span").hide()
2177
if err
2178
alert_message(type:"error", message:"Communications issue loading #{@filename} -- #{err}")
2179
cb?(err)
2180
else if mesg.event == 'error'
2181
alert_message(type:"error", message:"Error getting #{@filename} -- #{to_json(mesg.error)}")
2182
cb?(mesg.event)
2183
else
2184
@set_src(mesg.url + "?random=#{Math.random()}")
2185
cb?()
2186
2187
show: () =>
2188
if not @is_active()
2189
return
2190
@element.show()
2191
2192
2193
class PublicHTML extends FileEditor
2194
constructor: (@project_id, @filename, @content, opts) ->
2195
super(@project_id, @filename)
2196
@element = templates.find(".webapp-editor-static-html").clone()
2197
# ATTN: we can't set src='raw-path' because the sever might not run.
2198
# therefore we retrieve the content and set it directly.
2199
if not @content?
2200
@content = 'Loading...'
2201
# Now load the content from the backend...
2202
webapp_client.public_get_text_file
2203
project_id : @project_id
2204
path : @filename
2205
timeout : 60
2206
cb : (err, content) =>
2207
if err
2208
@content = "Error opening file -- #{err}"
2209
else
2210
@content = content
2211
if @iframe?
2212
@set_iframe()
2213
2214
show: () =>
2215
if not @is_active()
2216
return
2217
if not @iframe?
2218
# Setting the iframe in the *next* tick is critical on Firefox; otherwise, the browser
2219
# just deletes what we set. I do not claim to fully understand why, but this does work.
2220
# See https://github.com/sagemathinc/cocalc/issues/843
2221
# -- wstein
2222
setTimeout(@set_iframe, 1)
2223
else
2224
@set_iframe()
2225
@element.show()
2226
2227
set_iframe: () =>
2228
@iframe = @element.find(".webapp-editor-static-html-content").find('iframe')
2229
# We do this, since otherwise just loading the iframe using
2230
# @iframe.contents().find('html').html(@content)
2231
# messes up the parent html page...
2232
# ... but setting the innerHTML=@content causes issue 1347!
2233
# A compromise is to set the 'srcdoc' attribute to the content,
2234
# but that doesn't work in IE/Edge -- http://caniuse.com/#search=srcdoc
2235
if $.browser.edge or $.browser.ie
2236
@iframe.contents().find('body').html(@content)
2237
else
2238
@iframe.attr('srcdoc', @content)
2239
@iframe.contents().find('body').find("a").attr('target','_blank')
2240
@iframe.maxheight()
2241
2242
class PublicCodeMirrorEditor extends CodeMirrorEditor
2243
constructor: (@project_id, @filename, content, opts, cb) ->
2244
opts.read_only = true
2245
opts.public_access = true
2246
super(@project_id, @filename, "Loading...", opts)
2247
@element.find('a[href="#save"]').hide() # no need to even put in the button for published
2248
@element.find('a[href="#readonly"]').hide() # ...
2249
webapp_client.public_get_text_file
2250
project_id : @project_id
2251
path : @filename
2252
timeout : 60
2253
cb : (err, content) =>
2254
if err
2255
content = "Error opening file -- #{err}"
2256
@_set(content)
2257
cb?(err)
2258
2259
class PublicSagews extends PublicCodeMirrorEditor
2260
constructor: (@project_id, @filename, content, opts) ->
2261
opts.allow_javascript_eval = false
2262
super @project_id, @filename, content, opts, (err) =>
2263
@element.find('a[href="#split-view"]').hide() # disable split view
2264
if not err
2265
@syncdoc = new (sagews.SynchronizedWorksheet)(@, {static_viewer:true})
2266
@syncdoc.process_sage_updates()
2267
@syncdoc.init_hide_show_gutter()
2268
2269
class FileEditorWrapper extends FileEditor
2270
constructor: (@project_id, @filename, @content, @opts) ->
2271
super(@project_id, @filename)
2272
@init_wrapped(@project_id, @filename, @content, @opts)
2273
2274
init_wrapped: () =>
2275
# Define @element and @wrapped in derived class
2276
throw Error('must define in derived class')
2277
2278
save: (cb) =>
2279
if @wrapped?.save?
2280
@wrapped.save(cb)
2281
else
2282
cb?()
2283
2284
has_unsaved_changes: (val) =>
2285
return @wrapped?.has_unsaved_changes?(val)
2286
2287
has_uncommitted_changes: (val) =>
2288
return @wrapped?.has_uncommitted_changes?(val)
2289
2290
_get: () =>
2291
# FUTURE
2292
return 'history saving not yet implemented'
2293
2294
_set: (content) =>
2295
# FUTURE ???
2296
2297
focus: () =>
2298
2299
terminate_session: () =>
2300
2301
disconnect_from_session: () =>
2302
@wrapped?.destroy?()
2303
2304
remove: () =>
2305
super()
2306
@wrapped?.destroy?()
2307
delete @filename; delete @content; delete @opts
2308
2309
show: () =>
2310
if not @is_active()
2311
return
2312
if not @element?
2313
return
2314
@element.show()
2315
2316
if IS_MOBILE
2317
@element.css(position:'relative')
2318
2319
@wrapped?.show?()
2320
2321
hide: () =>
2322
@element?.hide()
2323
@wrapped?.hide?()
2324
2325
###
2326
# Task list
2327
###
2328
2329
class TaskList extends FileEditorWrapper
2330
init_wrapped: () =>
2331
@element = $("<div><span>&nbsp;&nbsp;Loading...</span></div>")
2332
require.ensure [], () =>
2333
tasks = require('./tasks')
2334
elt = tasks.task_list(@project_id, @filename, {})
2335
@element.replaceWith(elt)
2336
@element = elt
2337
@wrapped = elt.data('task_list')
2338
@show() # need to do this due to async loading -- otherwise once it appears it isn't the right size, which is BAD.
2339
2340
mount: () =>
2341
if not @mounted
2342
$(document.body).append(@element)
2343
@mounted = true
2344
return @mounted
2345
2346
###
2347
# Jupyter notebook
2348
###
2349
jupyter = require('./editor_jupyter')
2350
2351
class JupyterNotebook extends FileEditorWrapper
2352
init_wrapped: () =>
2353
@element = $("<div><span>&nbsp;&nbsp;Loading...</span></div>")
2354
require.ensure [], =>
2355
@init_font_size() # get the @default_font_size
2356
# console.log("JupyterNotebook@default_font_size: #{@default_font_size}")
2357
@opts.default_font_size = @default_font_size
2358
@element = jupyter.jupyter_notebook(@, @filename, @opts)
2359
@wrapped = @element.data('jupyter_notebook')
2360
2361
mount: () =>
2362
if not @mounted
2363
$(document.body).append(@element)
2364
@mounted = true
2365
return @mounted
2366
2367
class JupyterNBViewer extends FileEditorWrapper
2368
init_wrapped: () ->
2369
@element = jupyter.jupyter_nbviewer(@project_id, @filename, @content, @opts)
2370
@wrapped = @element.data('jupyter_nbviewer')
2371
2372
class JupyterNBViewerEmbedded extends FileEditor
2373
# this is like JupyterNBViewer but https://nbviewer.jupyter.org in an iframe
2374
# it's only used for public files and when not part of the project or anonymous
2375
constructor: (@project_id, @filename, @content, opts) ->
2376
super(@project_id, @filename)
2377
@element = $(".smc-jupyter-templates .smc-jupyter-nbviewer").clone()
2378
@init_buttons()
2379
2380
init_buttons: () =>
2381
# code duplication from editor_jupyter/JupyterNBViewer
2382
@element.find('a[href="#copy"]').click () =>
2383
actions = redux.getProjectActions(@project_id)
2384
actions.load_target('files')
2385
actions.set_all_files_unchecked()
2386
actions.set_file_checked(@filename, true)
2387
actions.set_file_action('copy')
2388
return false
2389
2390
@element.find('a[href="#download"]').click () =>
2391
actions = redux.getProjectActions(@project_id)
2392
actions.load_target('files')
2393
actions.set_all_files_unchecked()
2394
actions.set_file_checked(@filename, true)
2395
actions.set_file_action('download')
2396
return false
2397
2398
show: () =>
2399
if not @is_active()
2400
return
2401
if not @iframe?
2402
@iframe = @element.find(".smc-jupyter-nbviewer-content").find('iframe')
2403
{join} = require('path')
2404
ipynb_src = join(window.location.hostname,
2405
window.app_base_url,
2406
@project_id,
2407
'raw',
2408
@filename)
2409
# for testing, set it to a src like this: (smc-in-smc doesn't work for published files, since it
2410
# still requires the user to be logged in with access to the host project)
2411
#ipynb_src = 'cocalc.com/14eed217-2d3c-4975-a381-b69edcb40e0e/raw/scratch/1_notmnist.ipynb'
2412
@iframe.attr('src', "//nbviewer.jupyter.org/urls/#{ipynb_src}")
2413
@element.show()
2414
2415
{HTML_MD_Editor} = require('./editor-html-md/editor-html-md')
2416
html_md_exts = (ext for ext, opts of file_associations when opts.editor == 'html-md')
2417
2418
{LatexEditor} = require('./latex/editor')
2419
2420
exports.register_nonreact_editors = () ->
2421
2422
# Make non-react editors available in react rewrite
2423
reg = require('./editor_react_wrapper').register_nonreact_editor
2424
2425
reg
2426
ext : '' # fallback for any type not otherwise explicitly specified
2427
f : (project_id, path, opts) -> codemirror_session_editor(project_id, path, opts)
2428
is_public : false
2429
2430
# wrapper for registering private and public editors
2431
register = (is_public, cls, extensions) ->
2432
require.ensure [], ->
2433
icon = file_icon_class(extensions[0])
2434
reg
2435
ext : extensions
2436
is_public : is_public
2437
icon : icon
2438
f : (project_id, path, opts) ->
2439
e = new cls(project_id, path, undefined, opts)
2440
if not e.ext?
2441
console.error('You have to call super(@project_id, @filename) in the constructor to properly initialize this FileEditor instance.')
2442
return e
2443
2444
# Editors for private normal editable files.
2445
register(false, HTML_MD_Editor, html_md_exts)
2446
register(false, LatexEditor, ['tex', 'rnw'])
2447
register(false, Terminal, ['term', 'sage-term'])
2448
register(false, Media, ['png', 'jpg', 'jpeg', 'gif', 'svg'].concat(VIDEO_EXTS))
2449
2450
{HistoryEditor} = require('./editor_history')
2451
register(false, HistoryEditor, ['sage-history'])
2452
register(false, TaskList, ['tasks'])
2453
exports.switch_to_ipynb_classic = ->
2454
register(false, JupyterNotebook, ['ipynb'])
2455
2456
# "Editors" for read-only public files
2457
register(true, PublicCodeMirrorEditor, [''])
2458
register(true, PublicHTML, ['html'])
2459
register(true, PublicSagews, ['sagews'])
2460
2461