PdfManager.coffee 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  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. if !@ide.project.get("rootDoc_id")?
  108. new Modal
  109. title: "No root document selected"
  110. message: "First you need to choose a root document via the settings menu. This tells ShareLaTeX which file to run LaTeX on."
  111. buttons: [{
  112. text: "OK",
  113. class: "btn-primary"
  114. }]
  115. else if !@compiling
  116. @view.onCompiling()
  117. @syncButtonsView?.hide()
  118. @compiling = true
  119. @_doCompile opts, (error, status, outputFiles) =>
  120. @compiling = false
  121. @view.doneCompiling()
  122. @syncButtonsView?.show()
  123. if error?
  124. @view.updateLog(systemError: true)
  125. @view.unsetPdf()
  126. @view.showLog()
  127. else if status == "timedout"
  128. @view.updateLog(timedOut: true)
  129. @view.unsetPdf()
  130. @view.showLog()
  131. else if status == "autocompile-backoff"
  132. @view.showBeforeCompile()
  133. else
  134. pdfExists = (status == "success")
  135. @fetchLogAndUpdateView(pdfExists)
  136. if pdfExists
  137. @view.setPdf("/project/#{@ide.project_id}/output/output.pdf?cache_bust=#{Date.now()}")
  138. @view.showPdf()
  139. else
  140. @view.unsetPdf()
  141. @view.showLog()
  142. @syncButtonsView?.hide()
  143. if outputFiles?
  144. @view.showOutputFileDownloadLinks(outputFiles)
  145. _doCompile: (opts, callback = (error, status, outputFiles) ->) ->
  146. url = "/project/#{@ide.project_id}/compile"
  147. if opts.isAutoCompile
  148. url += "?auto_compile=true"
  149. $.ajax(
  150. url: url
  151. type: "POST"
  152. headers:
  153. "X-CSRF-Token": window.csrfToken
  154. contentType: "application/json; charset=utf-8"
  155. dataType: 'json'
  156. data: JSON.stringify settingsOverride:
  157. rootDoc_id: opts.rootDocOverride_id ? null
  158. success: (body, status, response) ->
  159. callback null, body.status, body.outputFiles
  160. error: (error) ->
  161. callback error
  162. )
  163. fetchLogAndUpdateView: (pdfExists) ->
  164. $.ajax(
  165. url: "/project/#{@ide.project_id}/output/output.log"
  166. success: (body, status, response) =>
  167. @parseLogAndUpdateView(pdfExists, body)
  168. error: () =>
  169. @view.updateLog(pdfExists: pdfExists, logExists: false)
  170. )
  171. parseLogAndUpdateView: (pdfExists, log) ->
  172. errors = LogParser.parse(log, ignoreDuplicates: true)
  173. lastCompileErrors = {}
  174. for error in errors.all
  175. error.file = @_normalizeFilePath(error.file)
  176. doc_id = @ide.fileTreeManager.getDocIdOfPath(error.file)
  177. if doc_id?
  178. lastCompileErrors[doc_id] ||= []
  179. lastCompileErrors[doc_id].push
  180. row: error.line - 1
  181. type: if error.level == "error" then "error" else "warning"
  182. text: error.message
  183. @ide.editor.compilationErrors = lastCompileErrors
  184. @ide.editor.refreshCompilationErrors()
  185. @view.updateLog(pdfExists: pdfExists, logExists: true, compileErrors: errors, rawLog: log)
  186. _normalizeFilePath: (path) ->
  187. path = path.replace(/^compiles\/[0-9a-f]{32}\/(\.\/)?/, "")
  188. path = path.replace(/^\/compile\//, "")
  189. rootDoc_id = @ide.project.get("rootDoc_id")
  190. if rootDoc_id?
  191. rootDocPath = @ide.fileTreeManager.getPathOfEntityId(rootDoc_id)
  192. if rootDocPath?
  193. rootDocDir = rootDocPath.split("/").slice(0,-1).map( (part) -> part + "/" ).join("")
  194. path = path.replace(/^\.\//, rootDocDir)
  195. return path
  196. downloadPdf: () ->
  197. @ide.mainAreaManager.setIframeSrc "/project/#{@ide.project_id}/output/output.pdf?popupDownload=true"
  198. deleteCachedFiles: () ->
  199. modal = new Modal
  200. title: "Clear cache?"
  201. 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."
  202. buttons: [{
  203. text: "Cancel"
  204. }, {
  205. text: "Clear from cache",
  206. class: "btn-primary",
  207. close: false
  208. callback: ($button) =>
  209. $button.text("Clearing...")
  210. $button.prop("disabled", true)
  211. $.ajax({
  212. url: "/project/#{@ide.project_id}/output"
  213. type: "DELETE"
  214. headers:
  215. "X-CSRF-Token": window.csrfToken
  216. complete: () -> modal.remove()
  217. })
  218. }]
  219. syncToCode: (e) ->
  220. if !e?
  221. e = @view.getPdfPosition()
  222. return if !e?
  223. # It's not clear exactly where we should sync to if it was directly
  224. # clicked on, but a little bit down from the very top seems best.
  225. e.y = e.y + 80
  226. $.ajax {
  227. url: "/project/#{@ide.project_id}/sync/pdf"
  228. data:
  229. page: e.page + 1
  230. h: e.x.toFixed(2)
  231. v: e.y.toFixed(2)
  232. type: "GET"
  233. success: (response) =>
  234. data = JSON.parse(response)
  235. if data.code and data.code.length > 0
  236. file = data.code[0].file
  237. line = data.code[0].line
  238. @ide.fileTreeManager.openDocByPath(file, line)
  239. }
  240. syncToPdf: () ->
  241. entity_id = @ide.editor.getCurrentDocId()
  242. file = @ide.fileTreeManager.getPathOfEntityId(entity_id)
  243. # If the root file is folder/main.tex, then synctex sees the
  244. # path as folder/./main.tex
  245. rootFolderPath = @ide.fileTreeManager.getRootFolderPath()
  246. if rootFolderPath != ""
  247. file = file.replace(RegExp("^#{rootFolderPath}"), "#{rootFolderPath}/.")
  248. line = @ide.editor.getCurrentLine()
  249. column = @ide.editor.getCurrentColumn()
  250. $.ajax {
  251. url: "/project/#{@ide.project_id}/sync/code"
  252. data:
  253. file: file
  254. line: line + 1
  255. column: column
  256. type: "GET"
  257. success: (response) =>
  258. data = JSON.parse(response)
  259. @view.highlightInPdf(data.pdf or [])
  260. }