diff --git a/services/web/app/views/project/editor.jade b/services/web/app/views/project/editor.jade index 6b99b3fdcb..20334d9fc4 100644 --- a/services/web/app/views/project/editor.jade +++ b/services/web/app/views/project/editor.jade @@ -142,9 +142,11 @@ block content font-size="settings.fontSize", auto-complete="settings.autoComplete", spell-check-language="project.spellCheckLanguage", + annotations="onlineUserCursorAnnotations[editor.open_doc_id]" show-print-margin="false", sharejs-doc="editor.sharejs_doc", - last-updated="editor.last_updated" + last-updated="editor.last_updated", + cursor-position="editor.cursorPosition" ) //- #loadingScreen diff --git a/services/web/public/coffee/app/ide.coffee b/services/web/public/coffee/app/ide.coffee index 030dfb6a27..355993df4e 100644 --- a/services/web/public/coffee/app/ide.coffee +++ b/services/web/public/coffee/app/ide.coffee @@ -4,6 +4,7 @@ define [ "ide/connection/ConnectionManager" "ide/editor/EditorManager" "ide/settings/SettingsManager" + "ide/online-users/OnlineUsersManager" "ide/directives/layout" "ide/services/ide" "directives/focus" @@ -15,6 +16,7 @@ define [ ConnectionManager EditorManager SettingsManager + OnlineUsersManager ) -> App.controller "IdeController", ["$scope", "$timeout", "ide", ($scope, $timeout, ide) -> # Don't freak out if we're already in an apply callback @@ -43,6 +45,7 @@ define [ ide.fileTreeManager = new FileTreeManager(ide, $scope) ide.editorManager = new EditorManager(ide, $scope) ide.settingsManager = new SettingsManager(ide, $scope) + ide.onlineUsersManager = new OnlineUsersManager(ide, $scope) ] angular.bootstrap(document.body, ["SharelatexApp"]) \ No newline at end of file diff --git a/services/web/public/coffee/app/ide/editor/EditorManager.coffee b/services/web/public/coffee/app/ide/editor/EditorManager.coffee index 3433390b90..b20d9e718b 100644 --- a/services/web/public/coffee/app/ide/editor/EditorManager.coffee +++ b/services/web/public/coffee/app/ide/editor/EditorManager.coffee @@ -9,6 +9,7 @@ define [ last_updated: null open_doc_id: null opening: true + cursorPosition: null } @$scope.$on "entity:selected", (event, entity) => @@ -32,8 +33,8 @@ define [ openDoc: (doc, options = {}) -> console.log "Trying to open doc", doc.id - return if doc.id == @$scope.open_doc_id and !options.forceReopen - @$scope.open_doc_id = doc.id + return if doc.id == @$scope.editor.open_doc_id and !options.forceReopen + @$scope.editor.open_doc_id = doc.id console.log "Actually opening doc", doc.id $.localStorage "doc.open_id.#{@$scope.project_id}", doc.id diff --git a/services/web/public/coffee/app/ide/editor/annotations/AnnotationsManager.coffee b/services/web/public/coffee/app/ide/editor/annotations/AnnotationsManager.coffee new file mode 100644 index 0000000000..4bba724d5e --- /dev/null +++ b/services/web/public/coffee/app/ide/editor/annotations/AnnotationsManager.coffee @@ -0,0 +1,37 @@ +define [ + "ace/range" +], () -> + Range = require("ace/range").Range + + class AnnotationsManager + constructor: (@$scope, @editor) -> + @markerIds = [] + + @$scope.$watch "annotations", (value) => + if value? + @redrawAnnotations() + + redrawAnnotations: () -> + console.log "REDRAWING ANNOTATIONS" + for marker_id in @markerIds + @editor.getSession().removeMarker(marker_id) + @markerIds = [] + + for annotation in @$scope.annotations or [] + do (annotation) => + console.log "DRAWING ANNOTATION", annotation + @markerIds.push @editor.getSession().addMarker new Range( + annotation.cursor.row, annotation.cursor.column, + annotation.cursor.row, annotation.cursor.column + 1 + ), "remote-cursor", (html, range, left, top, config) -> + div = """ +
+
+ + """ + html.push div + , true \ No newline at end of file diff --git a/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee index 7d340403a1..de577f321c 100644 --- a/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee +++ b/services/web/public/coffee/app/ide/editor/directives/aceEditor.coffee @@ -4,11 +4,12 @@ define [ "ide/editor/undo/UndoManager" "ide/editor/auto-complete/AutoCompleteManager" "ide/editor/spell-check/SpellCheckManager" + "ide/editor/annotations/AnnotationsManager" "ace/keyboard/vim" "ace/keyboard/emacs" "ace/mode/latex" "ace/edit_session" -], (App, Ace, UndoManager, AutoCompleteManager, SpellCheckManager) -> +], (App, Ace, UndoManager, AutoCompleteManager, SpellCheckManager, AnnotationsManager) -> LatexMode = require("ace/mode/latex").Mode EditSession = require('ace/edit_session').EditSession @@ -23,13 +24,25 @@ define [ sharejsDoc: "=" lastUpdated: "=" spellCheckLanguage: "=" + cursorPosition: "=" + annotations: "=" } link: (scope, element, attrs) -> + # Don't freak out if we're already in an apply callback + scope.$originalApply = scope.$apply + scope.$apply = (fn = () ->) -> + phase = @$root.$$phase + if (phase == '$apply' || phase == '$digest') + fn() + else + @$originalApply(fn); + editor = Ace.edit(element.find(".ace-editor-body")[0]) autoCompleteManager = new AutoCompleteManager(scope, editor) spellCheckManager = new SpellCheckManager(scope, editor, element) undoManager = new UndoManager(scope, editor) + annotationsManagaer = new AnnotationsManager(scope, editor) # Prevert Ctrl|Cmd-S from triggering save dialog editor.commands.addCommand @@ -41,6 +54,11 @@ define [ editor.commands.removeCommand "showSettingsMenu" editor.commands.removeCommand "foldall" + editor.on "changeSelection", () -> + cursor = editor.getCursorPosition() + scope.$apply () -> + scope.cursorPosition = cursor + scope.$watch "theme", (value) -> editor.setTheme("ace/theme/#{value}") diff --git a/services/web/public/coffee/app/ide/online-users/OnlineUsersManager.coffee b/services/web/public/coffee/app/ide/online-users/OnlineUsersManager.coffee new file mode 100644 index 0000000000..1d63ccb02b --- /dev/null +++ b/services/web/public/coffee/app/ide/online-users/OnlineUsersManager.coffee @@ -0,0 +1,56 @@ +define [], () -> + class OnlineUsersManager + constructor: (@ide, @$scope) -> + @$scope.onlineUsers = {} + @$scope.onlineUserCursorAnnotations = {} + + @$scope.$watch "editor.cursorPosition", (position) => + console.log "CURSOR POSITION UPDATE", position + if position? + @sendCursorPositionUpdate() + + @ide.socket.on "clientTracking.clientUpdated", (client) => + console.log "REMOTE CURSOR POSITION UPDATE", client + if client.id != @ide.socket.socket.sessionid # Check it's not me! + @$scope.$apply () => + @$scope.onlineUsers[client.id] = client + @updateCursorHighlights() + + @ide.socket.on "clientTracking.clientDisconnected", (client_id) => + @$scope.$apply () => + delete @$scope.onlineUsers[client_id] + @updateCursorHighlights() + + updateCursorHighlights: () -> + console.log "UPDATING CURSOR HIGHLIGHTS" + @$scope.onlineUserCursorAnnotations = {} + for client_id, client of @$scope.onlineUsers + doc_id = client.doc_id + @$scope.onlineUserCursorAnnotations[doc_id] ||= [] + @$scope.onlineUserCursorAnnotations[doc_id].push { + text: client.name + cursor: + row: client.row + column: client.column + } + + UPDATE_INTERVAL: 500 + sendCursorPositionUpdate: () -> + if !@cursorUpdateTimeout? + console.log "CREATING DELAYED UPDATED" + @cursorUpdateTimeout = setTimeout ()=> + position = @$scope.editor.cursorPosition + doc_id = @$scope.editor.open_doc_id + + @ide.socket.emit "clientTracking.updatePosition", { + row: position.row + column: position.column + doc_id: doc_id + } + + delete @cursorUpdateTimeout + , @UPDATE_INTERVAL + else + console.log "NOT UPDATING" + + diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less index fe5202d8d1..41736b4538 100644 --- a/services/web/public/stylesheets/app/editor.less +++ b/services/web/public/stylesheets/app/editor.less @@ -193,6 +193,28 @@ background-repeat: repeat-x; background-position: bottom left; } + @cursor-color: rgb(14, 158, 0); + .remote-cursor { + position: absolute; + z-index: 2; + border-left: 2px solid @cursor-color; + .name { + font-size: 0.8em; + background-color: @cursor-color; + color: white; + padding: 2px 6px; + border-radius: 3px 3px 3px 0; + position: absolute; + left: -4px; + } + .nubbin { + height: 6px; + width: 6px; + background-color: @cursor-color; + position: absolute; + left: -4px; + } + } } .ui-layout-resizer {