Editor.coffee 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  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. @bindToFileTreeEvents()
  36. @enable()
  37. @loadingIndicator = $(@templates.loadingIndicator)
  38. @editorPanel.find("#editor").append(@loadingIndicator)
  39. @leftPanel = @editorPanel.find("#leftEditorPanel")
  40. @rightPanel = @editorPanel.find("#rightEditorPanel")
  41. @initSplitView()
  42. @switchToFlatView()
  43. bindToFileTreeEvents: () ->
  44. @ide.fileTreeManager.on "open:doc", (doc_id, options = {}) =>
  45. if @enabled
  46. @openDoc doc_id, options
  47. initSplitView: () ->
  48. @$splitter = splitter = @editorPanel.find("#editorSplitter")
  49. options =
  50. spacing_open: 8
  51. spacing_closed: 16
  52. east:
  53. size: "50%"
  54. maskIframesOnResize: true
  55. onresize: () =>
  56. @trigger("resize")
  57. if (state = $.localStorage("layout.editor"))?
  58. options.east = state.east
  59. splitter.layout options
  60. $(window).unload () =>
  61. @_saveSplitterState()
  62. _saveSplitterState: () ->
  63. if $("#editorSplitter").is(":visible")
  64. state = $("#editorSplitter").layout().readState()
  65. eastWidth = state.east.size + $("#editorSplitter .ui-layout-resizer-east").width()
  66. percentWidth = eastWidth / $("#editorSplitter").width() * 100 + "%"
  67. state.east.size = percentWidth
  68. $.localStorage("layout.editor", state)
  69. switchToSplitView: () ->
  70. if @currentViewState != @viewOptions.splitView
  71. @currentViewState = @viewOptions.splitView
  72. @leftPanel.prepend(
  73. @editorPanel.find("#editorWrapper")
  74. )
  75. splitter = @editorPanel.find("#editorSplitter")
  76. splitter.show()
  77. @ide.layoutManager.resizeAllSplitters()
  78. switchToFlatView: () ->
  79. if @currentViewState != @viewOptions.flatView
  80. @_saveSplitterState()
  81. @currentViewState = @viewOptions.flatView
  82. @editorPanel.prepend(
  83. @editorPanel.find("#editorWrapper")
  84. )
  85. @editorPanel.find("#editorSplitter").hide()
  86. @aceEditor.resize(true)
  87. showLoading: () ->
  88. delay = 600 # ms
  89. @loading = true
  90. setTimeout ( =>
  91. if @loading
  92. @loadingIndicator.show()
  93. ), delay
  94. hideLoading: () ->
  95. @loading = false
  96. @loadingIndicator.hide()
  97. showUndoConflictWarning: () ->
  98. $("#editor").prepend($("#undoConflictWarning"))
  99. $("#undoConflictWarning").show()
  100. hideBtn = $("#undoConflictWarning .js-hide")
  101. hideBtn.off("click")
  102. hideBtn.on "click", (e) ->
  103. e.preventDefault()
  104. $("#undoConflictWarning").hide()
  105. if @hideUndoWarningTimeout?
  106. clearTimeout @hideUndoWarningTimeout
  107. delete @hideUndoWarningTimeout
  108. @hideUndoWarningTimeout = setTimeout ->
  109. $("#undoConflictWarning").fadeOut("slow")
  110. , 4000
  111. initializeEditor: () ->
  112. @aceEditor = aceEditor = AceEditor.edit("editor")
  113. @on "resize", => @aceEditor.resize()
  114. @ide.layoutManager.on "resize", => @trigger "resize"
  115. mode = window.userSettings.mode
  116. theme = window.userSettings.theme
  117. chosenKeyBindings = keybindings[mode]
  118. aceEditor.setKeyboardHandler(chosenKeyBindings)
  119. aceEditor.setTheme("ace/theme/#{window.userSettings.theme}")
  120. aceEditor.setShowPrintMargin(false)
  121. # Prevert Ctrl|Cmd-S from triggering save dialog
  122. aceEditor.commands.addCommand
  123. name: "save",
  124. bindKey: win: "Ctrl-S", mac: "Command-S"
  125. exec: () ->
  126. readOnly: true
  127. aceEditor.commands.removeCommand "transposeletters"
  128. aceEditor.commands.removeCommand "showSettingsMenu"
  129. aceEditor.commands.removeCommand "foldall"
  130. aceEditor.showCommandLine = (args...) =>
  131. @trigger "showCommandLine", aceEditor, args...
  132. aceEditor.on "dblclick", (e) => @trigger "dblclick", e
  133. aceEditor.on "click", (e) => @trigger "click", e
  134. aceEditor.on "mousemove", (e) =>
  135. position = @aceEditor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
  136. e.position = position
  137. @trigger "mousemove", e
  138. setIdeToEditorPanel: (options = {}) ->
  139. @aceEditor.focus()
  140. @aceEditor.resize()
  141. loadDocument = =>
  142. @refreshCompilationErrors()
  143. @ide.layoutManager.resizeAllSplitters()
  144. if options.line?
  145. @gotoLine(options.line)
  146. else
  147. pos = $.localStorage("doc.position.#{@current_doc_id}") || {}
  148. @ignoreCursorPositionChanges = true
  149. @setCursorPosition(pos.cursorPosition or {row: 0, column: 0})
  150. @setScrollTop(pos.scrollTop or 0)
  151. @ignoreCursorPositionChanges = false
  152. @ide.mainAreaManager.change 'editor', =>
  153. setTimeout loadDocument, 0
  154. refreshCompilationErrors: () ->
  155. @getSession().setAnnotations @compilationErrors[@current_doc_id]
  156. openDoc: (doc_id, options = {}) ->
  157. if @current_doc_id == doc_id && !options.forceReopen
  158. @setIdeToEditorPanel(line: options.line)
  159. else
  160. @showLoading()
  161. @current_doc_id = doc_id
  162. @_openNewDocument doc_id, (error, document) =>
  163. if error?
  164. @ide.showGenericServerErrorMessage()
  165. return
  166. @setIdeToEditorPanel(line: options.line)
  167. @hideLoading()
  168. @trigger "change:doc", @getSession()
  169. _openNewDocument: (doc_id, callback = (error, document) ->) ->
  170. if @document?
  171. @document.leaveAndCleanUp()
  172. @_unbindFromDocumentEvents(@document)
  173. @_detachDocumentFromEditor(@document)
  174. @document = Document.getDocument @ide, doc_id
  175. @document.join (error) =>
  176. return callback(error) if error?
  177. @_bindToDocumentEvents(@document)
  178. @_bindDocumentToEditor(@document)
  179. callback null, @document
  180. _bindToDocumentEvents: (document) ->
  181. document.on "remoteop", () =>
  182. @undoManager.nextUpdateIsRemote = true
  183. document.on "error", (error) =>
  184. @openDoc(document.doc_id, forceReopen: true)
  185. Modal.createModal
  186. title: "Out of sync"
  187. 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."
  188. buttons:[
  189. text: "Ok"
  190. ]
  191. document.on "externalUpdate", () =>
  192. Modal.createModal
  193. title: "Document Updated Externally"
  194. 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."
  195. buttons:[
  196. text: "Ok"
  197. ]
  198. _unbindFromDocumentEvents: (document) ->
  199. document.off()
  200. _bindDocumentToEditor: (document) ->
  201. $("#editor").show()
  202. @_bindNewDocToAce(document)
  203. _detachDocumentFromEditor: (document) ->
  204. document.detachFromAce()
  205. _bindNewDocToAce: (document) ->
  206. @_createNewSessionFromDocLines(document.getSnapshot().split("\n"))
  207. @_setReadWritePermission()
  208. @_bindToAceEvents()
  209. # Updating the doc can cause the cursor to jump around
  210. # but we shouldn't record that
  211. @ignoreCursorPositionChanges = true
  212. document.attachToAce(@aceEditor)
  213. @ignoreCursorPositionChanges = false
  214. _bindToAceEvents: () ->
  215. aceDoc = @getDocument()
  216. aceDoc.on 'change', (change) => @onDocChange(change)
  217. session = @getSession()
  218. session.on "changeScrollTop", (e) => @onScrollTopChange(e)
  219. session.selection.on 'changeCursor', (e) => @onCursorChange(e)
  220. _createNewSessionFromDocLines: (docLines) ->
  221. @aceEditor.setSession(new EditSession(docLines))
  222. session = @getSession()
  223. session.setUseWrapMode(true)
  224. session.setMode(new LatexMode())
  225. @undoManager = new UndoManager(@)
  226. session.setUndoManager @undoManager
  227. _setReadWritePermission: () ->
  228. if !@ide.isAllowedToDoIt 'readAndWrite'
  229. @makeReadOnly()
  230. else
  231. @makeWritable()
  232. onDocChange: (change) ->
  233. @lastUpdated = new Date()
  234. @trigger "update:doc", change
  235. onScrollTopChange: (event) ->
  236. @trigger "scroll", event
  237. if !@ignoreCursorPositionChanges
  238. docPosition = $.localStorage("doc.position.#{@current_doc_id}") || {}
  239. docPosition.scrollTop = @getScrollTop()
  240. $.localStorage("doc.position.#{@current_doc_id}", docPosition)
  241. onCursorChange: (event) ->
  242. @trigger "cursor:change", event
  243. if !@ignoreCursorPositionChanges
  244. docPosition = $.localStorage("doc.position.#{@current_doc_id}") || {}
  245. docPosition.cursorPosition = @getCursorPosition()
  246. $.localStorage("doc.position.#{@current_doc_id}", docPosition)
  247. makeReadOnly: () ->
  248. @aceEditor.setReadOnly true
  249. makeWritable: () ->
  250. @aceEditor.setReadOnly false
  251. getSession: () -> @aceEditor.getSession()
  252. getDocument: () -> @getSession().getDocument()
  253. gotoLine: (line) ->
  254. @aceEditor.gotoLine(line)
  255. getCurrentLine: () ->
  256. @aceEditor.selection?.getCursor()?.row
  257. getCurrentColumn: () ->
  258. @aceEditor.selection?.getCursor()?.column
  259. getLines: (from, to) ->
  260. if from? and to?
  261. @getSession().doc.getLines(from, to)
  262. else
  263. @getSession().doc.getAllLines()
  264. addMarker: (position, klass, type, inFront) ->
  265. range = new Range(
  266. position.row, position.column,
  267. position.row, position.column + position.length
  268. )
  269. @getSession().addMarker range, klass, type, inFront
  270. removeMarker: (markerId) ->
  271. @getSession().removeMarker markerId
  272. getCursorPosition: () -> @aceEditor.getCursorPosition()
  273. setCursorPosition: (pos) -> @aceEditor.moveCursorToPosition(pos)
  274. getScrollTop: () -> @getSession().getScrollTop()
  275. setScrollTop: (pos) -> @getSession().setScrollTop(pos)
  276. replaceText: (range, text) ->
  277. @getSession().replace(new Range(
  278. range.start.row, range.start.column,
  279. range.end.row, range.end.column
  280. ), text)
  281. getContainerElement: () ->
  282. $(@aceEditor.renderer.getContainerElement())
  283. getCursorElement: () ->
  284. @getContainerElement().find(".ace_cursor")
  285. textToEditorCoordinates: (x, y) ->
  286. editorAreaOffset = @getContainerElement().offset()
  287. {pageX, pageY} = @aceEditor.renderer.textToScreenCoordinates(x, y)
  288. return {
  289. x: pageX - editorAreaOffset.left
  290. y: pageY - editorAreaOffset.top
  291. }
  292. chaosMonkey: (line = 0, char = "a") ->
  293. @_cm = setInterval () =>
  294. @aceEditor.session.insert({row: line, column: 0}, char)
  295. , 100
  296. clearChaosMonkey: () ->
  297. clearInterval @_cm
  298. getCurrentDocId: () ->
  299. @current_doc_id
  300. enable: () ->
  301. @enabled = true
  302. disable: () ->
  303. @enabled = false
  304. hasUnsavedChanges: () ->
  305. Document.hasUnsavedChanges()