James Allen 12 жил өмнө
parent
commit
11d951f021

+ 21 - 1
services/web/app/views/project/editor/editor.jade

@@ -27,4 +27,24 @@ div.full-size(
 		)
 
 	.ui-layout-east
-		include ./pdf
+		include ./pdf
+
+	.ui-layout-resizer-controls.synctex-controls(
+		ng-show="!!pdf.url"
+		ng-controller="PdfSynctexController"
+	)
+		a.btn.btn-default.btn-xs(
+			tooltip="Go to code location in PDF"
+			tooltip-placement="right"
+			tooltip-append-to-body="true"
+			ng-click="syncToPdf()"
+		)
+			i.fa.fa-long-arrow-right
+		br
+		a.btn.btn-default.btn-xs(
+			tooltip-html-unsafe="Go to PDF location in code<br/>(or double click PDF)"
+			tooltip-placement="right"
+			tooltip-append-to-body="true"
+			ng-click="syncToCode()"
+		)
+			i.fa.fa-long-arrow-left

+ 3 - 0
services/web/app/views/project/editor/pdf.jade

@@ -33,6 +33,9 @@ div.full-size(ng-controller="PdfController")
 			pdf-src="pdf.url"
 			key="project_id"
 			resize-on="layout:main:resize,layout:pdf:resize"
+			highlights="pdf.highlights"
+			position="pdf.position"
+			dbl-click-callback="syncToCode"
 		)
 
 	.pdf-uncompiled(ng-show="pdf.uncompiled && !pdf.compiling")

+ 10 - 0
services/web/public/coffee/app/ide/directives/layout.coffee

