PdfManager.coffee 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. define [
  2. "utils/Modal"
  3. "pdf/CompiledView"
  4. "pdf/SyncButtonsView"
  5. "libs/latex-log-parser"
  6. "libs/jquery.storage"
  7. "libs/underscore"
  8. "libs/backbone"
  9. ], (Modal, CompiledView, SyncButtonsView, LogParser) ->
  10. class PdfManager
  11. templates:
  12. pdfLink: $("#pdfSideBarLinkTemplate").html()
  13. constructor: (@ide) ->
  14. _.extend @, Backbone.Events
  15. @createSyncButtons()
  16. @createPdfPanel()
  17. @ide.editor.aceEditor.commands.addCommand
  18. name: "compile",
  19. bindKey: win: "Ctrl-Enter", mac: "Command-Enter"
  20. exec: (editor) =>
  21. @refreshPdf()
  22. readOnly: true
  23. @ide.editor.aceEditor.commands.removeCommand "replace"
  24. createPdfPanel: () ->
  25. @view = new CompiledView manager: @, ide: @ide
  26. @view.on "dblclick", (e) => @syncToCode(e)
  27. @view.render()
  28. if $.localStorage("layout.pdf") == "flat"
  29. @switchToFlatView()
  30. else if $.localStorage("layout.pdf") == "split"
  31. @switchToSplitView()
  32. else if $(window).width() < 1024
  33. @switchToFlatView()
  34. else
  35. @switchToSplitView()
  36. createSyncButtons: () ->
  37. unless @ide.userSettings.pdfViewer == "native"
  38. @syncButtonsView = new SyncButtonsView(ide: @ide)
  39. @syncButtonsView.on "click:sync-code-to-pdf", () =>
  40. @syncToPdf()
  41. @syncButtonsView.on "click:sync-pdf-to-code", () =>
  42. @syncToCode()
  43. @syncButtonsView.hide()
  44. switchToFlatView: (options = {showPdf: false}) ->
  45. @teardownSplitView()
  46. @setupFlatView()
  47. @view.toggleFlatViewButton()
  48. if options.showPdf
  49. @ide.sideBarView.selectLink "pdf"
  50. @ide.mainAreaManager.change "pdf"
  51. @view.resize()
  52. switchToSplitView: () ->
  53. @teardownFlatView()
  54. @setupSplitView()
  55. @view.toggleSplitViewButton()
  56. @view.resize()
  57. @ide.editor.setIdeToEditorPanel()
  58. setupFlatView: () ->
  59. @teardownFlatView()
  60. @ide.editor.switchToFlatView()
  61. pdfLink = $(@templates.pdfLink)
  62. @ide.sideBarView.addLink
  63. identifier : "pdf"
  64. before : "history"
  65. element : pdfLink
  66. pdfLink.on "click", (e) => @showPdfPanel()
  67. @ide.mainAreaManager.addArea
  68. identifier: "pdf"
  69. element: @view.$el
  70. @view.resize()
  71. @view.undelegateEvents()
  72. @view.delegateEvents()
  73. teardownFlatView: () ->
  74. @ide.sideBarView.removeLink("pdf")
  75. @ide.mainAreaManager.removeArea("pdf")
  76. @view.afterSwitchView()
  77. setupSplitView: () ->
  78. @ide.editor.switchToSplitView()
  79. @ide.editor.rightPanel.append(
  80. @view.$el
  81. )
  82. @view.$el.show()
  83. @view.resize()
  84. @view.undelegateEvents()
  85. @view.delegateEvents()
  86. @ide.editor.$splitter.append(
  87. @syncButtonsView?.$el
  88. )
  89. setTimeout(@ide.layoutManager.resizeAllSplitters, 100)
  90. teardownSplitView: () ->
  91. @view.afterSwitchView()
  92. showPdfPanel: () ->
  93. @ide.sideBarView.selectLink 'pdf'
  94. @ide.mainAreaManager.change 'pdf'
  95. @view.resize()
  96. if !@view.hasPdf()
  97. @refreshPdf()
  98. showRawLogPanel: () ->
  99. @ide.mainAreaManager.change 'rawLog'
  100. refreshPdf: (opts) ->
  101. if @ide.project?
  102. @_refreshPdfWhenProjectIsLoaded(opts)
  103. else
  104. @ide.on "afterJoinProject", () =>
  105. @_refreshPdfWhenProjectIsLoaded(opts)
  106. _refreshPdfWhenProjectIsLoaded: (opts = {}) ->
  107. doneCompiling = _.once =>
  108. @compiling = false
  109. @view.doneCompiling()
  110. @syncButtonsView?.show()
  111. setTimeout doneCompiling, 1000 * 60
  112. if !@ide.project.get("rootDoc_id")?
  113. new Modal
  114. title: "No root document selected"
  115. message: "First you need to choose a root document via the settings menu. This tells ShareLaTeX which file to run LaTeX on."
  116. buttons: [{
  117. text: "OK",
  118. class: "btn-primary"
  119. }]
  120. else if !@compiling
  121. @view.onCompiling()
  122. @syncButtonsView?.hide()
  123. @compiling = true
  124. @_doCompile opts, (error, status, outputFiles) =>
  125. @compiling = false
  126. doneCompiling()
  127. if error?
  128. @view.updateLog(systemError: true)
  129. @view.unsetPdf()
  130. @view.showLog()
  131. else if status == "timedout"
  132. @view.updateLog(timedOut: true)
  133. @view.unsetPdf()
  134. @view.showLog()
  135. else if status == "autocompile-backoff"
  136. @view.showBeforeCompile()
  137. else
  138. pdfExists = (status == "success")
  139. @fetchLogAndUpdateView(pdfExists)
  140. if pdfExists
  141. @view.setPdf("/project/#{@ide.project_id}/output/output.pdf?cache_bust=#{Date.now()}")
  142. @view.showPdf()
  143. else
  144. @view.unsetPdf()
  145. @view.showLog()
  146. @syncButtonsView?.hide()
  147. if outputFiles?
  148. @view.showOutputFileDownloadLinks(outputFiles)
  149. _doCompile: (opts, callback = (error, status, outputFiles) ->) ->
  150. url = "/project/#{@ide.project_id}/compile"
  151. if opts.isAutoCompile
  152. url += "?auto_compile=true"
  153. $.ajax(
  154. url: url
  155. type: "POST"
  156. headers:
  157. "X-CSRF-Token": window.csrfToken
  158. contentType: "application/json; charset=utf-8"
  159. dataType: 'json'
  160. data: JSON.stringify settingsOverride:
  161. rootDoc_id: opts.rootDocOverride_id ? null
  162. success: (body, status, response) ->
  163. callback null, body.status, body.outputFiles
  164. error: (error) ->
  165. callback error
  166. )
  167. fetchLogAndUpdateView: (pdfExists) ->
  168. $.ajax(
  169. url: "/project/#{@ide.project_id}/output/output.log"
  170. success: (body, status, response) =>
  171. @parseLogAndUpdateView(pdfExists, body)
  172. error: () =>
  173. @view.updateLog(pdfExists: pdfExists, logExists: false)
  174. )
  175. parseLogAndUpdateView: (pdfExists, log) ->
  176. errors = LogParser.parse(log, ignoreDuplicates: true)
  177. lastCompileErrors = {}
  178. for error in errors.all
  179. error.file = @_normalizeFilePath(error.file)
  180. doc_id = @ide.fileTreeManager.getDocIdOfPath(error.file)
  181. if doc_id?
  182. lastCompileErrors[doc_id] ||= []
  183. lastCompileErrors[doc_id].push
  184. row: error.line - 1
  185. type: if error.level == "error" then "error" else "warning"
  186. text: error.message
  187. @ide.editor.compilationErrors = lastCompileErrors
  188. @ide.editor.refreshCompilationErrors()
  189. @view.updateLog(pdfExists: pdfExists, logExists: true, compileErrors: errors, rawLog: log)
  190. _normalizeFilePath: (path) ->
  191. path = path.replace(/^compiles\/[0-9a-f]{32}\/(\.\/)?/, "")
  192. path = path.replace(/^\/compile\//, "")
  193. rootDoc_id = @ide.project.get("rootDoc_id")
  194. if rootDoc_id?
  195. rootDocPath = @ide.fileTreeManager.getPathOfEntityId(rootDoc_id)
  196. if rootDocPath?
  197. rootDocDir = rootDocPath.split("/").slice(0,-1).map( (part) -> part + "/" ).join("")
  198. path = path.replace(/^\.\//, rootDocDir)
  199. return path
  200. downloadPdf: () ->
  201. @ide.mainAreaManager.setIframeSrc "/project/#{@ide.project_id}/output/output.pdf?popupDownload=true"
  202. deleteCachedFiles: () ->
  203. modal = new Modal
  204. title: "Clear cache?"
  205. message: "This will clear all hidden LaTeX files like .aux, .bbl, etc, from our compile server. You generally don't need to do this unless you're having trouble with references. Your project files will not be deleted or changed."
  206. buttons: [{
  207. text: "Cancel"
  208. }, {
  209. text: "Clear from cache",
  210. class: "btn-primary",
  211. close: false
  212. callback: ($button) =>
  213. $button.text("Clearing...")
  214. $button.prop("disabled", true)
  215. $.ajax({
  216. url: "/project/#{@ide.project_id}/output"
  217. type: "DELETE"
  218. headers:
  219. "X-CSRF-Token": window.csrfToken
  220. complete: () -> modal.remove()
  221. })
  222. }]
  223. syncToCode: (e) ->
  224. if !e?
  225. e = @view.getPdfPosition()
  226. return if !e?
  227. # It's not clear exactly where we should sync to if it was directly
  228. # clicked on, but a little bit down from the very top seems best.
  229. e.y = e.y + 80
  230. $.ajax {
  231. url: "/project/#{@ide.project_id}/sync/pdf"
  232. data:
  233. page: e.page + 1
  234. h: e.x.toFixed(2)
  235. v: e.y.toFixed(2)
  236. type: "GET"
  237. success: (response) =>
  238. data = JSON.parse(response)
  239. if data.code and data.code.length > 0
  240. file = data.code[0].file
  241. line = data.code[0].line
  242. @ide.fileTreeManager.openDocByPath(file, line)
  243. }
  244. syncToPdf: () ->
  245. entity_id = @ide.editor.getCurrentDocId()
  246. file = @ide.fileTreeManager.getPathOfEntityId(entity_id)
  247. # If the root file is folder/main.tex, then synctex sees the
  248. # path as folder/./main.tex
  249. rootFolderPath = @ide.fileTreeManager.getRootFolderPath()
  250. if rootFolderPath != ""
  251. file = file.replace(RegExp("^#{rootFolderPath}"), "#{rootFolderPath}/.")
  252. line = @ide.editor.getCurrentLine()
  253. column = @ide.editor.getCurrentColumn()
  254. $.ajax {
  255. url: "/project/#{@ide.project_id}/sync/code"
  256. data:
  257. file: file
  258. line: line + 1
  259. column: column
  260. type: "GET"
  261. success: (response) =>
  262. data = JSON.parse(response)
  263. @view.highlightInPdf(data.pdf or [])
  264. }