Editor.coffee 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. define [
  2. "editor/Document"
  3. "undo/UndoManager"
  4. "utils/Modal"
  5. "ace/ace"
  6. "ace/edit_session"
  7. "ace/mode/latex"
  8. "ace/range"
  9. "ace/keyboard/vim"
  10. "ace/keyboard/emacs"
  11. "libs/backbone"
  12. "libs/jquery.storage"
  13. ], (Document, UndoManager, Modal) ->
  14. AceEditor = require("ace/ace")
  15. EditSession = require('ace/edit_session').EditSession
  16. LatexMode = require("ace/mode/latex").Mode
  17. Range = require("ace/range").Range
  18. Vim = require("ace/keyboard/vim").handler
  19. Emacs = require("ace/keyboard/emacs").handler
  20. keybindings = ace: null, vim: Vim, emacs: Emacs
  21. class Editor
  22. templates:
  23. editorPanel: $("#editorPanelTemplate").html()
  24. loadingIndicator: $("#loadingIndicatorTemplate").html()
  25. viewOptions: {flatView:"flatView", splitView:"splitView"}
  26. currentViewState: undefined
  27. compilationErrors: {}
  28. constructor: (@ide) ->
  29. _.extend @, Backbone.Events
  30. @editorPanel = $(@templates.editorPanel)
  31. @ide.mainAreaManager.addArea
  32. identifier: "editor"
  33. element: @editorPanel
  34. @initializeEditor()
  35. @loadingIndicator = $(@templates.loadingIndicator)
  36. @editorPanel.find("#editor").append(@loadingIndicator)
  37. @leftPanel = @editorPanel.find("#leftEditorPanel")
  38. @rightPanel = @editorPanel.find("#rightEditorPanel")
  39. @initSplitView()
  40. @switchToFlatView()
  41. initSplitView: () ->
  42. splitter = @editorPanel.find("#editorSplitter")
  43. options =
  44. spacing_open: 8
  45. spacing_closed: 16
  46. east:
  47. size: "50%"
  48. maskIframesOnResize: true
  49. onresize: () =>
  50. @trigger("resize")
  51. if (state = $.localStorage("layout.editor"))?
  52. options.east = state.east
  53. splitter.layout options
  54. $(window).unload () =>
  55. @_saveSplitterState()
  56. _saveSplitterState: () ->
  57. if $("#editorSplitter").is(":visible")
  58. state = $("#editorSplitter").layout().readState()
  59. eastWidth = state.east.size
  60. percentWidth = eastWidth / $("#editorSplitter").width() * 100 + "%"
  61. state.east.size = percentWidth
  62. $.localStorage("layout.editor", state)
  63. switchToSplitView: () ->
  64. if @currentViewState != @viewOptions.splitView
  65. @currentViewState = @viewOptions.splitView
  66. @leftPanel.prepend(
  67. @editorPanel.find("#editorWrapper")
  68. )
  69. splitter = @editorPanel.find("#editorSplitter")
  70. splitter.show()
  71. @ide.layoutManager.resizeAllSplitters()
  72. switchToFlatView: () ->
  73. if @currentViewState != @viewOptions.flatView
  74. @_saveSplitterState()
  75. @currentViewState = @viewOptions.flatView
  76. @editorPanel.prepend(
  77. @editorPanel.find("#editorWrapper")
  78. )
  79. @editorPanel.find("#editorSplitter").hide()
  80. @aceEditor.resize(true)
  81. showLoading: () ->
  82. delay = 600 # ms
  83. @loading = true
  84. setTimeout ( =>
  85. if @loading
  86. @loadingIndicator.show()
  87. ), delay
  88. hideLoading: () ->
  89. @loading = false
  90. @loadingIndicator.hide()
  91. showUndoConflictWarning: () ->
  92. $("#editor").prepend($("#undoConflictWarning"))
  93. $("#undoConflictWarning").show()
  94. hideBtn = $("#undoConflictWarning .js-hide")
  95. hideBtn.off("click")
  96. hideBtn.on "click", (e) ->
  97. e.preventDefault()
  98. $("#undoConflictWarning").hide()
  99. if @hideUndoWarningTimeout?
  100. clearTimeout @hideUndoWarningTimeout
  101. delete @hideUndoWarningTimeout
  102. @hideUndoWarningTimeout = setTimeout ->
  103. $("#undoConflictWarning").fadeOut("slow")
  104. , 4000
  105. initializeEditor: () ->
  106. @aceEditor = aceEditor = AceEditor.edit("editor")
  107. @on "resize", => @aceEditor.resize()
  108. @ide.layoutManager.on "resize", => @trigger "resize"
  109. mode = window.userSettings.mode
  110. theme = window.userSettings.theme
  111. chosenKeyBindings = keybindings[mode]
  112. aceEditor.setKeyboardHandler(chosenKeyBindings)
  113. aceEditor.setTheme("ace/theme/#{window.userSettings.theme}")
  114. aceEditor.setShowPrintMargin(false)
  115. # Prevert Ctrl|Cmd-S from triggering save dialog
  116. aceEditor.commands.addCommand
  117. name: "save",
  118. bindKey: win: "Ctrl-S", mac: "Command-S"
  119. exec: () ->
  120. readOnly: true
  121. aceEditor.commands.removeCommand "transposeletters"
  122. aceEditor.commands.removeCommand "showSettingsMenu"
  123. aceEditor.commands.removeCommand "foldall"
  124. aceEditor.showCommandLine = (args...) =>
  125. @trigger "showCommandLine", aceEditor, args...
  126. aceEditor.on "dblclick", (e) => @trigger "dblclick", e
  127. aceEditor.on "click", (e) => @trigger "click", e
  128. aceEditor.on "mousemove", (e) =>
  129. position = @aceEditor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
  130. e.position = position
  131. @trigger "mousemove", e
  132. setIdeToEditorPanel: (options = {}) ->
  133. @aceEditor.focus()
  134. @aceEditor.resize()
  135. loadDocument = =>
  136. @refreshCompilationErrors()
  137. @ide.layoutManager.resizeAllSplitters()
  138. if options.line?
  139. @gotoLine(options.line)
  140. else
  141. pos = $.localStorage("doc.position.#{@current_doc_id}") || {}
  142. @ignoreCursorPositionChanges = true
  143. @setCursorPosition(pos.cursorPosition or {row: 0, column: 0})
  144. @setScrollTop(pos.scrollTop or 0)
  145. @ignoreCursorPositionChanges = false
  146. @ide.mainAreaManager.change 'editor', =>
  147. setTimeout loadDocument, 0
  148. refreshCompilationErrors: () ->
  149. @getSession().setAnnotations @compilationErrors[@current_doc_id]
  150. openDoc: (doc_id, options = {}) ->
  151. if @current_doc_id == doc_id && !options.forceReopen
  152. @setIdeToEditorPanel(line: options.line)
  153. else
  154. @showLoading()
  155. @current_doc_id = doc_id
  156. @_openNewDocument doc_id, (error, document) =>
  157. if error?
  158. @ide.showGenericServerErrorMessage()
  159. return
  160. @setIdeToEditorPanel(line: options.line)
  161. @hideLoading()
  162. @trigger "change:doc", @getSession()
  163. _openNewDocument: (doc_id, callback = (error, document) ->) ->
  164. if @document?
  165. @document.leaveAndCleanUp()
  166. @_unbindFromDocumentEvents(@document)
  167. @_detachDocumentFromEditor(@document)
  168. @document = Document.getDocument @ide, doc_id
  169. @document.join (error) =>
  170. return callback(error) if error?
  171. @_bindToDocumentEvents(@document)
  172. @_bindDocumentToEditor(@document)
  173. callback null, @document
  174. _bindToDocumentEvents: (document) ->
  175. document.on "op:sent", () =>
  176. @ide.savingAreaManager.saving()
  177. document.on "op:acknowledged", () =>
  178. @ide.savingAreaManager.saved()
  179. document.on "remoteop", () =>
  180. @undoManager.nextUpdateIsRemote = true
  181. document.on "error", (error) =>
  182. @openDoc(document.doc_id, forceReopen: true)
  183. Modal.createModal
  184. title: "Out of sync"
  185. message: "Sorry, this file has gone out of sync and we need to do a full refresh. Please let us know if this happens frequently."
  186. buttons:[
  187. text: "Ok"
  188. ]
  189. document.on "externalUpdate", () =>
  190. Modal.createModal
  191. title: "Document Updated Externally"
  192. message: "This document was just updated externally. Any recent changes you have made may have been overwritten. To see previous versions please look in the history."
  193. buttons:[
  194. text: "Ok"
  195. ]
  196. _unbindFromDocumentEvents: (document) ->
  197. document.off()
  198. _bindDocumentToEditor: (document) ->
  199. $("#editor").show()
  200. @_bindNewDocToAce(document)
  201. _detachDocumentFromEditor: (document) ->
  202. document.detachFromAce()
  203. _bindNewDocToAce: (document) ->
  204. @_createNewSessionFromDocLines(document.getSnapshot().split("\n"))
  205. @_setReadWritePermission()
  206. @_bindToAceEvents()
  207. # Updating the doc can cause the cursor to jump around
  208. # but we shouldn't record that
  209. @ignoreCursorPositionChanges = true
  210. document.attachToAce(@aceEditor)
  211. @ignoreCursorPositionChanges = false
  212. _bindToAceEvents: () ->
  213. aceDoc = @getDocument()
  214. aceDoc.on 'change', (change) => @onDocChange(change)
  215. session = @getSession()
  216. session.on "changeScrollTop", (e) => @onScrollTopChange(e)
  217. session.selection.on 'changeCursor', (e) => @onCursorChange(e)
  218. _createNewSessionFromDocLines: (docLines) ->
  219. @aceEditor.setSession(new EditSession(docLines))
  220. session = @getSession()
  221. session.setUseWrapMode(true)
  222. session.setMode(new LatexMode())
  223. @undoManager = new UndoManager(@)
  224. session.setUndoManager @undoManager
  225. _setReadWritePermission: () ->
  226. if !@ide.isAllowedToDoIt 'readAndWrite'
  227. @makeReadOnly()
  228. else
  229. @makeWritable()
  230. onDocChange: (change) ->
  231. @lastUpdated = new Date()
  232. @trigger "update:doc", change
  233. onScrollTopChange: (event) ->
  234. @trigger "scroll", event
  235. if !@ignoreCursorPositionChanges
  236. docPosition = $.localStorage("doc.position.#{@current_doc_id}") || {}
  237. docPosition.scrollTop = @getScrollTop()
  238. $.localStorage("doc.position.#{@current_doc_id}", docPosition)
  239. onCursorChange: (event) ->
  240. if !@ignoreCursorPositionChanges
  241. docPosition = $.localStorage("doc.position.#{@current_doc_id}") || {}
  242. docPosition.cursorPosition = @getCursorPosition()
  243. $.localStorage("doc.position.#{@current_doc_id}", docPosition)
  244. makeReadOnly: () ->
  245. @aceEditor.setReadOnly true
  246. makeWritable: () ->
  247. @aceEditor.setReadOnly false
  248. getSession: () -> @aceEditor.getSession()
  249. getDocument: () -> @getSession().getDocument()
  250. gotoLine: (line) ->
  251. @aceEditor.gotoLine(line)
  252. getLines: (from, to) ->
  253. if from? and to?
  254. @getSession().doc.getLines(from, to)
  255. else
  256. @getSession().doc.getAllLines()
  257. addMarker: (position, klass, type, inFront) ->
  258. range = new Range(
  259. position.row, position.column,
  260. position.row, position.column + position.length
  261. )
  262. @getSession().addMarker range, klass, type, inFront
  263. removeMarker: (markerId) ->
  264. @getSession().removeMarker markerId
  265. getCursorPosition: () -> @aceEditor.getCursorPosition()
  266. setCursorPosition: (pos) -> @aceEditor.moveCursorToPosition(pos)
  267. getScrollTop: () -> @getSession().getScrollTop()
  268. setScrollTop: (pos) -> @getSession().setScrollTop(pos)
  269. replaceText: (range, text) ->
  270. @getSession().replace(new Range(
  271. range.start.row, range.start.column,
  272. range.end.row, range.end.column
  273. ), text)
  274. getContainerElement: () ->
  275. $(@aceEditor.renderer.getContainerElement())
  276. textToEditorCoordinates: (x, y) ->
  277. editorAreaOffset = @getContainerElement().offset()
  278. {pageX, pageY} = @aceEditor.renderer.textToScreenCoordinates(x, y)
  279. return {
  280. x: pageX - editorAreaOffset.left
  281. y: pageY - editorAreaOffset.top
  282. }
  283. chaosMonkey: (line = 0, char = "a") ->
  284. @_cm = setInterval () =>
  285. @aceEditor.session.insert({row: line, column: 0}, char)
  286. , 100
  287. clearChaosMonkey: () ->
  288. clearInterval @_cm
  289. getCurrentDocId: () ->
  290. @current_doc_id
  291. show: () ->
  292. $("#editor").show()
  293. hide: () ->
  294. $("#editor").hide()