@@ -12,6 +12,7 @@ define [
 					onresize: () =>
 						console.log "Triggering", "layout:#{name}:resize", name
 						scope.$broadcast "layout:#{name}:resize"
+						repositionControls()
 					#maskIframesOnResize: true
 
 				# Restore previously recorded state
@@ -28,4 +29,13 @@ define [
 				# Save state when exiting
 				$(window).unload () ->
 					$.localStorage("layout.#{name}", element.layout().readState())
+
+				repositionControls = () ->
+					state = element.layout().readState()
+					if state.east?
+						element.find(".ui-layout-resizer-controls").css({
+							position: "absolute"
+							right: state.east.size
+							"z-index": 10
+						})
 		}

+ 24 - 0
services/web/public/coffee/app/ide/file-tree/FileTreeManager.coffee

@@ -95,6 +95,10 @@ define [
 			parts = path.split("/")
 			name = parts.shift()
 			rest = parts.join("/")
+			
+			if name == "."
+				return @_findEntityByPathInFolder(folder, rest)
+
 			for entity in folder.children
 				if entity.name == name
 					if rest == ""
@@ -115,6 +119,26 @@ define [
 				if entity.children?
 					@_forEachEntityInFolder(entity, callback)
 
+		getEntityPath: (entity) ->
+			@_getEntityPathInFolder @$scope.rootFolder, entity
+
+		_getEntityPathInFolder: (folder, entity) ->
+			for child in folder.children or []
+				if child == entity
+					return entity.name
+				else if child.type == "folder"
+					path = @_getEntityPathInFolder(child, entity)
+					if path?
+						return child.name + "/" + path
+			return null
+
+		getRootDocDirname: () ->
+			rootDoc = @findEntityById @$scope.project.rootDoc_id
+			return if !rootDoc?
+			path = @getEntityPath(rootDoc)
+			return if !path?
+			return path.split("/").slice(0, -1).join("/")
+
 		# forEachFolder: (callback) ->
 		# 	@forEachEntity (entity) ->
 		# 		if entity.type == "folder"

+ 2 - 0
services/web/public/coffee/app/ide/pdf/PdfManager.coffee

@@ -16,3 +16,5 @@ define [
 				rawLog: ""
 				view: null # 'pdf' 'logs'
 				showRawLog: false
+				highlights: []
+				position: null

+ 115 - 4
services/web/public/coffee/app/ide/pdf/controllers/PdfController.coffee

@@ -2,7 +2,7 @@ define [
 	"base"
 	"libs/latex-log-parser"
 ], (App, LogParser) ->
-	App.controller "PdfController", ["$scope", "$http", "ide", "$modal", ($scope, $http, ide, $modal) ->
+	App.controller "PdfController", ["$scope", "$http", "ide", "$modal", "synctex", ($scope, $http, ide, $modal, synctex) ->
 		autoCompile = true
 		$scope.$on "doc:opened", () ->
 			return if !autoCompile
@@ -59,8 +59,7 @@ define [
 
 					$scope.pdf.logEntryAnnotations = {}
 					for entry in logEntries.all
-						entry.file = entry.file.replace(/^(.*)\/compiles\/[0-9a-f]{24}\/(\.\/)?/, "")
-						entry.file = entry.file.replace(/^\/compile\//, "")
+						entry.file = normalizeFilePath(entry.file)
 
 						entity = ide.fileTreeManager.findEntityByPath(entry.file)
 						if entity?
@@ -84,6 +83,16 @@ define [
 					return ide.editorManager.getCurrentDocId()
 			return null
 
+		normalizeFilePath = (path) ->
+			path = path.replace(/^(.*)\/compiles\/[0-9a-f]{24}\/(\.\/)?/, "")
+			path = path.replace(/^\/compile\//, "")
+
+			rootDocDirname = ide.fileTreeManager.getRootDocDirname()
+			if rootDocDirname?
+				path = path.replace(/^\.\//, rootDocDirname)
+
+			return path
+
 		$scope.recompile = (options = {}) ->
 			console.log "Recompiling", options
 			return if $scope.pdf.compiling
@@ -129,13 +138,115 @@ define [
 				controller: "ClearCacheModalController"
 				scope: $scope
 			)
+
+		$scope.syncToCode = (position) ->
+			console.log "SYNCING VIA DBL CLICK", position
+			synctex
+				.syncToCode(position)
+				.then (data) ->
+					{doc, line} = data
+					ide.editorManager.openDoc(doc, gotoLine: line)
+
+	]
+
+	App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->
+		synctex =
+			syncToPdf: (cursorPosition) ->
+				deferred = $q.defer()
+
+				doc_id = ide.editorManager.getCurrentDocId()
+				if !doc_id?
+					deferred.reject()
+					return deferred.promise()
+				doc = ide.fileTreeManager.findEntityById(doc_id)
+				if !doc?
+					deferred.reject()
+					return deferred.promise()
+				path = ide.fileTreeManager.getEntityPath(doc)
+				if !path?
+					deferred.reject()
+					return deferred.promise()
+				
+				# If the root file is folder/main.tex, then synctex sees the
+				# path as folder/./main.tex
+				rootDocDirname = ide.fileTreeManager.getRootDocDirname()
+				if rootDocDirname? and rootDocDirname != ""
+					path = path.replace(RegExp("^#{rootDocDirname}"), "#{rootDocDirname}/.")
+
+				{row, column} = cursorPosition
+
+				$http({
+						url: "/project/#{ide.project_id}/sync/code", 
+						method: "GET",
+						params: {
+							file: path
+							line: row + 1
+							column: column
+						}
+					})
+					.success (data) ->
+						console.log "SYNCTEX RESPONSE", data
+						deferred.resolve(data.pdf or [])
+					.error (error) ->
+						deferred.reject(error)
+
+				return deferred.promise
+
+			syncToCode: (position, options = {}) ->
+				deferred = $q.defer()
+				if !position?
+					deferred.reject()
+					return deferred.promise()
+
+				# It's not clear exactly where we should sync to if it wasn't directly
+				# clicked on, but a little bit down from the very top seems best.
+				if options.includeVisualOffset
+					position.offset.top = position.offset.top + 80
+
+				$http({
+						url: "/project/#{ide.project_id}/sync/pdf", 
+						method: "GET",
+						params: {
+							page: position.page + 1
+							h: position.offset.left.toFixed(2)
+							v: position.offset.top.toFixed(2)
+						}
+					})
+					.success (data) ->
+						console.log "SYNCTEX RESPONSE", data
+						if data.code? and data.code.length > 0
+							doc = ide.fileTreeManager.findEntityByPath(data.code[0].file)
+							return if !doc?
+							deferred.resolve({doc: doc, line: data.code[0].line})
+					.error (error) ->
+						deferred.reject(error)
+
+				return deferred.promise
+
+		return synctex
+	]
+
+	App.controller "PdfSynctexController", ["$scope", "synctex", "ide", ($scope, synctex, ide) ->
+		$scope.syncToPdf = () ->
+			synctex
+				.syncToPdf($scope.editor.cursorPosition)
+				.then (highlights) ->
+					$scope.pdf.highlights = highlights
+
+		$scope.syncToCode = () ->
+			synctex
+				.syncToCode($scope.pdf.position, includeVisualOffset: true)
+				.then (data) ->
+					{doc, line} = data
+					console.log "OPENING DOC", doc, line
+					ide.editorManager.openDoc(doc, gotoLine: line)
 	]
 
 	App.controller "PdfLogEntryController", ["$scope", "ide", ($scope, ide) ->
 		$scope.openInEditor = (entry) ->
 			console.log "OPENING", entry.file, entry.line
 			entity = ide.fileTreeManager.findEntityByPath(entry.file)
-			return if entity.type != "doc"
+			return if !entity? or entity.type != "doc"
 			if entry.line?
 				line = entry.line
 			ide.editorManager.openDoc(entity, gotoLine: line)

+ 43 - 1
services/web/public/coffee/app/ide/pdf/directives/pdfJs.coffee

@@ -28,12 +28,16 @@ define [
 		return {
 			scope: {
 				"pdfSrc": "="
+				"highlights": "="
+				"position": "="
+				"dblClickCallback": "="
 			}
 			link: (scope, element, attrs) ->
 				pdfListView = new PDFListView element.find(".pdfjs-viewer")[0],
 					textLayerBuilder: TextLayerBuilder
 					annotationsLayerBuilder: AnnotationsLayerBuilder
 					highlightsLayerBuilder: HighlightsLayerBuilder
+					ondblclick: (e) -> onDoubleClick(e)
 					logLevel: PDFListView.Logger.DEBUG
 				pdfListView.listView.pageWidthOffset = 20
 				pdfListView.listView.pageHeightOffset = 20
@@ -58,6 +62,8 @@ define [
 					if (position = $.localStorage("pdf.position.#{attrs.key}"))
 						pdfListView.setPdfPosition(position)
 
+					scope.position = pdfListView.getPdfPosition(true)
+
 					$(window).unload () =>
 						$.localStorage "pdf.scale", {
 							scaleMode: pdfListView.getScaleMode()
@@ -69,7 +75,14 @@ define [
 					scope.flashControls = true
 					$timeout () ->
 						scope.flashControls = false
-					, 1000	
+					, 1000
+
+				element.find(".pdfjs-viewer").scroll () ->
+					console.log "UPDATING POSITION", pdfListView.getPdfPosition(true)
+					scope.position = pdfListView.getPdfPosition(true)
+
+				onDoubleClick = (e) ->
+					scope.dblClickCallback?(page: e.page, offset: { top: e.y, left: e.x })
 
 				scope.$watch "pdfSrc", (url) ->
 					if url
@@ -85,6 +98,35 @@ define [
 									initializePosition()
 									flashControls()
 
+				scope.$watch "highlights", (areas) ->
+					console.log "UPDATING HIGHLIGHTS", areas
+					return if !areas?
+					highlights = for area in areas or []
+						{
+							page: area.page - 1
+							highlight:
+								left: area.h
+								top: area.v
+								height: area.height
+								width: area.width
+						}
+
+					if highlights.length > 0
+						first = highlights[0]
+						pdfListView.setPdfPosition({
+							page: first.page
+							offset:
+								left: first.highlight.left
+								top: first.highlight.top - 80
+						}, true)
+
+					pdfListView.clearHighlights()
+					pdfListView.setHighlights(highlights, true)
+					
+					setTimeout () =>
+						pdfListView.clearHighlights()
+					, 1000
+
 				scope.fitToHeight = () ->
 					pdfListView.setToFitHeight()
 

+ 8 - 0
services/web/public/stylesheets/app/editor/pdf.less

@@ -112,4 +112,12 @@
 		position: relative;
 	}
 }
+
+.synctex-controls {
+	padding: 68px 2px 0;
+	.btn-xs {
+		line-height: 1.3;
+		padding: 0 2px 0;
+	}
+}