diff --git a/services/web/app/views/project/editor/file-tree.pug b/services/web/app/views/project/editor/file-tree.pug
index 79a128493f..c4490204ac 100644
--- a/services/web/app/views/project/editor/file-tree.pug
+++ b/services/web/app/views/project/editor/file-tree.pug
@@ -6,21 +6,21 @@ aside.file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
tooltip-html="'"+translate('new_file').replace(' ', '
')+"'",
tooltip-placement="bottom"
)
- i.fa.fa-file
+ i.fa.fa-fw.fa-file
a(
href,
ng-click="openNewFolderModal()",
tooltip-html="'"+translate('new_folder').replace(' ', '
')+"'",
tooltip-placement="bottom"
)
- i.fa.fa-folder
+ i.fa.fa-fw.fa-folder
a(
href,
ng-click="openUploadFileModal()",
tooltip=translate('upload'),
tooltip-placement="bottom"
)
- i.fa.fa-upload
+ i.fa.fa-fw.fa-upload
.toolbar-right
a(
@@ -30,7 +30,7 @@ aside.file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
tooltip-placement="bottom",
ng-show="multiSelectedCount == 0"
)
- i.fa.fa-pencil
+ i.fa.fa-fw.fa-pencil
a(
href,
ng-click="openDeleteModalForSelected()",
@@ -38,7 +38,7 @@ aside.file-tree(ng-controller="FileTreeController", ng-class="{ 'multi-selected'
tooltip-placement="bottom",
tooltip-append-to-body="true"
)
- i.fa.fa-trash-o
+ i.fa.fa-fw.fa-trash-o
.file-tree-inner(
diff --git a/services/web/app/views/project/editor/pdf.pug b/services/web/app/views/project/editor/pdf.pug
index 9681c4fc9a..a87b765cf2 100644
--- a/services/web/app/views/project/editor/pdf.pug
+++ b/services/web/app/views/project/editor/pdf.pug
@@ -1,5 +1,5 @@
div.full-size.pdf(ng-controller="PdfController")
- .toolbar.toolbar-pdf
+ .toolbar.toolbar-pdf(ng-class="{ 'uncompiled-changes': uncompiledChanges && !autoCompileLintingError }")
.btn-group.btn-recompile-group#recompile(
dropdown,
tooltip-html="'"+translate('recompile_pdf')+" ({{modifierKey}} + Enter)'"
@@ -65,7 +65,7 @@ div.full-size.pdf(ng-controller="PdfController")
tooltip=translate('stop_compile')
tooltip-placement="bottom"
)
- i.fa.fa-stop()
+ i.fa.fa-fw.fa-stop()
a.log-btn(
href
ng-click="toggleLogs()"
@@ -73,7 +73,7 @@ div.full-size.pdf(ng-controller="PdfController")
tooltip=translate('logs_and_output_files')
tooltip-placement="bottom"
)
- i.fa.fa-file-text-o
+ i.fa.fa-fw.fa-file-text-o
span.label(
ng-show="pdf.logEntries.warnings.length + pdf.logEntries.errors.length > 0"
ng-class="{\
@@ -89,9 +89,21 @@ div.full-size.pdf(ng-controller="PdfController")
tooltip=translate('download_pdf')
tooltip-placement="bottom"
)
- i.fa.fa-download
+ i.fa.fa-fw.fa-download
.toolbar-right
+ span.auto-compile-status.small(
+ ng-show="uncompiledChanges && !compiling && !autoCompileLintingError"
+ ) #{translate('uncompiled_changes')}
+ span.auto-compile-status.auto-compile-error.small(
+ ng-show="autoCompileLintingError"
+ tooltip-placement="bottom"
+ tooltip=translate("code_check_failed_explanation")
+ tooltip-append-to-body="true"
+ )
+ i.fa.fa-fw.fa-exclamation-triangle
+ |
+ | #{translate("code_check_failed")}
a(
href,
ng-click="switchToFlatLayout()"
diff --git a/services/web/public/coffee/ide/editor/Document.coffee b/services/web/public/coffee/ide/editor/Document.coffee
index afdd924588..1965eaefd8 100644
--- a/services/web/public/coffee/ide/editor/Document.coffee
+++ b/services/web/public/coffee/ide/editor/Document.coffee
@@ -352,8 +352,13 @@ define [
v: version
@doc.on "change", (ops, oldSnapshot, msg) =>
@_applyOpsToRanges(ops, oldSnapshot, msg)
+ @ide.$scope.$emit "doc:changed",
+ doc_id: @doc_id
@doc.on "flipped_pending_to_inflight", () =>
@trigger "flipped_pending_to_inflight"
+ @doc.on "saved", () =>
+ @ide.$scope.$emit "doc:saved",
+ doc_id: @doc_id
_onError: (error, meta = {}) ->
meta.doc_id = @doc_id
diff --git a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee
index 63fca95856..20ccdea056 100644
--- a/services/web/public/coffee/ide/editor/ShareJsDoc.coffee
+++ b/services/web/public/coffee/ide/editor/ShareJsDoc.coffee
@@ -46,6 +46,8 @@ define [
@trigger "remoteop", args...
@_doc.on "flipped_pending_to_inflight", () =>
@trigger "flipped_pending_to_inflight"
+ @_doc.on "saved", () =>
+ @trigger "saved"
@_doc.on "error", (e) =>
@_handleError(e)
diff --git a/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee b/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee
index aca3560b49..edfed10386 100644
--- a/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee
+++ b/services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee
@@ -164,6 +164,9 @@ class Doc
@inflightOp = null
@inflightSubmittedIds.length = 0
+ if @pendingOp == null # All ops are acked
+ @emit 'saved'
+
error = msg.error
if error
# The server has rejected an op from the client for some reason.
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index d5d3a41af5..bf55961589 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -5,8 +5,12 @@ define [
"libs/bib-log-parser"
"services/log-hints-feedback"
], (App, Ace, HumanReadableLogs, BibLogParser) ->
- AUTO_COMPILE_TIMEOUT = 5000
- OP_ACKNOWLEDGEMENT_TIMEOUT = 1100
+ AUTO_COMPILE_MAX_WAIT = 5000
+ # We add a 1 second debounce to sending user changes to server if they aren't
+ # collaborating with anyone. This needs to be higher than that, and allow for
+ # client to server latency, otherwise we compile before the op reaches the server
+ # and then again on ack.
+ AUTO_COMPILE_DEBOUNCE = 2000
App.controller "PdfController", ($scope, $http, ide, $modal, synctex, event_tracking, logHintsFeedback, localStorage) ->
# enable per-user containers by default
@@ -75,38 +79,102 @@ define [
$scope.pdf.view = 'errors'
$scope.pdf.renderingError = true
- autoCompileTimeout = null
+ autoCompileInterval = null
+ autoCompileIfReady = () ->
+ if $scope.pdf.compiling
+ return
+
+ # Only checking linting if syntaxValidation is on and visible to the user
+ autoCompileLintingError = ide.$scope.hasLintingError and ide.$scope.settings.syntaxValidation
+ if $scope.autoCompileLintingError != autoCompileLintingError
+ $scope.$apply () ->
+ $scope.autoCompileLintingError = autoCompileLintingError
+ # We've likely been waiting a while until the user fixed the linting, but we
+ # don't want to compile as soon as it is fixed, so reset the timeout.
+ $scope.startedTryingAutoCompileAt = Date.now()
+ $scope.docLastChangedAt = Date.now()
+ if autoCompileLintingError
+ return
+
+ # If there's a longish compile, don't compile immediately after if user is still typing
+ startedTryingAt = Math.max($scope.startedTryingAutoCompileAt, $scope.lastFinishedCompileAt || 0)
+
+ timeSinceStartedTrying = Date.now() - startedTryingAt
+ timeSinceLastChange = Date.now() - $scope.docLastChangedAt
+
+ shouldCompile = false
+ if timeSinceLastChange > AUTO_COMPILE_DEBOUNCE # Don't compile in the middle of the user typing
+ shouldCompile = true
+ else if timeSinceStartedTrying > AUTO_COMPILE_MAX_WAIT # Unless they type for a long time
+ shouldCompile = true
+ else if timeSinceStartedTrying < 0 or timeSinceLastChange < 0
+ # If time is non-monotonic, assume that the user's system clock has been
+ # changed and continue with compile
+ shouldCompile = true
+
+ if shouldCompile
+ triggerAutoCompile()
+
triggerAutoCompile = () ->
- return if autoCompileTimeout or $scope.ui.pdfHidden
+ $scope.recompile(isAutoCompileOnChange: true)
- timeSinceLastCompile = Date.now() - $scope.recompiledAt
- # If time is non-monotonic, assume that the user's system clock has been
- # changed and continue with recompile
- isTimeNonMonotonic = timeSinceLastCompile < 0
+ startTryingAutoCompile = () ->
+ return if autoCompileInterval?
+ $scope.startedTryingAutoCompileAt = Date.now()
+ autoCompileInterval = setInterval autoCompileIfReady, 200
- if isTimeNonMonotonic || timeSinceLastCompile >= AUTO_COMPILE_TIMEOUT
- # If user has code check disabled, it is likely because they have
- # linting errors that they are ignoring. Therefore it doesn't make sense
- # to block auto compiles. It also causes problems where server-provided
- # linting errors aren't cleared after typing
- if (ide.$scope.settings.syntaxValidation and !ide.$scope.hasLintingError)
- $scope.recompile(isAutoCompileOnChange: true) # compile if no linting errors
- else if !ide.$scope.settings.syntaxValidation
- $scope.recompile(isAutoCompileOnChange: true) # always recompile
+ stopTryingAutoCompile = () ->
+ clearInterval autoCompileInterval
+ autoCompileInterval = null
+
+ $scope.$watch "uncompiledChanges", (uncompiledChanges) ->
+ if uncompiledChanges
+ startTryingAutoCompile()
else
- # Extend remainder of timeout
- autoCompileTimeout = setTimeout () ->
- autoCompileTimeout = null
- triggerAutoCompile()
- , AUTO_COMPILE_TIMEOUT - timeSinceLastCompile
+ stopTryingAutoCompile()
- autoCompileListener = null
+ $scope.uncompiledChanges = false
+ recalculateUncompiledChanges = () ->
+ if $scope.ui.pdfHidden
+ # Don't bother auto-compiling if pdf isn't visible
+ $scope.uncompiledChanges = false
+ else if !$scope.docLastChangedAt?
+ $scope.uncompiledChanges = false
+ else if !$scope.lastStartedCompileAt? or $scope.docLastChangedAt > $scope.lastStartedCompileAt
+ $scope.uncompiledChanges = true
+ else
+ $scope.uncompiledChanges = false
+
+ _updateDocLastChangedAt = () ->
+ $scope.docLastChangedAt = Date.now()
+ recalculateUncompiledChanges()
+
+ onDocChanged = () ->
+ $scope.autoCompileLintingError = false
+ _updateDocLastChangedAt()
+
+ onDocSaved = () ->
+ # We use the save as a trigger too, to account for the delay between the client
+ # and server. Otherwise, we might have compiled after the user made
+ # the change on the client, but before the server had it.
+ _updateDocLastChangedAt()
+
+ onCompilingStateChanged = (compiling) ->
+ recalculateUncompiledChanges()
+
+ autoCompileListeners = []
toggleAutoCompile = (enabling) ->
if enabling
- autoCompileListener = ide.$scope.$on "ide:opAcknowledged", _.debounce(triggerAutoCompile, OP_ACKNOWLEDGEMENT_TIMEOUT)
+ autoCompileListeners = [
+ ide.$scope.$on "doc:changed", onDocChanged
+ ide.$scope.$on "doc:saved", onDocSaved
+ $scope.$watch "pdf.compiling", onCompilingStateChanged
+ ]
else
- autoCompileListener() if autoCompileListener
- autoCompileListener = null
+ for unbind in autoCompileListeners
+ unbind()
+ autoCompileListeners = []
+ $scope.autoCompileLintingError = false
$scope.autocompile_enabled = localStorage("autocompile_enabled:#{$scope.project_id}") or false
$scope.$watch "autocompile_enabled", (newValue, oldValue) ->
@@ -428,6 +496,7 @@ define [
event_tracking.sendMBSampled "editor-recompile-sampled", options
+ $scope.lastStartedCompileAt = Date.now()
$scope.pdf.compiling = true
$scope.pdf.isAutoCompileOnLoad = options?.isAutoCompileOnLoad # initial autocompile
@@ -460,7 +529,7 @@ define [
$scope.pdf.error = true
$scope.pdf.view = 'errors'
.finally () ->
- $scope.recompiledAt = Date.now()
+ $scope.lastFinishedCompileAt = Date.now()
# This needs to be public.
ide.$scope.recompile = $scope.recompile
diff --git a/services/web/public/stylesheets/app/editor/pdf.less b/services/web/public/stylesheets/app/editor/pdf.less
index d59ca93069..0f76816fd5 100644
--- a/services/web/public/stylesheets/app/editor/pdf.less
+++ b/services/web/public/stylesheets/app/editor/pdf.less
@@ -1,13 +1,34 @@
+@stripe-width: 20px;
+@keyframes pdf-toolbar-stripes {
+ from { background-position: 0 0; }
+ to { background-position: @stripe-width 0; }
+}
+
.pdf .toolbar.toolbar-pdf when (@is-overleaf = true) {
.toolbar-small-mixin;
.toolbar-alt-mixin;
border-bottom: 0;
padding-right: 5px;
+ &.uncompiled-changes {
+ #gradient > .striped(@color: rgba(255,255,255,.10), @angle: -45deg);
+ background-size: @stripe-width @stripe-width;
+ .animation(pdf-toolbar-stripes 2s linear infinite);
+ }
+ .auto-compile-status {
+ color: white;
+ margin-right: (@line-height-computed / 2);
+ i {
+ color: @brand-danger;
+ }
+ }
}
.pdf .toolbar.toolbar-pdf when (@is-overleaf = false) {
.toolbar-tall-mixin;
padding: 0 (@line-height-computed / 2);
+ .auto-compile-status {
+ display: none;
+ }
}
.pdf {
@@ -31,29 +52,30 @@
.btn-recompile-group when (@is-overleaf = true) {
align-self: stretch;
- margin-right: 5px;
+ margin-right: 6px;
+ .btn-recompile {
+ height: 100%;
+ .btn-primary;
+ padding-top: 3px;
+ padding-bottom: 3px;
+ &:first-child {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ &[disabled] {
+ background-color: mix(@btn-primary-bg, @toolbar-alt-bg-color, 65%);
+ .opacity(1.0);
+ }
+ }
}
.btn-recompile-group when (@is-overleaf = false) {
margin-right: (@line-height-computed / 2);
-}
-
-
-.btn-recompile when (@is-overleaf = true) {
- height: 100%;
- .btn-primary;
- padding-top: 3px;
- padding-bottom: 3px;
- &:first-child {
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
+ .btn-recompile {
+ .btn-info;
}
}
-.btn-recompile when (@is-overleaf = false) {
- .btn-info;
-}
-
.btn-split-screen when (@is-overleaf = false) {
.fa {
display: none;
@@ -146,7 +168,6 @@
width: 16px;
height: 16px;
border: 1px solid @gray-light;
- margin-top: 5px;
}
i.full-screen {
diff --git a/services/web/public/stylesheets/app/editor/toolbar.less b/services/web/public/stylesheets/app/editor/toolbar.less
index 8370e40959..3175aa3b29 100644
--- a/services/web/public/stylesheets/app/editor/toolbar.less
+++ b/services/web/public/stylesheets/app/editor/toolbar.less
@@ -25,7 +25,9 @@
.toolbar-right > a:not(.btn) {
display: inline-block;
color: @toolbar-icon-btn-color;
- padding: 0 5px;
+ padding: 4px 2px;
+ line-height: 1;
+ height: 24px;
border-radius: @border-radius-small;
&.toolbar-header-back-projects {
padding: 5px 10px 4px;
@@ -49,6 +51,10 @@
}
}
+ &.toolbar-pdf > a:not(.btn) {
+ margin-right: 3px;
+ }
+
.btn-full-height {
border: none;
border-radius: 0;
diff --git a/services/web/public/stylesheets/components/dropdowns.less b/services/web/public/stylesheets/components/dropdowns.less
index 516db7178e..8744c1f35c 100755
--- a/services/web/public/stylesheets/components/dropdowns.less
+++ b/services/web/public/stylesheets/components/dropdowns.less
@@ -13,6 +13,7 @@
border-top: @caret-width-base solid;
border-right: @caret-width-base solid transparent;
border-left: @caret-width-base solid transparent;
+ margin-top: -@caret-width-base/2;
}
// The dropdown wrapper (div)