Browse Source

Merge pull request #1037 from sharelatex/as-decaffeinate

Decaffeinate frontend

GitOrigin-RevId: 1c8c53dedecfe55f9936a13408df17b852f996de
Alasdair Smith 7 năm trước cách đây
mục cha
commit
659242b457
100 tập tin đã thay đổi với 559 bổ sung11607 xóa
  1. 6 0
      services/web/.babelrc
  2. 10 2
      services/web/.eslintrc
  3. 0 1
      services/web/.gitignore
  4. 12 0
      services/web/.prettierignore
  5. 4 0
      services/web/.prettierrc
  6. 27 35
      services/web/Makefile
  7. 22 18
      services/web/Makefile.module
  8. 57 0
      services/web/decaffeinate.sh
  9. 3 3
      services/web/nodemon.frontend.json
  10. 412 0
      services/web/npm-shrinkwrap.json
  11. 6 1
      services/web/package.json
  12. 0 71
      services/web/public/coffee/analytics/AbTestingManager.coffee
  13. 0 61
      services/web/public/coffee/base.coffee
  14. 0 74
      services/web/public/coffee/components/inputSuggestions.coffee
  15. 0 168
      services/web/public/coffee/directives/asyncForm.coffee
  16. 0 43
      services/web/public/coffee/directives/bookmarkableTabset.coffee
  17. 0 539
      services/web/public/coffee/directives/creditCards.coffee
  18. 0 15
      services/web/public/coffee/directives/equals.coffee
  19. 0 75
      services/web/public/coffee/directives/eventTracking.coffee
  20. 0 17
      services/web/public/coffee/directives/expandableTextArea.coffee
  21. 0 68
      services/web/public/coffee/directives/fineUpload.coffee
  22. 0 78
      services/web/public/coffee/directives/focus.coffee
  23. 0 21
      services/web/public/coffee/directives/mathjax.coffee
  24. 0 11
      services/web/public/coffee/directives/maxHeight.coffee
  25. 0 10
      services/web/public/coffee/directives/onEnter.coffee
  26. 0 12
      services/web/public/coffee/directives/rightClick.coffee
  27. 0 36
      services/web/public/coffee/directives/scroll.coffee
  28. 0 81
      services/web/public/coffee/directives/selectAll.coffee
  29. 0 18
      services/web/public/coffee/directives/stopPropagation.coffee
  30. 0 15
      services/web/public/coffee/directives/videoPlayState.coffee
  31. 0 23
      services/web/public/coffee/filters/formatDate.coffee
  32. 0 25
      services/web/public/coffee/filters/wrapLongWords.coffee
  33. 0 245
      services/web/public/coffee/ide.coffee
  34. 0 78
      services/web/public/coffee/ide/SafariScrollPatcher.coffee
  35. 0 21
      services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee
  36. 0 125
      services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee
  37. 0 46
      services/web/public/coffee/ide/chat/controllers/ChatButtonController.coffee
  38. 0 33
      services/web/public/coffee/ide/chat/controllers/ChatController.coffee
  39. 0 24
      services/web/public/coffee/ide/chat/controllers/ChatMessageController.coffee
  40. 0 7
      services/web/public/coffee/ide/chat/index.coffee
  41. 0 108
      services/web/public/coffee/ide/chat/services/chatMessages.coffee
  42. 0 9
      services/web/public/coffee/ide/clone/controllers/CloneProjectController.coffee
  43. 0 38
      services/web/public/coffee/ide/clone/controllers/CloneProjectModalController.coffee
  44. 0 4
      services/web/public/coffee/ide/clone/index.coffee
  45. 0 44
      services/web/public/coffee/ide/colors/ColorManager.coffee
  46. 0 281
      services/web/public/coffee/ide/connection/ConnectionManager.coffee
  47. 0 76
      services/web/public/coffee/ide/directives/SafePath.coffee
  48. 0 203
      services/web/public/coffee/ide/directives/layout.coffee
  49. 0 12
      services/web/public/coffee/ide/directives/validFile.coffee
  50. 0 29
      services/web/public/coffee/ide/editor/AceShareJsCodec.coffee
  51. 0 410
      services/web/public/coffee/ide/editor/Document.coffee
  52. 0 196
      services/web/public/coffee/ide/editor/EditorManager.coffee
  53. 0 190
      services/web/public/coffee/ide/editor/ShareJsDoc.coffee
  54. 0 35
      services/web/public/coffee/ide/editor/components/spellMenu.coffee
  55. 0 62
      services/web/public/coffee/ide/editor/controllers/SavingNotificationController.coffee
  56. 0 633
      services/web/public/coffee/ide/editor/directives/aceEditor.coffee
  57. 0 320
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee
  58. 0 167
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/CommandManager.coffee
  59. 0 194
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/EnvironmentManager.coffee
  60. 0 43
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/Helpers.coffee
  61. 0 45
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/PackageManager.coffee
  62. 0 29
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/snippets/Environments.coffee
  63. 0 691
      services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/snippets/TopHundredSnippets.coffee
  64. 0 32
      services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter.coffee
  65. 0 67
      services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee
  66. 0 265
      services/web/public/coffee/ide/editor/directives/aceEditor/highlights/HighlightsManager.coffee
  67. 0 66
      services/web/public/coffee/ide/editor/directives/aceEditor/metadata/MetadataManager.coffee
  68. 0 96
      services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/HighlightedWordManager.coffee
  69. 0 62
      services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckAdapter.coffee
  70. 0 209
      services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee
  71. 0 565
      services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee
  72. 0 367
      services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee
  73. 0 3
      services/web/public/coffee/ide/editor/sharejs/header.coffee
  74. 0 157
      services/web/public/coffee/ide/editor/sharejs/vendor/client/ace.coffee
  75. 0 89
      services/web/public/coffee/ide/editor/sharejs/vendor/client/cm.coffee
  76. 0 167
      services/web/public/coffee/ide/editor/sharejs/vendor/client/connection.coffee
  77. 0 349
      services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee
  78. 0 73
      services/web/public/coffee/ide/editor/sharejs/vendor/client/index.coffee
  79. 0 46
      services/web/public/coffee/ide/editor/sharejs/vendor/client/microevent.coffee
  80. 0 69
      services/web/public/coffee/ide/editor/sharejs/vendor/client/textarea.coffee
  81. 0 13
      services/web/public/coffee/ide/editor/sharejs/vendor/client/web-prelude.coffee
  82. 0 5
      services/web/public/coffee/ide/editor/sharejs/vendor/index.coffee
  83. 0 333
      services/web/public/coffee/ide/editor/sharejs/vendor/server/browserchannel.coffee
  84. 0 149
      services/web/public/coffee/ide/editor/sharejs/vendor/server/db/couchdb.coffee
  85. 0 28
      services/web/public/coffee/ide/editor/sharejs/vendor/server/db/index.coffee
  86. 0 198
      services/web/public/coffee/ide/editor/sharejs/vendor/server/db/pg.coffee
  87. 0 140
      services/web/public/coffee/ide/editor/sharejs/vendor/server/db/redis.coffee
  88. 0 57
      services/web/public/coffee/ide/editor/sharejs/vendor/server/index.coffee
  89. 0 602
      services/web/public/coffee/ide/editor/sharejs/vendor/server/model.coffee
  90. 0 148
      services/web/public/coffee/ide/editor/sharejs/vendor/server/rest.coffee
  91. 0 336
      services/web/public/coffee/ide/editor/sharejs/vendor/server/socketio.coffee
  92. 0 42
      services/web/public/coffee/ide/editor/sharejs/vendor/server/syncqueue.coffee
  93. 0 146
      services/web/public/coffee/ide/editor/sharejs/vendor/server/useragent.coffee
  94. 0 48
      services/web/public/coffee/ide/editor/sharejs/vendor/types/README.md
  95. 0 22
      services/web/public/coffee/ide/editor/sharejs/vendor/types/count.coffee
  96. 0 65
      services/web/public/coffee/ide/editor/sharejs/vendor/types/helpers.coffee
  97. 0 15
      services/web/public/coffee/ide/editor/sharejs/vendor/types/index.coffee
  98. 0 180
      services/web/public/coffee/ide/editor/sharejs/vendor/types/json-api.coffee
  99. 0 441
      services/web/public/coffee/ide/editor/sharejs/vendor/types/json.coffee
  100. 0 38
      services/web/public/coffee/ide/editor/sharejs/vendor/types/simple.coffee

+ 6 - 0
services/web/.babelrc

@@ -0,0 +1,6 @@
+{
+  "presets": [
+    "react",
+    ["env", { "modules": false }]
+  ]
+}

+ 10 - 2
services/web/.eslintrc

@@ -2,9 +2,13 @@
   "extends": [
     "standard",
     "standard-react",
+    "prettier",
+    "prettier/react",
+    "prettier/standard",
     "plugin:jsx-a11y/recommended"
   ],
   "plugins": [
+    "prettier",
     "jsx-a11y",
     "mocha",
     "chai-expect",
@@ -24,11 +28,15 @@
   },
   "rules": {
     "max-len": ["error", {
-      // Ignore long describe/it test blocks
       "ignoreUrls": true,
-      "ignorePattern": "^\\s*(it|describe)\\s*\\(['\"]"
+      // Ignore long describe/it test blocks, long import/require statements
+      "ignorePattern": "(^\\s*(it|describe)\\s*\\(['\"]|^import\\s*.*\\s*from\\s*['\"]|^.*\\s*=\\s*require\\(['\"])"
     }],
 
+    // Fix conflict between prettier & standard by overriding to prefer
+    // double quotes
+    "jsx-quotes": ["error", "prefer-double"],
+
     // Override weird behaviour of jsx-a11y label-has-for (says labels must be
     // nested *and* have for/id attributes)
     "jsx-a11y/label-has-for": [

+ 0 - 1
services/web/.gitignore

@@ -53,7 +53,6 @@ public/es/modules
 
 public/js/*.js
 public/js/*.map
-public/js/libs/sharejs.js
 public/js/analytics/
 public/js/directives/
 public/js/components/

+ 12 - 0
services/web/.prettierignore

@@ -0,0 +1,12 @@
+app/js
+modules/**/app/js
+modules/**/scripts
+modules/**/index.js
+public/js
+public/minjs
+modules/**/public/js
+test/**/js
+modules/**/test/**/js
+app.js
+webpack.config.*
+karma.conf.js

+ 4 - 0
services/web/.prettierrc

@@ -0,0 +1,4 @@
+{
+  "semi": false,
+  "singleQuote": true
+}

+ 27 - 35
services/web/Makefile

@@ -6,23 +6,19 @@ PROJECT_NAME = web
 MODULE_DIRS := $(shell find modules -mindepth 1 -maxdepth 1 -type d -not -name '.git' )
 MODULE_MAKEFILES := $(MODULE_DIRS:=/Makefile)
 COFFEE := node_modules/.bin/coffee $(COFFEE_OPTIONS)
+BABEL := node_modules/.bin/babel
 GRUNT := node_modules/.bin/grunt
+
 APP_COFFEE_FILES := $(shell find app/coffee -name '*.coffee')
-FRONT_END_COFFEE_FILES := $(shell find public/coffee -name '*.coffee')
+FRONT_END_SRC_FILES := $(shell find public/src -name '*.js')
 TEST_COFFEE_FILES := $(shell find test/*/coffee -name '*.coffee')
-MODULE_MAIN_COFFEE_FILES := $(shell find modules -type f -wholename '*main/index.coffee')
-MODULE_IDE_COFFEE_FILES := $(shell find modules -type f -wholename '*ide/index.coffee')
+TEST_SRC_FILES := $(shell find test/*/src -name '*.js')
+MODULE_MAIN_SRC_FILES := $(shell find modules -type f -wholename '*main/index.js')
+MODULE_IDE_SRC_FILES := $(shell find modules -type f -wholename '*ide/index.js')
 COFFEE_FILES := app.coffee $(APP_COFFEE_FILES) $(FRONT_END_COFFEE_FILES) $(TEST_COFFEE_FILES)
+SRC_FILES := $(FRONT_END_SRC_FILES) $(TEST_SRC_FILES)
 JS_FILES := $(subst coffee,js,$(COFFEE_FILES))
-SHAREJS_COFFEE_FILES := \
-	public/coffee/ide/editor/sharejs/header.coffee \
-	public/coffee/ide/editor/sharejs/vendor/types/helpers.coffee \
-	public/coffee/ide/editor/sharejs/vendor/types/text.coffee \
-	public/coffee/ide/editor/sharejs/vendor/types/text-api.coffee \
-	public/coffee/ide/editor/sharejs/vendor/client/microevent.coffee \
-	public/coffee/ide/editor/sharejs/vendor/client/doc.coffee \
-	public/coffee/ide/editor/sharejs/vendor/client/ace.coffee \
-	public/coffee/ide/editor/sharejs/vendor/client/cm.coffee
+OUTPUT_SRC_FILES := $(subst src,js,$(SRC_FILES))
 LESS_FILES := $(shell find public/stylesheets -name '*.less')
 CSS_FILES := public/stylesheets/style.css public/stylesheets/ol-style.css public/stylesheets/ol-light-style.css
 
@@ -34,9 +30,9 @@ app/js/%.js: app/coffee/%.coffee
 	@mkdir -p $(@D)
 	$(COFFEE) --compile -o $(@D) $<
 
-public/js/%.js: public/coffee/%.coffee
+public/js/%.js: public/src/%.js
 	@mkdir -p $(@D)
-	$(COFFEE) --output $(@D) --map --compile $<
+	$(BABEL) $< --out-file $@
 
 test/unit/js/%.js: test/unit/coffee/%.coffee
 	@mkdir -p $(@D)
@@ -46,55 +42,49 @@ test/acceptance/js/%.js: test/acceptance/coffee/%.coffee
 	@mkdir -p $(@D)
 	$(COFFEE) --compile -o $(@D) $<
 
-test/unit_frontend/js/%.js: test/unit_frontend/coffee/%.coffee
+test/unit_frontend/js/%.js: test/unit_frontend/src/%.js
 	@mkdir -p $(@D)
-	$(COFFEE) --compile -o $(@D) $< 
+	$(BABEL) $< --out-file $@
 
 test/smoke/js/%.js: test/smoke/coffee/%.coffee
 	@mkdir -p $(@D)
 	$(COFFEE) --compile -o $(@D) $<
 
-public/js/libs/sharejs.js: $(SHAREJS_COFFEE_FILES)
-	@echo "Compiling public/js/libs/sharejs.js"
-	@echo 'define(["ace/ace"], function() {' > public/js/libs/sharejs.js
-	@cat $(SHAREJS_COFFEE_FILES) | $(COFFEE) --stdio --print >> public/js/libs/sharejs.js
-	@echo "" >> public/js/libs/sharejs.js
-	@echo "return window.sharejs; });" >> public/js/libs/sharejs.js
 
-public/js/ide.js: public/coffee/ide.coffee $(MODULE_IDE_COFFEE_FILES)
+public/js/ide.js: public/src/ide.js $(MODULE_IDE_SRC_FILES)
 	@echo Compiling and injecting module includes into public/js/ide.js
 	@INCLUDES=""; \
 	for dir in modules/*; \
 	do \
 		MODULE=`echo $$dir | cut -d/ -f2`; \
-		if [ -e $$dir/public/coffee/ide/index.coffee ]; then \
+		if [ -e $$dir/public/src/ide/index.js ]; then \
 			INCLUDES="\"ide/$$MODULE/index\",$$INCLUDES"; \
 		fi \
 	done; \
 	INCLUDES=$${INCLUDES%?}; \
-	$(COFFEE) --compile --print $< | \
-		sed -e s=\"__IDE_CLIENTSIDE_INCLUDES__\"=$$INCLUDES= \
+	$(BABEL) $< | \
+		sed -e s=\'__IDE_CLIENTSIDE_INCLUDES__\'=$$INCLUDES= \
 		> $@
 
-public/js/main.js: public/coffee/main.coffee $(MODULE_MAIN_COFFEE_FILES)
+public/js/main.js: public/src/main.js $(MODULE_MAIN_SRC_FILES)
 	@echo Compiling and injecting module includes into public/js/main.js
 	@INCLUDES=""; \
 	for dir in modules/*; \
 	do \
 		MODULE=`echo $$dir | cut -d/ -f2`; \
-		if [ -e $$dir/public/coffee/main/index.coffee ]; then \
+		if [ -e $$dir/public/src/main/index.js ]; then \
 			INCLUDES="\"main/$$MODULE/index\",$$INCLUDES"; \
 		fi \
 	done; \
 	INCLUDES=$${INCLUDES%?}; \
-	$(COFFEE) --compile --print $< | \
-		sed -e s=\"__MAIN_CLIENTSIDE_INCLUDES__\"=$$INCLUDES= \
+	$(BABEL) $< | \
+		sed -e s=\'__MAIN_CLIENTSIDE_INCLUDES__\'=$$INCLUDES= \
 		> $@
 
 $(CSS_FILES): $(LESS_FILES)
 	$(GRUNT) compile:css
 
-minify: $(CSS_FILES) $(JS_FILES)
+minify: $(CSS_FILES) $(JS_FILES) $(OUTPUT_SRC_FILES)
 	$(GRUNT) compile:minify
 	$(MAKE) minify_es
 
@@ -103,17 +93,17 @@ minify_es:
 
 css: $(CSS_FILES)
 
-compile: $(JS_FILES) css public/js/libs/sharejs.js public/js/main.js public/js/ide.js
+compile: $(JS_FILES) $(OUTPUT_SRC_FILES) css public/js/main.js public/js/ide.js
 	@$(MAKE) compile_modules
 
 compile_full:
 	$(COFFEE) -c -p app.coffee > app.js
 	$(COFFEE) -o app/js -c app/coffee
-	$(COFFEE) -o public/js -c public/coffee
+	$(BABEL) public/src --out-dir public/js
 	$(COFFEE) -o test/acceptance/js -c test/acceptance/coffee
 	$(COFFEE) -o test/smoke/js -c test/smoke/coffee
 	$(COFFEE) -o test/unit/js -c test/unit/coffee
-	$(COFFEE) -o test/unit_frontend/js -c test/unit_frontend/coffee
+	$(BABEL) test/unit_frontend/src --out-dir test/unit_frontend/js
 	rm -f public/js/ide.js public/js/main.js # We need to generate ide.js, main.js manually later
 	$(MAKE) $(CSS_FILES)
 	$(MAKE) compile_modules_full
@@ -160,7 +150,6 @@ clean_app:
 clean_frontend:
 	rm -rf public/js/{analytics,directives,es,filters,ide,main,modules,services,utils}
 	rm -f public/js/*.{js,map}
-	rm -f public/js/libs/sharejs.{js,map}
 
 clean_tests:
 	rm -rf test/unit/js
@@ -239,6 +228,9 @@ ci:
 	MOCHA_ARGS="--reporter tap" \
 	$(MAKE) test
 
+format:
+	npm -q run format
+
 lint:
 	npm -q run lint
 

+ 22 - 18
services/web/Makefile.module

@@ -1,19 +1,22 @@
 MODULE_NAME := $(notdir $(shell pwd))
 MODULE_DIR := modules/$(MODULE_NAME)
 COFFEE := ../../node_modules/.bin/coffee
+BABEL := ../../node_modules/.bin/babel
+
 APP_COFFEE_FILES := $(shell [ -e app/coffee ] && find app/coffee -name '*.coffee') \
 	$(shell [ -e test/unit/coffee ] && find test/unit/coffee -name '*.coffee') \
 	$(shell [ -e test/acceptance/coffee ] && find test/acceptance/coffee -name '*.coffee')
 APP_JS_FILES := $(subst coffee,js,$(APP_COFFEE_FILES))
-IDE_COFFEE_FILES := $(shell [ -e public/coffee/ide ] && find public/coffee/ide -name '*.coffee')
-IDE_JS_FILES := $(subst public/coffee/ide,../../public/js/ide/$(MODULE_NAME),$(IDE_COFFEE_FILES))
-IDE_JS_FILES := $(subst coffee,js,$(IDE_JS_FILES))
-IDE_TEST_COFFEE_FILES := $(shell [ -e test/unit_frontend/coffee ] && find test/unit_frontend/coffee -name '*.coffee')
-IDE_TEST_JS_FILES := $(subst test/unit_frontend/coffee/ide,../../test/unit_frontend/js/ide/$(MODULE_NAME),$(IDE_TEST_COFFEE_FILES))
-IDE_TEST_JS_FILES := $(subst coffee,js,$(IDE_TEST_JS_FILES))
-MAIN_COFFEE_FILES := $(shell [ -e public/coffee/main ] && find public/coffee/main -name '*.coffee')
-MAIN_JS_FILES := $(subst public/coffee/main,../../public/js/main/$(MODULE_NAME),$(MAIN_COFFEE_FILES))
-MAIN_JS_FILES := $(subst coffee,js,$(MAIN_JS_FILES))
+
+IDE_SRC_FILES := $(shell [ -e public/src/ide ] && find public/src/ide -name '*.js')
+IDE_OUTPUT_FILES := $(subst public/src/ide,../../public/js/ide/$(MODULE_NAME),$(IDE_SRC_FILES))
+
+IDE_TEST_SRC_FILES := $(shell [ -e test/unit_frontend/src/ide ] && find test/unit_frontend/src/ide -name '*.js')
+IDE_TEST_OUTPUT_FILES := $(subst test/unit_frontend/src/ide,../../test/unit_frontend/js/ide/$(MODULE_NAME),$(IDE_TEST_SRC_FILES))
+
+MAIN_SRC_FILES := $(shell [ -e public/src/main ] && find public/src/main -name '*.js')
+MAIN_OUTPUT_FILES := $(subst public/src/main,../../public/js/main/$(MODULE_NAME),$(MAIN_SRC_FILES))
+
 DOCKER_COMPOSE_FLAGS := -f $(MODULE_DIR)/docker-compose.yml
 DOCKER_COMPOSE := cd ../../ && MODULE_DIR=$(MODULE_DIR) docker-compose -f docker-compose.yml ${DOCKER_COMPOSE_FLAGS}
 
@@ -29,30 +32,31 @@ test/acceptance/js/%.js: test/acceptance/coffee/%.coffee
 	@mkdir -p $(dir $@)
 	$(COFFEE) --compile --print $< > $@
 
-../../test/unit_frontend/js/ide/$(MODULE_NAME)/%.js: test/unit_frontend/coffee/ide/%.coffee
+../../test/unit_frontend/js/ide/$(MODULE_NAME)/%.js: test/unit_frontend/src/ide/%.js
 	@mkdir -p $(dir $@)
-	$(COFFEE) --compile --print $< > $@
+	$(BABEL) $< --out-file $@
 
-../../public/js/ide/$(MODULE_NAME)/%.js: public/coffee/ide/%.coffee
+../../public/js/ide/$(MODULE_NAME)/%.js: public/src/ide/%.js
 	@mkdir -p $(dir $@)
-	$(COFFEE) --compile --print $< > $@
+	$(BABEL) $< --out-file $@
 
-../../public/js/main/$(MODULE_NAME)/%.js: public/coffee/main/%.coffee
+../../public/js/main/$(MODULE_NAME)/%.js: public/src/main/%.js
 	@mkdir -p $(dir $@)
-	$(COFFEE) --compile --print $< > $@
+	$(BABEL) $< --out-file $@
 
 index.js: index.coffee
 	$(COFFEE) --compile --print $< > $@
 
-compile: $(APP_JS_FILES) $(IDE_JS_FILES) $(MAIN_JS_FILES) $(IDE_TEST_JS_FILES) index.js
+compile: $(APP_JS_FILES) $(IDE_OUTPUT_FILES) $(MAIN_OUTPUT_FILES) $(IDE_TEST_OUTPUT_FILES) index.js
 	@echo > /dev/null
 
 compile_full:
 	if [ -e app/coffee ]; then $(COFFEE) -o app/js -c app/coffee; fi
 	if [ -e test/unit/coffee ]; then $(COFFEE) -o test/unit/js -c test/unit/coffee; fi
 	if [ -e test/acceptance/coffee ]; then $(COFFEE) -o test/acceptance/js -c test/acceptance/coffee; fi
-	if [ -e public/coffee/ide/ ]; then $(COFFEE) -o ../../public/js/ide/$(MODULE_NAME) -c public/coffee/ide/; fi
-	if [ -e public/coffee/main/ ]; then $(COFFEE) -o ../../public/js/main/$(MODULE_NAME) -c public/coffee/main/; fi
+	if [ -e public/src/ide ]; then $(BABEL) public/src/ide --out-dir ../../public/js/ide/$(MODULE_NAME); fi
+	if [ -e public/src/main ]; then $(BABEL) public/src/main --out-dir ../../public/js/main/$(MODULE_NAME); fi
+	if [ -e test/unit_frontend/src/ide ]; then $(BABEL) test/unit_frontend/src/ide --out-dir ../../test/unit_frontend/js/ide/$(MODULE_NAME); fi
 	@$(MAKE) compile
 
 test_acceptance: test_acceptance_start_service test_acceptance_run

+ 57 - 0
services/web/decaffeinate.sh

@@ -0,0 +1,57 @@
+set -ex
+
+npx bulk-decaffeinate convert --dir public/coffee
+
+for module in modules/**/public/coffee; do
+  npx bulk-decaffeinate convert --dir $module
+done
+
+npx bulk-decaffeinate clean
+
+git mv public/coffee public/src
+
+for module in modules/**/public; do
+  if [ -e $module/coffee ]; then
+    git mv $module/coffee $module/src
+  fi
+done
+
+git commit -m "Rename public/coffee dir to public/src"
+
+npx prettier-eslint 'public/src/**/*.js' --write
+
+for module in modules/**/public/src; do
+  npx prettier-eslint "$module/**/*.js" --write
+done
+
+git add .
+git commit -m "Prettier: convert public/src decaffeinated files to Prettier format"
+
+npx bulk-decaffeinate convert --dir test/unit_frontend/coffee
+
+for module in modules/**/test/unit_frontend/coffee; do
+  npx bulk-decaffeinate convert --dir $module
+done
+
+npx bulk-decaffeinate clean
+
+git mv test/unit_frontend/coffee test/unit_frontend/src
+
+for module in modules/**/test/unit_frontend; do
+  if [ -e $module/coffee ]; then
+    git mv $module/coffee $module/src
+  fi
+done
+
+git commit -m "Rename test/unit_frontend/coffee to test/unit_frontend/src"
+
+npx prettier-eslint 'test/unit_frontend/src/**/*.js' --write
+
+for module in modules/**/test/unit_frontend/src; do
+  npx prettier-eslint "$module/**/*.js" --write
+done
+
+git add .
+git commit -m "Prettier: convert test/unit_frontend decaffeinated files to Prettier format"
+
+echo "done"

+ 3 - 3
services/web/nodemon.frontend.json

@@ -6,9 +6,9 @@
   "verbose": true,
   "exec": "make compile || exit 1",
   "watch": [
-    "public/coffee/",
+    "public/src/",
     "public/stylesheets/",
-    "modules/**/public/coffee/"
+    "modules/**/public/src/"
   ],
-  "ext": "coffee less"
+  "ext": "js less"
 }

+ 412 - 0
services/web/npm-shrinkwrap.json

@@ -459,6 +459,38 @@
       "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz",
       "dev": true
     },
+    "babel-cli": {
+      "version": "6.26.0",
+      "from": "babel-cli@latest",
+      "resolved": "https://registry.npmjs.org/babel-cli/-/babel-cli-6.26.0.tgz",
+      "dev": true,
+      "dependencies": {
+        "commander": {
+          "version": "2.19.0",
+          "from": "commander@>=2.11.0 <3.0.0",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
+          "dev": true
+        },
+        "glob": {
+          "version": "7.1.3",
+          "from": "glob@>=7.1.2 <8.0.0",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+          "dev": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "from": "minimatch@>=3.0.4 <4.0.0",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.5.7",
+          "from": "source-map@>=0.5.6 <0.6.0",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "dev": true
+        }
+      }
+    },
     "babel-code-frame": {
       "version": "6.26.0",
       "from": "babel-code-frame@>=6.26.0 <7.0.0",
@@ -817,6 +849,20 @@
       "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz",
       "dev": true
     },
+    "babel-polyfill": {
+      "version": "6.26.0",
+      "from": "babel-polyfill@>=6.13.0 <7.0.0",
+      "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz",
+      "dev": true,
+      "dependencies": {
+        "regenerator-runtime": {
+          "version": "0.10.5",
+          "from": "regenerator-runtime@>=0.10.5 <0.11.0",
+          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz",
+          "dev": true
+        }
+      }
+    },
     "babel-preset-env": {
       "version": "1.7.0",
       "from": "babel-preset-env@>=1.6.1 <2.0.0",
@@ -1138,6 +1184,12 @@
         }
       }
     },
+    "boolify": {
+      "version": "1.0.1",
+      "from": "boolify@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/boolify/-/boolify-1.0.1.tgz",
+      "dev": true
+    },
     "boom": {
       "version": "4.3.1",
       "from": "boom@>=4.0.0 <5.0.0",
@@ -1737,6 +1789,12 @@
       "from": "commander@2.1.0",
       "resolved": "https://registry.npmjs.org/commander/-/commander-2.1.0.tgz"
     },
+    "common-tags": {
+      "version": "1.8.0",
+      "from": "common-tags@>=1.4.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz",
+      "dev": true
+    },
     "commondir": {
       "version": "1.0.1",
       "from": "commondir@>=1.0.1 <2.0.0",
@@ -2473,6 +2531,12 @@
       "resolved": "https://registry.npmjs.org/director/-/director-1.2.7.tgz",
       "dev": true
     },
+    "dlv": {
+      "version": "1.1.2",
+      "from": "dlv@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.2.tgz",
+      "dev": true
+    },
     "dns-equal": {
       "version": "1.0.0",
       "from": "dns-equal@>=1.0.0 <2.0.0",
@@ -2947,6 +3011,20 @@
         }
       }
     },
+    "eslint-config-prettier": {
+      "version": "3.1.0",
+      "from": "eslint-config-prettier@latest",
+      "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-3.1.0.tgz",
+      "dev": true,
+      "dependencies": {
+        "get-stdin": {
+          "version": "6.0.0",
+          "from": "get-stdin@>=6.0.0 <7.0.0",
+          "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz",
+          "dev": true
+        }
+      }
+    },
     "eslint-config-standard": {
       "version": "11.0.0",
       "from": "eslint-config-standard@>=11.0.0 <12.0.0",
@@ -3101,6 +3179,12 @@
         }
       }
     },
+    "eslint-plugin-prettier": {
+      "version": "2.7.0",
+      "from": "eslint-plugin-prettier@>=2.7.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-2.7.0.tgz",
+      "dev": true
+    },
     "eslint-plugin-promise": {
       "version": "3.7.0",
       "from": "eslint-plugin-promise@>=3.6.0 <4.0.0",
@@ -3441,6 +3525,12 @@
       "from": "fast-deep-equal@>=1.0.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz"
     },
+    "fast-diff": {
+      "version": "1.2.0",
+      "from": "fast-diff@>=1.1.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
+      "dev": true
+    },
     "fast-json-stable-stringify": {
       "version": "2.0.0",
       "from": "fast-json-stable-stringify@>=2.0.0 <3.0.0",
@@ -3714,6 +3804,12 @@
       "from": "fs-minipass@>=1.2.5 <2.0.0",
       "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz"
     },
+    "fs-readdir-recursive": {
+      "version": "1.1.0",
+      "from": "fs-readdir-recursive@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
+      "dev": true
+    },
     "fs.realpath": {
       "version": "1.0.0",
       "from": "fs.realpath@>=1.0.0 <2.0.0",
@@ -5414,6 +5510,12 @@
         }
       }
     },
+    "jest-docblock": {
+      "version": "21.2.0",
+      "from": "jest-docblock@>=21.0.0 <22.0.0",
+      "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.2.0.tgz",
+      "dev": true
+    },
     "jmespath": {
       "version": "0.15.0",
       "from": "jmespath@0.15.0",
@@ -6191,6 +6293,18 @@
       "from": "lodash.keys@>=4.2.0 <5.0.0",
       "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-4.2.0.tgz"
     },
+    "lodash.memoize": {
+      "version": "4.1.2",
+      "from": "lodash.memoize@>=4.1.2 <5.0.0",
+      "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+      "dev": true
+    },
+    "lodash.merge": {
+      "version": "4.6.1",
+      "from": "lodash.merge@>=4.6.0 <5.0.0",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz",
+      "dev": true
+    },
     "lodash.mergewith": {
       "version": "4.6.1",
       "from": "lodash.mergewith@>=4.6.0 <5.0.0",
@@ -6232,6 +6346,12 @@
       "from": "lodash.shuffle@>=4.2.0 <5.0.0",
       "resolved": "https://registry.npmjs.org/lodash.shuffle/-/lodash.shuffle-4.2.0.tgz"
     },
+    "lodash.unescape": {
+      "version": "4.0.1",
+      "from": "lodash.unescape@4.0.1",
+      "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz",
+      "dev": true
+    },
     "lodash.values": {
       "version": "4.3.0",
       "from": "lodash.values@>=4.3.0 <5.0.0",
@@ -6764,6 +6884,12 @@
       "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz",
       "dev": true
     },
+    "loglevel-colored-level-prefix": {
+      "version": "1.0.0",
+      "from": "loglevel-colored-level-prefix@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz",
+      "dev": true
+    },
     "lolex": {
       "version": "1.3.2",
       "from": "lolex@1.3.2",
@@ -6879,6 +7005,12 @@
       "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
       "dev": true
     },
+    "make-plural": {
+      "version": "4.3.0",
+      "from": "make-plural@>=4.1.1 <5.0.0",
+      "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-4.3.0.tgz",
+      "dev": true
+    },
     "mandrill-api": {
       "version": "1.0.45",
       "from": "mandrill-api@>=1.0.45 <2.0.0",
@@ -7040,6 +7172,32 @@
       "from": "mersenne@>=0.0.3 <0.1.0",
       "resolved": "https://registry.npmjs.org/mersenne/-/mersenne-0.0.4.tgz"
     },
+    "messageformat": {
+      "version": "1.1.1",
+      "from": "messageformat@>=1.0.2 <2.0.0",
+      "resolved": "https://registry.npmjs.org/messageformat/-/messageformat-1.1.1.tgz",
+      "dev": true,
+      "dependencies": {
+        "glob": {
+          "version": "7.0.6",
+          "from": "glob@>=7.0.6 <7.1.0",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz",
+          "dev": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "from": "minimatch@^3.0.2",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "dev": true
+        }
+      }
+    },
+    "messageformat-parser": {
+      "version": "1.1.0",
+      "from": "messageformat-parser@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/messageformat-parser/-/messageformat-parser-1.1.0.tgz",
+      "dev": true
+    },
     "method-override": {
       "version": "2.3.10",
       "from": "method-override@>=2.3.3 <3.0.0",
@@ -8393,6 +8551,12 @@
       "from": "osenv@>=0.1.4 <0.2.0",
       "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz"
     },
+    "output-file-sync": {
+      "version": "1.1.2",
+      "from": "output-file-sync@>=1.1.2 <2.0.0",
+      "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz",
+      "dev": true
+    },
     "p-finally": {
       "version": "1.0.0",
       "from": "p-finally@>=1.0.0 <2.0.0",
@@ -8817,6 +8981,172 @@
       "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz",
       "dev": true
     },
+    "prettier": {
+      "version": "1.14.3",
+      "from": "prettier@>=1.7.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.14.3.tgz",
+      "dev": true
+    },
+    "prettier-eslint": {
+      "version": "8.8.2",
+      "from": "prettier-eslint@>=8.5.0 <9.0.0",
+      "resolved": "https://registry.npmjs.org/prettier-eslint/-/prettier-eslint-8.8.2.tgz",
+      "dev": true,
+      "dependencies": {
+        "indent-string": {
+          "version": "3.2.0",
+          "from": "indent-string@^3.2.0",
+          "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz",
+          "dev": true
+        }
+      }
+    },
+    "prettier-eslint-cli": {
+      "version": "4.7.1",
+      "from": "prettier-eslint-cli@latest",
+      "resolved": "https://registry.npmjs.org/prettier-eslint-cli/-/prettier-eslint-cli-4.7.1.tgz",
+      "dev": true,
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "from": "ansi-regex@^3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "3.2.1",
+          "from": "ansi-styles@>=3.1.0 <4.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "dev": true
+        },
+        "camelcase": {
+          "version": "4.1.0",
+          "from": "camelcase@>=4.1.0 <5.0.0",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+          "dev": true
+        },
+        "camelcase-keys": {
+          "version": "4.2.0",
+          "from": "camelcase-keys@>=4.1.0 <5.0.0",
+          "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz",
+          "dev": true
+        },
+        "chalk": {
+          "version": "2.3.0",
+          "from": "chalk@2.3.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz",
+          "dev": true
+        },
+        "cliui": {
+          "version": "3.2.0",
+          "from": "cliui@>=3.2.0 <4.0.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+          "dev": true,
+          "dependencies": {
+            "string-width": {
+              "version": "1.0.2",
+              "from": "string-width@>=1.0.1 <2.0.0",
+              "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
+              "dev": true
+            }
+          }
+        },
+        "get-stdin": {
+          "version": "5.0.1",
+          "from": "get-stdin@>=5.0.1 <6.0.0",
+          "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz",
+          "dev": true
+        },
+        "glob": {
+          "version": "7.1.3",
+          "from": "glob@>=7.1.1 <8.0.0",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+          "dev": true
+        },
+        "has-flag": {
+          "version": "2.0.0",
+          "from": "has-flag@>=2.0.0 <3.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
+          "dev": true
+        },
+        "indent-string": {
+          "version": "3.2.0",
+          "from": "indent-string@>=3.1.0 <4.0.0",
+          "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz",
+          "dev": true
+        },
+        "map-obj": {
+          "version": "2.0.0",
+          "from": "map-obj@>=2.0.0 <3.0.0",
+          "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz",
+          "dev": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "from": "minimatch@>=3.0.4 <4.0.0",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "dev": true
+        },
+        "string-width": {
+          "version": "2.1.1",
+          "from": "string-width@>=2.0.0 <3.0.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+          "dev": true,
+          "dependencies": {
+            "is-fullwidth-code-point": {
+              "version": "2.0.0",
+              "from": "is-fullwidth-code-point@>=2.0.0 <3.0.0",
+              "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+              "dev": true
+            },
+            "strip-ansi": {
+              "version": "4.0.0",
+              "from": "strip-ansi@>=4.0.0 <5.0.0",
+              "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+              "dev": true
+            }
+          }
+        },
+        "supports-color": {
+          "version": "4.5.0",
+          "from": "supports-color@>=4.0.0 <5.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz",
+          "dev": true
+        },
+        "yargs": {
+          "version": "10.0.3",
+          "from": "yargs@10.0.3",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-10.0.3.tgz",
+          "dev": true
+        },
+        "yargs-parser": {
+          "version": "8.1.0",
+          "from": "yargs-parser@>=8.0.0 <9.0.0",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz",
+          "dev": true
+        }
+      }
+    },
+    "pretty-format": {
+      "version": "23.6.0",
+      "from": "pretty-format@>=23.0.1 <24.0.0",
+      "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz",
+      "dev": true,
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "from": "ansi-regex@>=3.0.0 <4.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "dev": true
+        },
+        "ansi-styles": {
+          "version": "3.2.1",
+          "from": "ansi-styles@^3.2.0",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "dev": true
+        }
+      }
+    },
     "private": {
       "version": "0.1.8",
       "from": "private@>=0.1.7 <0.2.0",
@@ -9097,6 +9427,12 @@
       "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-0.0.4.tgz",
       "dev": true
     },
+    "quick-lru": {
+      "version": "1.1.0",
+      "from": "quick-lru@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz",
+      "dev": true
+    },
     "ramda": {
       "version": "0.25.0",
       "from": "ramda@>=0.25.0 <0.26.0",
@@ -9459,6 +9795,12 @@
       "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
       "dev": true
     },
+    "require-relative": {
+      "version": "0.8.7",
+      "from": "require-relative@>=0.8.7 <0.9.0",
+      "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz",
+      "dev": true
+    },
     "require-uncached": {
       "version": "1.0.3",
       "from": "require-uncached@>=1.0.3 <2.0.0",
@@ -9484,6 +9826,12 @@
       "from": "requires-port@>=1.0.0 <2.0.0",
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz"
     },
+    "reserved-words": {
+      "version": "0.1.2",
+      "from": "reserved-words@>=0.1.2 <0.2.0",
+      "resolved": "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz",
+      "dev": true
+    },
     "resolve": {
       "version": "1.5.0",
       "from": "resolve@>=1.1.6 <2.0.0",
@@ -9601,6 +9949,12 @@
       "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz",
       "dev": true
     },
+    "rxjs": {
+      "version": "5.5.12",
+      "from": "rxjs@>=5.3.0 <6.0.0",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz",
+      "dev": true
+    },
     "safe-buffer": {
       "version": "5.1.1",
       "from": "safe-buffer@>=5.1.1 <5.2.0",
@@ -10546,6 +10900,12 @@
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
       "dev": true
     },
+    "symbol-observable": {
+      "version": "1.0.1",
+      "from": "symbol-observable@1.0.1",
+      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz",
+      "dev": true
+    },
     "table": {
       "version": "4.0.2",
       "from": "table@>=4.0.1 <5.0.0",
@@ -10925,6 +11285,26 @@
       "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
       "dev": true
     },
+    "typescript": {
+      "version": "2.9.2",
+      "from": "typescript@>=2.5.1 <3.0.0",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz",
+      "dev": true
+    },
+    "typescript-eslint-parser": {
+      "version": "16.0.1",
+      "from": "typescript-eslint-parser@>=16.0.0 <17.0.0",
+      "resolved": "https://registry.npmjs.org/typescript-eslint-parser/-/typescript-eslint-parser-16.0.1.tgz",
+      "dev": true,
+      "dependencies": {
+        "semver": {
+          "version": "5.5.0",
+          "from": "semver@5.5.0",
+          "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
+          "dev": true
+        }
+      }
+    },
     "ua-parser-js": {
       "version": "0.7.17",
       "from": "ua-parser-js@>=0.7.9 <0.8.0",
@@ -11204,6 +11584,12 @@
         }
       }
     },
+    "user-home": {
+      "version": "1.1.1",
+      "from": "user-home@>=1.1.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz",
+      "dev": true
+    },
     "useragent": {
       "version": "2.2.1",
       "from": "useragent@2.2.1",
@@ -11411,6 +11797,12 @@
         }
       }
     },
+    "v8flags": {
+      "version": "2.1.1",
+      "from": "v8flags@>=2.1.1 <3.0.0",
+      "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.1.1.tgz",
+      "dev": true
+    },
     "valid-url": {
       "version": "1.0.9",
       "from": "valid-url@>=1.0.9 <2.0.0",
@@ -11482,6 +11874,26 @@
       "from": "void-elements@>=2.0.1 <3.0.0",
       "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz"
     },
+    "vue-eslint-parser": {
+      "version": "2.0.3",
+      "from": "vue-eslint-parser@>=2.0.2 <3.0.0",
+      "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz",
+      "dev": true,
+      "dependencies": {
+        "debug": {
+          "version": "3.2.6",
+          "from": "debug@>=3.1.0 <4.0.0",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "dev": true
+        },
+        "ms": {
+          "version": "2.1.1",
+          "from": "ms@>=2.1.1 <3.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+          "dev": true
+        }
+      }
+    },
     "watch": {
       "version": "0.13.0",
       "from": "watch@>=0.13.0 <0.14.0",

+ 6 - 1
services/web/package.json

@@ -23,7 +23,8 @@
     "nodemon:frontend": "nodemon --config nodemon.frontend.json",
     "webpack": "webpack-dev-server --config webpack.config.dev.js",
     "webpack:production": "webpack --config webpack.config.prod.js",
-    "lint": "eslint -f unix ."
+    "lint": "eslint -f unix .",
+    "format": "prettier-eslint '**/*.js' --list-different"
   },
   "dependencies": {
     "archiver": "0.9.0",
@@ -108,6 +109,7 @@
   },
   "devDependencies": {
     "autoprefixer": "^6.6.1",
+    "babel-cli": "^6.26.0",
     "babel-core": "^6.26.0",
     "babel-loader": "^7.1.2",
     "babel-preset-env": "^1.6.1",
@@ -119,6 +121,7 @@
     "coffee-script": "^1.7.1",
     "es6-promise": "^4.0.5",
     "eslint": "^4.18.1",
+    "eslint-config-prettier": "^3.1.0",
     "eslint-config-standard": "^11.0.0",
     "eslint-config-standard-jsx": "^5.0.0",
     "eslint-config-standard-react": "^6.0.0",
@@ -128,6 +131,7 @@
     "eslint-plugin-jsx-a11y": "^6.1.2",
     "eslint-plugin-mocha": "^5.2.0",
     "eslint-plugin-node": "^6.0.0",
+    "eslint-plugin-prettier": "^2.7.0",
     "eslint-plugin-promise": "^3.6.0",
     "eslint-plugin-react": "^7.11.1",
     "eslint-plugin-standard": "^3.0.1",
@@ -162,6 +166,7 @@
     "karma-webpack": "^2.0.9",
     "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
     "nodemon": "^1.14.3",
+    "prettier-eslint-cli": "^4.7.1",
     "requirejs": "^2.1.22",
     "sandboxed-module": "0.2.0",
     "sinon": "^1.17.0",

+ 0 - 71
services/web/public/coffee/analytics/AbTestingManager.coffee

@@ -1,71 +0,0 @@
-define [
-	"base"
-	"libs/md5"
-], (App) ->
-	oldKeys = [
-		"sl_abt_multi_currency_editor_eu-eu"
-		"sl_abt_multi_currency_eu-eu"
-		"sl_abt_multi_currency_editor_eu-usd"
-		"sl_abt_multi_currency_eu-usd"
-		"sl_abt_trial_len_14d"
-		"sl_abt_trial_len_7d"
-		"sl_abt_trial_len_30d"
-		"sl_utt"
-		"sl_utt_trial_len"
-		"sl_utt_multi_currency"
-	]
-
-	App.factory "abTestManager", ($http, ipCookie) ->
-		
-		_.each oldKeys, (oldKey)->
-			ipCookie.remove(oldKey)
-
-		_buildCookieKey = (testName, bucket)-> 
-			key = "sl_abt_#{testName}_#{bucket}"
-			return key
-
-
-		_getTestCookie = (testName, bucket)->
-			cookieKey = _buildCookieKey(testName, bucket)
-			cookie =  ipCookie(cookieKey)
-			return cookie
-
-		_persistCookieStep = (testName, bucket, newStep)->
-			cookieKey = _buildCookieKey(testName, bucket)
-			ipCookie(cookieKey, {step:newStep}, {expires:100, path:"/"})
-			ga('send', 'event', 'ab_tests', "#{testName}:#{bucket}", "step-#{newStep}")
-
-		_checkIfStepIsNext = (cookieStep, newStep)->
-			if !cookieStep? and newStep != 0
-				return false
-			else if newStep == 0
-				return true
-			else if (cookieStep+1) == newStep
-				return true
-			else 
-				return false
-
-		_getUsersHash = (testName)->
-			sl_user_test_token = "sl_utt_#{testName}"
-			user_uuid = ipCookie(sl_user_test_token)
-			if !user_uuid?
-				user_uuid = Math.random()
-				ipCookie(sl_user_test_token, user_uuid, {expires:365, path:"/"})
-			hash = CryptoJS.MD5("#{user_uuid}:#{testName}")
-			return hash
-
-		processTestWithStep: processTestWithStep = (testName, bucket, newStep)->
-			currentCookieStep = _getTestCookie(testName, bucket)?.step
-			if _checkIfStepIsNext(currentCookieStep, newStep)
-				_persistCookieStep(testName, bucket, newStep)
-
-		getABTestBucket: getABTestBucket = (test_name, buckets) ->
-			hash = _getUsersHash(test_name)
-			bucketIndex = parseInt(hash.toString().slice(0,2), 16) % (buckets?.length or 2)
-			return buckets[bucketIndex]
-
-	App.controller "AbTestController", ($scope, abTestManager)->
-		testKeys = _.keys(window.ab)
-
-		_.each window.ab, (event)->
-			abTestManager.processTestWithStep event.testName, event.bucket, event.step

+ 0 - 61
services/web/public/coffee/base.coffee

@@ -1,61 +0,0 @@
-define [
-	"libraries"
-	"modules/recursionHelper"
-	"modules/errorCatcher"
-	"modules/localStorage"
-	"utils/underscore"
-], () ->
-	App = angular.module("SharelatexApp", [
-		"ui.bootstrap"
-		"autocomplete"
-		"RecursionHelper"
-		"ng-context-menu"
-		"underscore"
-		"ngSanitize"
-		"ipCookie"
-		"mvdSixpack"
-		"ErrorCatcher"
-		"localStorage"
-		"ngTagsInput"
-		"ui.select"
-	]).config ($qProvider, sixpackProvider, $httpProvider, uiSelectConfig) ->
-		$qProvider.errorOnUnhandledRejections(false)
-		uiSelectConfig.spinnerClass = 'fa fa-refresh ui-select-spin'
-		sixpackProvider.setOptions({
-			debug: false
-			baseUrl: window.sharelatex.sixpackDomain
-			client_id: window.user_id
-		})
-
-		MathJax?.Hub?.Config(
-			messageStyle: "none"
-			imageFont:null
-			"HTML-CSS":
-				availableFonts: ["TeX"]
-				# MathJax's automatic font scaling does not work well when we render math
-				# that isn't yet on the page, so we disable it and set a global font
-				# scale factor
-				scale: 110
-				matchFontHeight: false
-			TeX:
-				equationNumbers: { autoNumber: "AMS" }
-				useLabelIDs: false
-			skipStartupTypeset: true
-			tex2jax:
-				processEscapes: true
-				# Dollar delimiters are added by the mathjax directive
-				inlineMath: [ ["\\(","\\)"] ]
-				displayMath: [ ['$$','$$'], ["\\[","\\]"] ]
-		)
-
-	App.run ($templateCache) ->
-		# UI Select templates are hard-coded and use Glyphicon icons (which we don't import). 
-		# The line below simply overrides the hard-coded template with our own, which is 
-		# basically the same but using Font Awesome icons.
-		$templateCache.put "bootstrap/match.tpl.html", "<div class=\"ui-select-match\" ng-hide=\"$select.open && $select.searchEnabled\" ng-disabled=\"$select.disabled\" ng-class=\"{\'btn-default-focus\':$select.focus}\"><span tabindex=\"-1\" class=\"btn btn-default form-control ui-select-toggle\" aria-label=\"{{ $select.baseTitle }} activate\" ng-disabled=\"$select.disabled\" ng-click=\"$select.activate()\" style=\"outline: 0;\"><span ng-show=\"$select.isEmpty()\" class=\"ui-select-placeholder text-muted\">{{$select.placeholder}}</span> <span ng-hide=\"$select.isEmpty()\" class=\"ui-select-match-text pull-left\" ng-class=\"{\'ui-select-allow-clear\': $select.allowClear && !$select.isEmpty()}\" ng-transclude=\"\"></span> <i class=\"caret pull-right\" ng-click=\"$select.toggle($event)\"></i> <a ng-show=\"$select.allowClear && !$select.isEmpty() && ($select.disabled !== true)\" aria-label=\"{{ $select.baseTitle }} clear\" style=\"margin-right: 10px\" ng-click=\"$select.clear($event)\" class=\"btn btn-xs btn-link pull-right\"><i class=\"fa fa-times\" aria-hidden=\"true\"></i></a></span></div>"
-
-	sl_debugging = window.location?.search?.match(/debug=true/)?
-	window.sl_console =
-		log: (args...) -> console.log(args...) if sl_debugging
-
-	return App

+ 0 - 74
services/web/public/coffee/components/inputSuggestions.coffee

@@ -1,74 +0,0 @@
-define [
-	"base"
-], (App) ->
-	inputSuggestionsController = ($scope, $element, $attrs, Keys) ->
-		ctrl = @ 
-		ctrl.showHint = false
-		ctrl.hasFocus = false
-		ctrl.handleFocus = () ->
-			ctrl.hasFocus = true
-			ctrl.suggestion = null
-		ctrl.handleBlur = () ->
-			ctrl.showHint = false
-			ctrl.hasFocus = false
-			ctrl.suggestion = null
-			ctrl.onBlur()
-		ctrl.handleKeyDown = ($event) ->
-			if ($event.which == Keys.TAB or $event.which == Keys.ENTER) and ctrl.suggestion? and ctrl.suggestion != ""
-				$event.preventDefault()
-				ctrl.localNgModel += ctrl.suggestion
-			ctrl.suggestion = null
-			ctrl.showHint = false
-		$scope.$watch "$ctrl.localNgModel", (newVal, oldVal) ->
-			if ctrl.hasFocus and newVal != oldVal
-				ctrl.suggestion = null
-				ctrl.showHint = false
-				ctrl.getSuggestion({ userInput: newVal })
-					.then (suggestion) -> 
-						if suggestion? and newVal == ctrl.localNgModel
-							ctrl.showHint = true
-							ctrl.suggestion = suggestion.replace newVal, ""
-					.catch () -> ctrl.suggestion = null
-		return
-
-	App.component "inputSuggestions", {
-		bindings:
-			localNgModel: "=ngModel"
-			localNgModelOptions: "=?ngModelOptions"
-			getSuggestion: "&"
-			onBlur: "&?"
-			inputId: "@?"
-			inputName: "@?"
-			inputPlaceholder: "@?"
-			inputType: "@?"
-			inputRequired: "=?"
-		controller: inputSuggestionsController
-		template: [
-				'<div class="input-suggestions">',
-					'<div class="form-control input-suggestions-shadow">',
-						'<span ng-bind="$ctrl.localNgModel"',
-							' class="input-suggestions-shadow-existing"',
-							' ng-show="$ctrl.showHint">',
-						'</span>',
-						'<span ng-bind="$ctrl.suggestion"',
-							' class="input-suggestions-shadow-suggested"',
-							' ng-show="$ctrl.showHint">',
-						'</span>',
-					'</div>',
-					'<input type="text"',
-						' class="form-control input-suggestions-main"',
-						' ng-focus="$ctrl.handleFocus()"',
-						' ng-keyDown="$ctrl.handleKeyDown($event)"',
-						' ng-blur="$ctrl.handleBlur()"',
-						' ng-model="$ctrl.localNgModel"',
-						' ng-model-options="$ctrl.localNgModelOptions"',
-						' ng-model-options="{ debounce: 50 }"',
-						' ng-attr-id="{{ ::$ctrl.inputId }}"',
-						' ng-attr-placeholder="{{ ::$ctrl.inputPlaceholder }}"',
-						' ng-attr-type="{{ ::$ctrl.inputType }}"',
-						' ng-attr-name="{{ ::$ctrl.inputName }}"',
-						' ng-required="::$ctrl.inputRequired">',
-				'</div>'
-				].join ""
-				
-	}

+ 0 - 168
services/web/public/coffee/directives/asyncForm.coffee

@@ -1,168 +0,0 @@
-define [
-	"base"
-	"libs/passfield"
-], (App) ->
-	App.directive "asyncForm", ($http, validateCaptcha) ->
-		return {
-			controller: ['$scope', ($scope) ->
-				@getEmail = () ->
-					return $scope.email
-				return this
-			]
-			link: (scope, element, attrs) ->
-				formName = attrs.asyncForm
-
-				scope[attrs.name].response = response = {}
-				scope[attrs.name].inflight = false
-
-				validateCaptchaIfEnabled = (callback = (response) ->) ->
-					if attrs.captcha?
-						validateCaptcha callback
-					else
-						callback()
-
-				submitRequest = (grecaptchaResponse) ->
-					formData = {}
-					for data in element.serializeArray()
-						formData[data.name] = data.value
-
-					if grecaptchaResponse?
-						formData['g-recaptcha-response'] = grecaptchaResponse
-
-					scope[attrs.name].inflight = true
-
-					# for asyncForm prevent automatic redirect to /login if
-					# authentication fails, we will handle it ourselves
-					$http
-						.post(element.attr('action'), formData, {disableAutoLoginRedirect: true})
-						.then (httpResponse) ->
-							{ data, status, headers, config } = httpResponse
-							scope[attrs.name].inflight = false
-							response.success = true
-							response.error = false
-
-							onSuccessHandler = scope[attrs.onSuccess]
-							if onSuccessHandler
-								onSuccessHandler(httpResponse)
-								return
-							
-							if data.redir?
-								ga('send', 'event', formName, 'success')
-								window.location = data.redir
-							else if data.message?
-								response.message = data.message
-
-								if data.message.type == "error"
-									response.success = false
-									response.error = true
-									ga('send', 'event', formName, 'failure', data.message)
-								else
-									ga('send', 'event', formName, 'success')
-
-						.catch (httpResponse) ->
-							{ data, status, headers, config } = httpResponse
-							scope[attrs.name].inflight = false
-							response.success = false
-							response.error = true
-
-							onErrorHandler = scope[attrs.onError]
-							if onErrorHandler
-								onErrorHandler(httpResponse)
-								return
-
-							if status == 400 # Bad Request
-								response.message =
-									text: "Invalid Request. Please correct the data and try again."
-									type: 'error'
-							else if status == 403 # Forbidden
-								response.message =
-									text: "Session error. Please check you have cookies enabled. If the problem persists, try clearing your cache and cookies."
-									type: "error"
-							else
-								response.message =
-									text: data.message?.text or data.message or "Something went wrong talking to the server :(. Please try again."
-									type: 'error'
-							ga('send', 'event', formName, 'failure', data.message)
-
-				submit = () ->
-					validateCaptchaIfEnabled (response) ->
-						submitRequest response
-
-				element.on "submit", (e) ->
-					e.preventDefault()
-					submit()
-
-				if attrs.autoSubmit
-					submit()
-		}
-
-	App.directive "formMessages", () ->
-		return {
-			restrict: "E"
-			template: """
-				<div class="alert" ng-class="{
-					'alert-danger': form.response.message.type == 'error',
-					'alert-success': form.response.message.type != 'error'
-				}" ng-show="!!form.response.message">
-					{{form.response.message.text}}
-				</div>
-				<div ng-transclude></div>
-			"""
-			transclude: true
-			scope: {
-				form: "=for"
-			}
-
-		}
-
-
-	App.directive 'complexPassword', ->
-		require: ['^asyncForm', 'ngModel']
-
-		link: (scope, element, attrs, ctrl) ->
-
-			PassField.Config.blackList = []
-			defaultPasswordOpts =
-				pattern: ""
-				length:
-					min: 6
-					max: 128
-				allowEmpty: false
-				allowAnyChars: false
-				isMasked: true
-				showToggle: false
-				showGenerate: false
-				showTip:false
-				showWarn:false
-				checkMode : PassField.CheckModes.STRICT
-				chars:
-					digits: "1234567890"
-					letters: "abcdefghijklmnopqrstuvwxyz"
-					letters_up: "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
-					symbols: "@#$%^&*()-_=+[]{};:<>/?!£€.,"
-
-			opts = _.defaults(window.passwordStrengthOptions || {}, defaultPasswordOpts)
-			if opts.length.min == 1
-				opts.acceptRate = 0 #this allows basically anything to be a valid password
-			passField = new PassField.Field("passwordField", opts);
-
-			[asyncFormCtrl, ngModelCtrl] = ctrl
-
-			ngModelCtrl.$parsers.unshift (modelValue) ->
-				isValid = passField.validatePass()
-				email = asyncFormCtrl.getEmail() || window.usersEmail
-				if !isValid
-					scope.complexPasswordErrorMessage = passField.getPassValidationMessage()
-				else if (email? and email != "")
-					startOfEmail = email?.split("@")?[0]
-					if modelValue.indexOf(email) != -1 or modelValue.indexOf(startOfEmail) != -1
-						isValid = false
-						scope.complexPasswordErrorMessage = "Password can not contain email address"
-				if opts.length.max? and modelValue.length == opts.length.max
-					isValid = false
-					scope.complexPasswordErrorMessage = "Maximum password length #{opts.length.max} reached"
-				if opts.length.min? and modelValue.length < opts.length.min
-					isValid = false
-					scope.complexPasswordErrorMessage = "Password too short, minimum #{opts.length.min}"
-				ngModelCtrl.$setValidity('complexPassword', isValid)
-				return modelValue

+ 0 - 43
services/web/public/coffee/directives/bookmarkableTabset.coffee

@@ -1,43 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.directive "bookmarkableTabset", ($location, _) ->
-		restrict: "A"
-		require: "tabset"
-		link: (scope, el, attrs, tabset) ->
-
-			_makeActive = (hash) ->
-				if hash? and hash != ""
-					matchingTab = _.find tabset.tabs, (tab) ->
-						tab.bookmarkableTabId == hash
-					if matchingTab?
-						matchingTab.select()
-						el.children()[0].scrollIntoView({ behavior: "smooth" })
-
-			scope.$applyAsync () ->
-				# for page load
-				hash = $location.hash()
-				_makeActive(hash)
-
-				# for links within page to a tab
-				# this needs to be within applyAsync because there could be a link
-				# within a tab to another tab
-				linksToTabs = document.querySelectorAll(".link-to-tab");
-				_clickLinkToTab = (event) ->
-					_makeActive(event.currentTarget.getAttribute("href").replace('#', ''))
-
-				if linksToTabs
-					for link in linksToTabs
-						link.addEventListener("click", _clickLinkToTab)
-
-	App.directive "bookmarkableTab", ($location) ->
-		restrict: "A"
-		require: "tab"
-		link: (scope, el, attrs, tab) ->
-			tabScope = el.isolateScope()
-			tabId = attrs.bookmarkableTab
-			if tabScope? and tabId? and tabId != ""
-				tabScope.bookmarkableTabId = tabId
-				tabScope.$watch "active", (isActive, wasActive) ->
-					if isActive and !wasActive and $location.hash() != tabId
-						$location.hash tabId

+ 0 - 539
services/web/public/coffee/directives/creditCards.coffee

@@ -1,539 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.factory 'ccUtils', () ->
-		defaultFormat = /(\d{1,4})/g;
-		defaultInputFormat =  /(?:^|\s)(\d{4})$/
-		
-		cards = [
-			# Credit cards
-			{
-				type: 'visa'
-				patterns: [4]
-				format: defaultFormat
-				length: [13, 16]
-				cvcLength: [3]
-				luhn: true
-			}
-			{
-				type: 'mastercard'
-				patterns: [
-					51, 52, 53, 54, 55,
-					22, 23, 24, 25, 26, 27
-				]
-				format: defaultFormat
-				length: [16]
-				cvcLength: [3]
-				luhn: true
-			}
-			{
-				type: 'amex'
-				patterns: [34, 37]
-				format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/
-				length: [15]
-				cvcLength: [3..4]
-				luhn: true
-			}
-			{
-				type: 'dinersclub'
-				patterns: [30, 36, 38, 39]
-				format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/
-				length: [14]
-				cvcLength: [3]
-				luhn: true
-			}
-			{
-				type: 'discover'
-				patterns: [60, 64, 65, 622]
-				format: defaultFormat
-				length: [16]
-				cvcLength: [3]
-				luhn: true
-			}
-			{
-				type: 'unionpay'
-				patterns: [62, 88]
-				format: defaultFormat
-				length: [16..19]
-				cvcLength: [3]
-				luhn: false
-			}
-			{
-				type: 'jcb'
-				patterns: [35]
-				format: defaultFormat
-				length: [16]
-				cvcLength: [3]
-				luhn: true
-			}
-		]
-
-		cardFromNumber = (num) ->
-			num = (num + '').replace(/\D/g, "")
-			for card in cards
-				for pattern in card.patterns
-					p = pattern + ""
-					return card if num.substr(0, p.length) == p
-
-		cardFromType = (type) ->
-			return card for card in cards when card.type is type
-
-		cardType = (num) ->
-			return null unless num
-			cardFromNumber(num)?.type or null
-
-		formatCardNumber = (num) ->
-			num = num.replace(/\D/g, '')
-			card = cardFromNumber(num)
-			return num unless card
-
-			upperLength = card.length[card.length.length - 1]
-			num = num[0...upperLength]
-
-			if card.format.global
-				num.match(card.format)?.join(' ')
-			else
-				groups = card.format.exec(num)
-				return unless groups?
-				groups.shift()
-				groups = $.grep(groups, (n) -> n) # Filter empty groups
-				groups.join(' ')
-
-		formatExpiry = (expiry) ->
-			parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/)
-			return '' unless parts
-
-			mon = parts[1] || ''
-			sep = parts[2] || ''
-			year = parts[3] || ''
-
-			if year.length > 0
-				sep = ' / '
-
-			else if sep is ' /'
-				mon = mon.substring(0, 1)
-				sep = ''
-
-			else if mon.length == 2 or sep.length > 0
-				sep = ' / '
-
-			else if mon.length == 1 and mon not in ['0', '1']
-				mon = "0#{mon}"
-				sep = ' / '
-
-			return mon + sep + year
-
-		parseExpiry = (value = "") ->
-			[month, year] = value.split(/[\s\/]+/, 2)
-
-			# Allow for year shortcut
-			if year?.length is 2 and /^\d+$/.test(year)
-				prefix = (new Date).getFullYear()
-				prefix = prefix.toString()[0..1]
-				year   = prefix + year
-
-			month = parseInt(month, 10)
-			year  = parseInt(year, 10)
-
-			return unless !isNaN(month) and !isNaN(year)
-
-			month: month, year: year
-
-		return {
-			fromNumber: cardFromNumber
-			fromType: cardFromType
-			cardType: cardType 
-			formatExpiry: formatExpiry
-			formatCardNumber: formatCardNumber  
-			defaultFormat: defaultFormat
-			defaultInputFormat: defaultInputFormat
-			parseExpiry: parseExpiry
-		}
-
-	App.factory 'ccFormat', (ccUtils, $filter) ->
-		hasTextSelected = ($target) ->
-			# If some text is selected
-			return true if $target.prop('selectionStart')? and
-				$target.prop('selectionStart') isnt $target.prop('selectionEnd')
-
-			# If some text is selected in IE
-			if document?.selection?.createRange?
-				return true if document.selection.createRange().text
-
-			false
-
-		safeVal = (value, $target) ->
-			try
-				cursor = $target.prop('selectionStart')
-			catch error
-				cursor = null
-			
-			last = $target.val()
-			$target.val(value)
-
-			if cursor != null && $target.is(":focus")
-				cursor = value.length if cursor is last.length
-
-				# This hack looks for scenarios where we are changing an input's value such
-				# that "X| " is replaced with " |X" (where "|" is the cursor). In those
-				# scenarios, we want " X|".
-				#
-				# For example:
-				# 1. Input field has value "4444| "
-				# 2. User types "1"
-				# 3. Input field has value "44441| "
-				# 4. Reformatter changes it to "4444 |1"
-				# 5. By incrementing the cursor, we make it "4444 1|"
-				#
-				# This is awful, and ideally doesn't go here, but given the current design
-				# of the system there does not appear to be a better solution.
-				#
-				# Note that we can't just detect when the cursor-1 is " ", because that
-				# would incorrectly increment the cursor when backspacing, e.g. pressing
-				# backspace in this scenario: "4444 1|234 5".
-				if last != value
-					prevPair = last[cursor-1..cursor]
-					currPair = value[cursor-1..cursor]
-					digit = value[cursor]
-					cursor = cursor + 1 if /\d/.test(digit) and
-						prevPair == "#{digit} " and currPair == " #{digit}"
-
-				$target.prop('selectionStart', cursor)
-				$target.prop('selectionEnd', cursor)
-
-		# Replace Full-Width Chars
-		replaceFullWidthChars = (str = '') ->
-			fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19'
-			halfWidth = '0123456789'
-
-			value = ''
-			chars = str.split('')
-
-			# Avoid using reserved word `char`
-			for chr in chars
-				idx = fullWidth.indexOf(chr)
-				chr = halfWidth[idx] if idx > -1
-				value += chr
-
-			value
-
-		# Format Numeric
-		reFormatNumeric = (e) ->
-			$target = $(e.currentTarget)
-			setTimeout ->
-				value   = $target.val()
-				value   = replaceFullWidthChars(value)
-				value   = value.replace(/\D/g, '')
-				safeVal(value, $target)
-
-		# Format Card Number
-		reFormatCardNumber = (e) ->
-			$target = $(e.currentTarget)
-			setTimeout ->
-				value   = $target.val()
-				value   = replaceFullWidthChars(value)
-				value   = ccUtils.formatCardNumber(value)
-				safeVal(value, $target)
-
-		formatCardNumber = (e) ->
-			# Only format if input is a number
-			digit = String.fromCharCode(e.which)
-			return unless /^\d+$/.test(digit)
-
-			$target = $(e.currentTarget)
-			value   = $target.val()
-			card    = ccUtils.fromNumber(value + digit)
-			length  = (value.replace(/\D/g, '') + digit).length
-
-			upperLength = 16
-			upperLength = card.length[card.length.length - 1] if card
-			return if length >= upperLength
-
-			# Return if focus isn't at the end of the text
-			return if $target.prop('selectionStart')? and
-				$target.prop('selectionStart') isnt value.length
-
-			if card && card.type is 'amex'
-				# AMEX cards are formatted differently
-				re = /^(\d{4}|\d{4}\s\d{6})$/
-			else
-				re = /(?:^|\s)(\d{4})$/
-
-			# If '4242' + 4
-			if re.test(value)
-				e.preventDefault()
-				setTimeout -> $target.val(value + ' ' + digit)
-
-			# If '424' + 2
-			else if re.test(value + digit)
-				e.preventDefault()
-				setTimeout -> $target.val(value + digit + ' ')
-
-		formatBackCardNumber = (e) ->
-			$target = $(e.currentTarget)
-			value   = $target.val()
-
-			# Return unless backspacing
-			return unless e.which is 8
-
-			# Return if focus isn't at the end of the text
-			return if $target.prop('selectionStart')? and
-				$target.prop('selectionStart') isnt value.length
-
-			# Remove the digit + trailing space
-			if /\d\s$/.test(value)
-				e.preventDefault()
-				setTimeout -> $target.val(value.replace(/\d\s$/, ''))
-			# Remove digit if ends in space + digit
-			else if /\s\d?$/.test(value)
-				e.preventDefault()
-				setTimeout -> $target.val(value.replace(/\d$/, ''))
-
-		getFormattedCardNumber = (num) ->
-			num = num.replace(/\D/g, '')
-			card = ccUtils.fromNumber(num)
-			return num unless card
-
-			upperLength = card.length[card.length.length - 1]
-			num = num[0...upperLength]
-
-			if card.format.global
-				num.match(card.format)?.join(' ')
-			else
-				groups = card.format.exec(num)
-				return unless groups?
-				groups.shift()
-				groups = $.grep(groups, (n) -> n) # Filter empty groups
-				groups.join(' ')
-
-		parseCardNumber = (value) ->
-    		if value? then value.replace(/\s/g, '') else value
-
-		# Format Expiry
-		reFormatExpiry = (e) ->
-			$target = $(e.currentTarget)
-			setTimeout ->
-				value   = $target.val()
-				value   = replaceFullWidthChars(value)
-				value   = ccUtils.formatExpiry(value)
-				safeVal(value, $target)
-
-
-		formatExpiry = (e) ->
-			# Only format if input is a number
-			digit = String.fromCharCode(e.which)
-			return unless /^\d+$/.test(digit)
-
-			$target = $(e.currentTarget)
-			val     = $target.val() + digit
-
-			if /^\d$/.test(val) and val not in ['0', '1']
-				e.preventDefault()
-				setTimeout -> $target.val("0#{val} / ")
-
-			else if /^\d\d$/.test(val)
-				e.preventDefault()
-				setTimeout ->
-					# Split for months where we have the second digit > 2 (past 12) and turn
-					# that into (m1)(m2) => 0(m1) / (m2)
-					m1 = parseInt(val[0], 10)
-					m2 = parseInt(val[1], 10)
-					if m2 > 2 and m1 != 0
-						$target.val("0#{m1} / #{m2}")
-					else
-						$target.val("#{val} / ")
-
-		formatForwardExpiry = (e) ->
-			digit = String.fromCharCode(e.which)
-			return unless /^\d+$/.test(digit)
-
-			$target = $(e.currentTarget)
-			val     = $target.val()
-
-			if /^\d\d$/.test(val)
-				$target.val("#{val} / ")
-
-		formatForwardSlash = (e) ->
-			which = String.fromCharCode(e.which)
-			return unless which is '/' or which is ' '
-
-			$target = $(e.currentTarget)
-			val     = $target.val()
-
-			if /^\d$/.test(val) and val isnt '0'
-				$target.val("0#{val} / ")
-
-		formatBackExpiry = (e) ->
-			$target = $(e.currentTarget)
-			value   = $target.val()
-
-			# Return unless backspacing
-			return unless e.which is 8
-
-			# Return if focus isn't at the end of the text
-			return if $target.prop('selectionStart')? and
-				$target.prop('selectionStart') isnt value.length
-
-			# Remove the trailing space + last digit
-			if /\d\s\/\s$/.test(value)
-				e.preventDefault()
-				setTimeout -> $target.val(value.replace(/\d\s\/\s$/, ''))
-
-		parseExpiry = (value) ->
-			if value?
-				dateAsObj = ccUtils.parseExpiry(value)
-				
-				return unless dateAsObj?
-
-				expiry = new Date dateAsObj.year, dateAsObj.month - 1
-
-				return $filter('date')(expiry, 'MM/yyyy')
-
-		# Format CVC
-		reFormatCVC = (e) ->
-			$target = $(e.currentTarget)
-			setTimeout ->
-				value   = $target.val()
-				value   = replaceFullWidthChars(value)
-				value   = value.replace(/\D/g, '')[0...4]
-				safeVal(value, $target)
-
-		# Restrictions
-		restrictNumeric = (e) ->
-			# Key event is for a browser shortcut
-			return true if e.metaKey or e.ctrlKey
-
-			# If keycode is a space
-			return false if e.which is 32
-
-			# If keycode is a special char (WebKit)
-			return true if e.which is 0
-
-			# If char is a special char (Firefox)
-			return true if e.which < 33
-
-			input = String.fromCharCode(e.which)
-
-			# Char is a number or a space
-			!!/[\d\s]/.test(input)
-
-		restrictCardNumber = (e) ->
-			$target = $(e.currentTarget)
-			digit   = String.fromCharCode(e.which)
-			return unless /^\d+$/.test(digit)
-
-			return if hasTextSelected($target)
-
-			# Restrict number of digits
-			value = ($target.val() + digit).replace(/\D/g, '')
-			card  = ccUtils.fromNumber(value)
-
-			if card
-				value.length <= card.length[card.length.length - 1]
-			else
-				# All other cards are 16 digits long
-				value.length <= 16
-
-		restrictExpiry = (e) ->
-			$target = $(e.currentTarget)
-			digit   = String.fromCharCode(e.which)
-			return unless /^\d+$/.test(digit)
-
-			return if hasTextSelected($target)
-
-			value = $target.val() + digit
-			value = value.replace(/\D/g, '')
-
-			return false if value.length > 6
-
-		restrictCVC = (e) ->
-			$target = $(e.currentTarget)
-			digit   = String.fromCharCode(e.which)
-			return unless /^\d+$/.test(digit)
-
-			return if hasTextSelected($target)
-
-			val     = $target.val() + digit
-			val.length <= 4
-
-		setCardType = (e) ->
-			$target  = $(e.currentTarget)
-			val      = $target.val()
-			cardType = ccUtils.cardType(val) or 'unknown'
-
-			unless $target.hasClass(cardType)
-				allTypes = (card.type for card in cards)
-
-				$target.removeClass('unknown')
-				$target.removeClass(allTypes.join(' '))
-
-				$target.addClass(cardType)
-				$target.toggleClass('identified', cardType isnt 'unknown')
-				$target.trigger('payment.cardType', cardType)
-
-		return {
-			hasTextSelected
-			replaceFullWidthChars
-			reFormatNumeric
-			reFormatCardNumber
-			formatCardNumber
-			formatBackCardNumber
-			getFormattedCardNumber
-			parseCardNumber
-			reFormatExpiry
-			formatExpiry
-			formatForwardExpiry
-			formatForwardSlash
-			formatBackExpiry
-			parseExpiry
-			reFormatCVC
-			restrictNumeric
-			restrictCardNumber
-			restrictExpiry
-			restrictCVC
-			setCardType
-		}
-		
-	App.directive "ccFormatExpiry", (ccFormat) ->
-		restrict: "A"
-		require: "ngModel"
-		link: (scope, el, attrs, ngModel) ->
-			el.on "keypress", 	ccFormat.restrictNumeric
-			el.on "keypress", 	ccFormat.restrictExpiry
-			el.on "keypress", 	ccFormat.formatExpiry
-			el.on "keypress", 	ccFormat.formatForwardSlash
-			el.on "keypress", 	ccFormat.formatForwardExpiry
-			el.on "keydown", 	ccFormat.formatBackExpiry
-			el.on "change", 	ccFormat.reFormatExpiry
-			el.on "input", 		ccFormat.reFormatExpiry
-			el.on "paste", 		ccFormat.reFormatExpiry
-
-			ngModel.$parsers.push ccFormat.parseExpiry
-			ngModel.$formatters.push ccFormat.parseExpiry
-
-	App.directive "ccFormatCardNumber", (ccFormat) ->
-		restrict: "A"
-		require: "ngModel"
-		link: (scope, el, attrs, ngModel) ->
-			el.on "keypress", 	ccFormat.restrictNumeric
-			el.on "keypress", 	ccFormat.restrictCardNumber
-			el.on "keypress", 	ccFormat.formatCardNumber
-			el.on "keydown", 	ccFormat.formatBackCardNumber
-			el.on "paste", 		ccFormat.reFormatCardNumber
-
-			ngModel.$parsers.push ccFormat.parseCardNumber
-			ngModel.$formatters.push ccFormat.getFormattedCardNumber
-
-	App.directive "ccFormatSecCode", (ccFormat) ->
-		restrict: "A"
-		require: "ngModel"
-		link: (scope, el, attrs, ngModel) ->
-			el.on "keypress", 	ccFormat.restrictNumeric
-			el.on "keypress", 	ccFormat.restrictCVC
-			el.on "paste", 		ccFormat.reFormatCVC
-			el.on "change", 	ccFormat.reFormatCVC
-			el.on "input", 		ccFormat.reFormatCVC
-
-
-
-	

+ 0 - 15
services/web/public/coffee/directives/equals.coffee

@@ -1,15 +0,0 @@
-define [
-	"base"
-], (App) ->
-
-	App.directive "equals", [->
-		return {
-			require: "ngModel"
-			link: (scope, elem, attrs, ctrl) ->
-				firstField = "#" + attrs.equals
-				elem.add(firstField).on "keyup", ->
-					scope.$apply ->
-						equal = elem.val() == $(firstField).val()
-						ctrl.$setValidity "areEqual", equal
-		}
-	]

+ 0 - 75
services/web/public/coffee/directives/eventTracking.coffee

@@ -1,75 +0,0 @@
-# For sending event data to metabase and google analytics
-# ---
-# by default,
-#   event not sent to MB.
-#     for MB, add event-tracking-mb='true'
-#     by default, event sent to MB via sendMB
-#   event not sent to GA.
-#     for GA, add event-tracking-ga attribute, where the value is the GA category
-# Either GA or MB can use the attribute event-tracking-send-once='true' to
-# send event just once
-#   MB will use the key and GA will use the action to determine if the event
-#   has been sent
-# event-tracking-trigger attribute is required to send event
-
-isInViewport = (element) ->
-	elTop = element.offset().top
-	elBtm = elTop + element.outerHeight()
-
-	viewportTop = $(window).scrollTop()
-	viewportBtm = viewportTop + $(window).height()
-
-	elBtm > viewportTop && elTop < viewportBtm
-
-define [
-	'base'
-], (App) ->
-	App.directive 'eventTracking', ['event_tracking', (event_tracking) ->
-		return {
-			scope: {
-				eventTracking: '@',
-				eventSegmentation: '=?'
-			}
-			link: (scope, element, attrs) ->
-				sendGA = attrs.eventTrackingGa || false
-				sendMB = attrs.eventTrackingMb || false
-				sendMBFunction = if attrs.eventTrackingSendOnce then 'sendMBOnce' else 'sendMB'
-				sendGAFunction = if attrs.eventTrackingSendOnce then 'sendGAOnce' else 'send'
-				segmentation = scope.eventSegmentation || {}
-				segmentation.page = window.location.pathname
-
-				sendEvent = (scrollEvent) ->
-					###
-						@param {boolean}		scrollEvent		Use to unbind scroll event
-					###
-					if sendMB
-						event_tracking[sendMBFunction] scope.eventTracking, segmentation
-					if sendGA
-						event_tracking[sendGAFunction] attrs.eventTrackingGa, attrs.eventTrackingAction || scope.eventTracking, attrs.eventTrackingLabel || ''
-					if scrollEvent
-						$(window).unbind('resize scroll')
-
-				if attrs.eventTrackingTrigger == 'load'
-					sendEvent()
-				else if attrs.eventTrackingTrigger == 'click'
-					element.on 'click', (e) ->
-						sendEvent()
-				else if attrs.eventTrackingTrigger == 'hover'
-					timer = null
-					timeoutAmt = 500
-					if attrs.eventHoverAmt
-						timeoutAmt = parseInt(attrs.eventHoverAmt, 10)
-					element.on 'mouseenter', () ->
-						timer = setTimeout((-> sendEvent()), timeoutAmt)
-						return
-					.on 'mouseleave', () ->
-						clearTimeout(timer)
-				else if attrs.eventTrackingTrigger == 'scroll'
-					if !event_tracking.eventInCache(scope.eventTracking)
-						$(window).on 'resize scroll', () ->
-							_.throttle(
-								if isInViewport(element) && !event_tracking.eventInCache(scope.eventTracking)
-									sendEvent(true)
-							, 500)
-		}
-	]

+ 0 - 17
services/web/public/coffee/directives/expandableTextArea.coffee

@@ -1,17 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.directive "expandableTextArea", () ->
-		restrict: "A"
-		link: (scope, el) ->
-			resetHeight = () ->
-				curHeight = el.outerHeight()
-				fitHeight = el.prop("scrollHeight")
-				
-				if fitHeight > curHeight and el.val() != ""
-					scope.$emit "expandable-text-area:resize"
-					el.css("height", fitHeight) 
-
-			scope.$watch (() -> el.val()), resetHeight
-
-	

+ 0 - 68
services/web/public/coffee/directives/fineUpload.coffee

@@ -1,68 +0,0 @@
-define [
-	"base"
-	"fineuploader"
-], (App, qq) ->
-	App.directive 'fineUpload', ($timeout) ->
-		return {
-			scope: {
-				multiple: "="
-				endpoint: "@"
-				templateId: "@"
-				allowedExtensions: "="
-				onCompleteCallback: "="
-				onUploadCallback: "="
-				onValidateBatch: "="
-				onErrorCallback: "="
-				onSubmitCallback: "="
-				onCancelCallback: "="
-				autoUpload: "="
-				params: "="
-				control: "="
-			}
-			link: (scope, element, attrs) ->
-				multiple = scope.multiple or false
-				endpoint = scope.endpoint
-				templateId = scope.templateId
-				if scope.allowedExtensions?
-					validation =
-						allowedExtensions: scope.allowedExtensions
-				else
-					validation = {}
-				maxConnections = scope.maxConnections or 1
-				onComplete = scope.onCompleteCallback or () ->
-				onUpload   = scope.onUploadCallback or () ->
-				onError   = scope.onErrorCallback or () ->
-				onValidateBatch = scope.onValidateBatch or () ->
-				onSubmit = scope.onSubmitCallback or () ->
-				onCancel = scope.onCancelCallback or () ->
-				if !scope.autoUpload?
-					autoUpload = true
-				else
-					autoUpload = scope.autoUpload
-				params     = scope.params or {}
-				params._csrf = window.csrfToken
-
-				q = new qq.FineUploader
-					element: element[0]
-					multiple: multiple
-					autoUpload: autoUpload
-					disabledCancelForFormUploads: true
-					validation: validation
-					maxConnections: maxConnections
-					request:
-						endpoint: endpoint
-						forceMultipart: true
-						params: params
-						paramsInBody: false
-					callbacks:
-						onComplete: onComplete
-						onUpload:   onUpload
-						onValidateBatch: onValidateBatch
-						onError: onError
-						onSubmit: onSubmit
-						onCancel: onCancel
-					template: templateId
-				window.q = q
-				scope.control?.q = q
-				return q
-		}

+ 0 - 78
services/web/public/coffee/directives/focus.coffee

@@ -1,78 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.directive "focusWhen", ($timeout) ->
-		return {
-			restrict: "A"
-			link: (scope, element, attr) ->
-				scope.$watch attr.focusWhen, (value) ->
-					if value
-						$timeout ->
-							element.focus()
-		}
-
-	App.directive 'focusOn', ($timeout) ->
-		return {
-			restrict: 'A'
-			link: (scope, element, attrs) ->
-				scope.$on attrs.focusOn, () ->
-					element.focus()
-		}
-
-	App.directive "selectWhen", ($timeout) ->
-		return {
-			restrict: "A"
-			link: (scope, element, attr) ->
-				scope.$watch attr.selectWhen, (value) ->
-					if value
-						$timeout ->
-							element.select()
-		}
-
-	App.directive 'selectOn', ($timeout) ->
-		return {
-			restrict: 'A'
-			link: (scope, element, attrs) ->
-				scope.$on attrs.selectOn, () ->
-					element.select()
-		}
-
-	App.directive "selectNameWhen", ($timeout) ->
-		return {
-			restrict: 'A'
-			link: (scope, element, attrs) ->
-				scope.$watch attrs.selectNameWhen, (value) ->
-					if value
-						$timeout () ->
-							selectName(element)
-		}
-
-	App.directive "selectNameOn", () ->
-		return {
-			restrict: 'A'
-			link: (scope, element, attrs) ->
-				scope.$on attrs.selectNameOn, () ->
-					selectName(element)
-		}
-
-
-	App.directive "focus", ($timeout) ->
-		scope:
-			trigger: "@focus"
-
-		link: (scope, element) ->
-			scope.$watch "trigger", (value) ->
-				if value is "true"
-					$timeout ->
-						element[0].focus()
-
-	selectName = (element) ->
-		# Select up to last '.'. I.e. everything
-		# except the file extension
-		element.focus()
-		name = element.val()
-		if element[0].setSelectionRange?
-			selectionEnd = name.lastIndexOf(".")
-			if selectionEnd == -1
-				selectionEnd = name.length
-			element[0].setSelectionRange(0, selectionEnd)

+ 0 - 21
services/web/public/coffee/directives/mathjax.coffee

@@ -1,21 +0,0 @@
-define [
-	"base"
-], (App) ->	
-	App.directive "mathjax", () ->
-		return {
-			link: (scope, element, attrs) ->
-				if attrs.delimiter != 'no-single-dollar'
-					inlineMathConfig = MathJax?.Hub?.config?.tex2jax.inlineMath
-					alreadyConfigured = _.find inlineMathConfig, (c) ->
-						c[0] == '$' and c[1] == '$'
-
-					if !alreadyConfigured?
-						MathJax?.Hub?.Config(
-							tex2jax:
-								inlineMath: inlineMathConfig.concat([['$', '$']])
-						)
-
-				setTimeout () ->
-					MathJax?.Hub?.Queue(["Typeset", MathJax?.Hub, element.get(0)])
-				, 0
-		}

+ 0 - 11
services/web/public/coffee/directives/maxHeight.coffee

@@ -1,11 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.directive "maxHeight", () ->
-		return {
-			restrict: "A"
-			link: (scope, element, attrs) ->
-				scope.$watch attrs.maxHeight, (value) ->
-					if value?
-						element.css("max-height": value)
-		}

+ 0 - 10
services/web/public/coffee/directives/onEnter.coffee

@@ -1,10 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.directive 'onEnter', () ->
-		return (scope, element, attrs) ->
-			element.bind "keydown keypress", (event) ->
-				if event.which == 13
-					scope.$apply () ->
-						scope.$eval(attrs.onEnter, event: event)
-					event.preventDefault()

+ 0 - 12
services/web/public/coffee/directives/rightClick.coffee

@@ -1,12 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.directive "rightClick", () ->
-		return {
-			restrict: "A",
-			link: (scope, element, attrs) ->
-				element.bind "contextmenu", (e) ->
-					e.preventDefault()
-					e.stopPropagation()
-					scope.$eval(attrs.rightClick)
-		}

+ 0 - 36
services/web/public/coffee/directives/scroll.coffee

@@ -1,36 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.directive "updateScrollBottomOn", ($timeout) ->
-		return {
-			restrict: "A"
-			link: (scope, element, attrs, ctrls) ->
-				# We keep the offset from the bottom fixed whenever the event fires
-				#
-				# ^   | ^
-				# |   | | scrollTop
-				# |   | v
-				# |   |-----------
-				# |   | ^
-				# |   | |
-				# |   | | clientHeight (viewable area)
-				# |   | |
-				# |   | |
-				# |   | v
-				# |   |-----------
-				# |   | ^
-				# |   | | scrollBottom
-				# v   | v
-				#  \
-				#   scrollHeight
-				
-				scrollBottom = 0
-				element.on "scroll", (e) ->
-					scrollBottom = element[0].scrollHeight - element[0].scrollTop - element[0].clientHeight
-					
-				scope.$on attrs.updateScrollBottomOn, () ->
-					$timeout () ->
-						element.scrollTop(element[0].scrollHeight - element[0].clientHeight - scrollBottom)
-					, 0
-		}
-

+ 0 - 81
services/web/public/coffee/directives/selectAll.coffee

@@ -1,81 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.directive "selectAllList", () ->
-		return {
-			controller: ["$scope", ($scope) ->
-				# Selecting or deselecting all should apply to all projects
-				@selectAll = () ->
-					$scope.$broadcast "select-all:select"
-
-				@deselectAll = () ->
-					$scope.$broadcast "select-all:deselect"
-
-				@clearSelectAllState = () ->
-					$scope.$broadcast "select-all:clear"
-				return
-			]
-			link: (scope, element, attrs) ->
-
-
-		}
-
-	App.directive "selectAll", () ->
-		return {
-			require: "^selectAllList"
-			link: (scope, element, attrs, selectAllListController) ->
-				scope.$on "select-all:clear", () ->
-					element.prop("checked", false)
-
-				element.change () ->
-					if element.is(":checked")
-						selectAllListController.selectAll()
-					else
-						selectAllListController.deselectAll()
-					return true
-		}
-
-	App.directive "selectIndividual", () ->
-		return {
-			require: "^selectAllList"
-			scope: {
-				ngModel: "="
-			}
-			link: (scope, element, attrs, selectAllListController) ->
-				ignoreChanges = false
-
-				scope.$watch "ngModel", (value) ->
-					if value? and !ignoreChanges
-						selectAllListController.clearSelectAllState()
-
-				scope.$on "select-all:select", () ->
-					return if element.prop('disabled')
-					ignoreChanges = true
-					scope.$apply () ->
-						scope.ngModel = true
-					ignoreChanges = false
-
-				scope.$on "select-all:deselect", () ->
-					return if element.prop('disabled')
-					ignoreChanges = true
-					scope.$apply () ->
-						scope.ngModel = false
-					ignoreChanges = false
-
-				scope.$on "select-all:row-clicked", () ->
-					return if element.prop('disabled')
-					ignoreChanges = true
-					scope.$apply () ->
-						scope.ngModel = !scope.ngModel
-						if !scope.ngModel
-							selectAllListController.clearSelectAllState()
-					ignoreChanges = false
-		}
-
-	App.directive "selectRow", () ->
-		return {
-			scope: true
-			link: (scope, element, attrs) ->
-				element.on "click", (e) ->
-					scope.$broadcast "select-all:row-clicked"
-		}

+ 0 - 18
services/web/public/coffee/directives/stopPropagation.coffee

@@ -1,18 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.directive "stopPropagation", ($http) ->
-		return {
-			restrict: "A",
-			link: (scope, element, attrs) ->
-				element.bind attrs.stopPropagation, (e) ->
-					e.stopPropagation()
-		}
-
-	App.directive "preventDefault", ($http) ->
-		return {
-			restrict: "A",
-			link: (scope, element, attrs) ->
-				element.bind attrs.preventDefault, (e) ->
-					e.preventDefault()
-		}

+ 0 - 15
services/web/public/coffee/directives/videoPlayState.coffee

@@ -1,15 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.directive "videoPlayState", ($parse) ->
-		return {
-			restrict: "A",
-			link: (scope, element, attrs) ->
-				videoDOMEl = element[0]
-				scope.$watch (() -> $parse(attrs.videoPlayState)(scope)), (shouldPlay) ->
-					if shouldPlay
-						videoDOMEl.currentTime = 0
-						videoDOMEl.play()
-					else 
-						videoDOMEl.pause()
-		}

+ 0 - 23
services/web/public/coffee/filters/formatDate.coffee

@@ -1,23 +0,0 @@
-define [
-	"base"
-	"moment"
-], (App, moment) ->
-	moment.locale "en", calendar:
-		lastDay : '[Yesterday]'
-		sameDay : '[Today]'
-		nextDay : '[Tomorrow]'
-		lastWeek : "ddd, Do MMM YY"
-		nextWeek : "ddd, Do MMM YY"
-		sameElse : 'ddd, Do MMM YY'
-
-	App.filter "formatDate", () ->
-		(date, format = "Do MMM YYYY, h:mm a") ->
-			moment(date).format(format)
-
-	App.filter "relativeDate", () ->
-		(date) ->
-			moment(date).calendar()
-
-	App.filter "fromNowDate", () ->
-		(date) ->
-			moment(date).fromNow()

+ 0 - 25
services/web/public/coffee/filters/wrapLongWords.coffee

@@ -1,25 +0,0 @@
-define [
-	"base"
-], (App) ->
-	DEF_MIN_LENGTH = 20
-
-	_decodeHTMLEntities = (str) ->
-		str.replace /&#(\d+);/g, (match, dec) ->
-    		String.fromCharCode dec;
-
-	_getWrappedWordsString = (baseStr, wrapperElName, minLength) ->
-		minLength = minLength || DEF_MIN_LENGTH
-		words = baseStr.split ' '
-
-		wordsWrapped = for word in words
-			if _decodeHTMLEntities(word).length >= minLength
-				"<#{wrapperElName} class=\"break-word\">#{word}</#{wrapperElName}>"
-			else
-				word
-
-		outputStr = wordsWrapped.join ' '
-
-
-	App.filter "wrapLongWords", () ->
-		(input, minLength) ->
-			_getWrappedWordsString input, "span", minLength

+ 0 - 245
services/web/public/coffee/ide.coffee

@@ -1,245 +0,0 @@
-define [
-	"base"
-	"ide/file-tree/FileTreeManager"
-	"ide/connection/ConnectionManager"
-	"ide/editor/EditorManager"
-	"ide/online-users/OnlineUsersManager"
-	"ide/history/HistoryManager"
-	"ide/history/HistoryV2Manager"
-	"ide/permissions/PermissionsManager"
-	"ide/pdf/PdfManager"
-	"ide/binary-files/BinaryFilesManager"
-	"ide/references/ReferencesManager"
-	"ide/metadata/MetadataManager"
-	"ide/review-panel/ReviewPanelManager"
-	"ide/SafariScrollPatcher"
-	"ide/settings/index"
-	"ide/share/index"
-	"ide/chat/index"
-	"ide/clone/index"
-	"ide/hotkeys/index"
-	"ide/test-controls/index"
-	"ide/wordcount/index"
-	"ide/directives/layout"
-	"ide/directives/validFile"
-	"ide/services/ide"
-	"__IDE_CLIENTSIDE_INCLUDES__"
-	"analytics/AbTestingManager"
-	"directives/focus"
-	"directives/fineUpload"
-	"directives/scroll"
-	"directives/onEnter"
-	"directives/stopPropagation"
-	"directives/rightClick"
-	"directives/expandableTextArea"
-	"directives/videoPlayState"
-	"services/queued-http"
-	"services/validateCaptcha"
-	"services/wait-for"
-	"filters/formatDate"
-	"main/event"
-	"main/account-upgrade"
-], (
-	App
-	FileTreeManager
-	ConnectionManager
-	EditorManager
-	OnlineUsersManager
-	HistoryManager
-	HistoryV2Manager
-	PermissionsManager
-	PdfManager
-	BinaryFilesManager
-	ReferencesManager
-	MetadataManager
-	ReviewPanelManager
-	SafariScrollPatcher
-) ->
-
-	App.controller "IdeController", ($scope, $timeout, ide, localStorage, sixpack, event_tracking, metadata, $q) ->
-		# 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
-				this.$originalApply(fn);
-
-		$scope.state = {
-			loading: true
-			load_progress: 40
-			error: null
-		}
-		$scope.ui = {
-			leftMenuShown: false
-			view: "editor"
-			chatOpen: false
-			pdfLayout: 'sideBySide'
-			pdfHidden: false
-			pdfWidth: 0
-			reviewPanelOpen: localStorage("ui.reviewPanelOpen.#{window.project_id}")
-			miniReviewPanelVisible: false
-			chatResizerSizeOpen: window.uiConfig.chatResizerSizeOpen
-			chatResizerSizeClosed: window.uiConfig.chatResizerSizeClosed
-			defaultFontFamily: window.uiConfig.defaultFontFamily
-			defaultLineHeight: window.uiConfig.defaultLineHeight
-		}
-		$scope.user = window.user
-
-		$scope.settings = window.userSettings
-		$scope.anonymous = window.anonymous
-		$scope.isTokenMember = window.isTokenMember
-
-		$scope.chat = {}
-
-		ide.toggleReviewPanel = $scope.toggleReviewPanel = () ->
-			if !$scope.project.features.trackChangesVisible
-				return
-			$scope.ui.reviewPanelOpen = !$scope.ui.reviewPanelOpen
-			event_tracking.sendMB "rp-toggle-panel", { value : $scope.ui.reviewPanelOpen }
-
-		$scope.$watch "ui.reviewPanelOpen", (value) ->
-			if value?
-				localStorage "ui.reviewPanelOpen.#{window.project_id}", value
-
-		$scope.$on "layout:pdf:resize", (_, layoutState) ->
-			$scope.ui.pdfHidden = layoutState.east.initClosed
-			$scope.ui.pdfWidth = layoutState.east.size
-
-		# Tracking code.
-		$scope.$watch "ui.view", (newView, oldView) ->
-			if newView? and newView != "editor" and newView != "pdf"
-				event_tracking.sendMBOnce "ide-open-view-#{ newView }-once"
-
-		$scope.$watch "ui.chatOpen", (isOpen) ->
-			event_tracking.sendMBOnce "ide-open-chat-once" if isOpen
-
-		$scope.$watch "ui.leftMenuShown", (isOpen) ->
-			event_tracking.sendMBOnce "ide-open-left-menu-once" if isOpen
-
-		$scope.trackHover = (feature) ->
-			event_tracking.sendMBOnce "ide-hover-#{feature}-once"
-		# End of tracking code.
-
-		window._ide = ide
-
-		ide.validFileRegex = '^[^\*\/]*$' # Don't allow * and /
-
-		ide.project_id = $scope.project_id = window.project_id
-		ide.$scope = $scope
-
-		ide.referencesSearchManager = new ReferencesManager(ide, $scope)
-		ide.connectionManager = new ConnectionManager(ide, $scope)
-		ide.fileTreeManager = new FileTreeManager(ide, $scope)
-		ide.editorManager = new EditorManager(ide, $scope, localStorage)
-		ide.onlineUsersManager = new OnlineUsersManager(ide, $scope)
-		if window.data.useV2History
-			ide.historyManager = new HistoryV2Manager(ide, $scope)
-		else
-			ide.historyManager = new HistoryManager(ide, $scope)
-		ide.pdfManager = new PdfManager(ide, $scope)
-		ide.permissionsManager = new PermissionsManager(ide, $scope)
-		ide.binaryFilesManager = new BinaryFilesManager(ide, $scope)
-		ide.metadataManager = new MetadataManager(ide, $scope, metadata)
-
-		inited = false
-		$scope.$on "project:joined", () ->
-			return if inited
-			inited = true
-			if $scope?.project?.deletedByExternalDataSource
-				ide.showGenericMessageModal("Project Renamed or Deleted", """
-					This project has either been renamed or deleted by an external data source such as Dropbox.
-					We don't want to delete your data on ShareLaTeX, so this project still contains your history and collaborators.
-					If the project has been renamed please look in your project list for a new project under the new name.
-				""")
-			$timeout(
-				() ->
-					if $scope.permissions.write
-						ide.metadataManager.loadProjectMetaFromServer()
-						_labelsInitialLoadDone = true
-				, 200
-			)
-
-		# Count the first 'doc:opened' as a sign that the ide is loaded
-		# and broadcast a message. This is a good event to listen for
-		# if you want to wait until the ide is fully loaded and initialized
-		_loaded = false
-		$scope.$on 'doc:opened', () ->
-			if _loaded
-				return
-			$scope.$broadcast('ide:loaded')
-			_loaded = true
-
-		$scope.$on 'cursor:editor:update', event_tracking.editingSessionHeartbeat
-
-		DARK_THEMES = [
-			"ambiance", "chaos", "clouds_midnight", "cobalt", "idle_fingers",
-			"merbivore", "merbivore_soft", "mono_industrial", "monokai",
-			"pastel_on_dark", "solarized_dark", "terminal", "tomorrow_night",
-			"tomorrow_night_blue", "tomorrow_night_bright", "tomorrow_night_eighties",
-			"twilight", "vibrant_ink"
-		]
-		$scope.darkTheme = false
-		$scope.$watch "settings.editorTheme", (theme) ->
-			if theme in DARK_THEMES
-				$scope.darkTheme = true
-			else
-				$scope.darkTheme = false
-
-		ide.localStorage = localStorage
-
-		ide.browserIsSafari = false
-
-		$scope.switchToFlatLayout = (view) ->
-			$scope.ui.pdfLayout = 'flat'
-			$scope.ui.view = view
-			ide.localStorage "pdf.layout", "flat"
-
-		$scope.switchToSideBySideLayout = (view) ->
-			$scope.ui.pdfLayout = 'sideBySide'
-			$scope.ui.view = view
-			localStorage "pdf.layout", "split"
-
-		if pdfLayout = localStorage("pdf.layout")
-			$scope.switchToSideBySideLayout() if pdfLayout == "split"
-			$scope.switchToFlatLayout() if pdfLayout == "flat"
-		else
-			$scope.switchToSideBySideLayout()
-
-		try
-			userAgent = navigator.userAgent
-			ide.browserIsSafari = (
-				userAgent &&
-				/.*Safari\/.*/.test(userAgent) &&
-				!/.*Chrome\/.*/.test(userAgent) &&
-				!/.*Chromium\/.*/.test(userAgent)
-			)
-		catch err
-			console.error err
-
-		if ide.browserIsSafari
-			ide.safariScrollPatcher = new SafariScrollPatcher($scope)
-
-		# Fix Chrome 61 and 62 text-shadow rendering
-		browserIsChrome61or62 = false
-		try
-			chromeVersion = parseFloat(navigator.userAgent.split(" Chrome/")[1]) || null;
-			browserIsChrome61or62 = (
-				chromeVersion?
-			)
-			if browserIsChrome61or62
-				document.styleSheets[0].insertRule(".ace_editor.ace_autocomplete .ace_completion-highlight { text-shadow: none !important; font-weight: bold; }", 1)
-		catch err
-			console.error err
-
-
-		# User can append ?ft=somefeature to url to activate a feature toggle
-		ide.featureToggle = location?.search?.match(/^\?ft=(\w+)$/)?[1]
-
-		ide.socket.on 'project:publicAccessLevel:changed', (data) =>
-			if data.newAccessLevel?
-				ide.$scope.project.publicAccesLevel = data.newAccessLevel
-				$scope.$digest()
-
-	angular.bootstrap(document.body, ["SharelatexApp"])

+ 0 - 78
services/web/public/coffee/ide/SafariScrollPatcher.coffee

@@ -1,78 +0,0 @@
-define [
-], () ->
-	class SafariScrollPatcher
-		constructor: ($scope) ->
-			@isOverAce = false # Flag to control if the pointer is over Ace.
-			@pdfDiv = null
-			@aceDiv = null
-
-			# Start listening to PDF wheel events when the pointer leaves the PDF region.
-			# P.S. This is the problem in a nutshell: although the pointer is elsewhere,
-			# wheel events keep being dispatched to the PDF.
-			@handlePdfDivMouseLeave = () =>
-				@pdfDiv.addEventListener "wheel", @dispatchToAce
-
-			# Stop listening to wheel events when the pointer enters the PDF region. If 
-			# the pointer is over the PDF, native behaviour is adequate. 
-			@handlePdfDivMouseEnter = () =>
-				@pdfDiv.removeEventListener "wheel", @dispatchToAce
-
-			# Set the "pointer over Ace" flag as false, when the mouse leaves its area.
-			@handleAceDivMouseLeave = () =>
-				@isOverAce = false
-
-			# Set the "pointer over Ace" flag as true, when the mouse enters its area.
-			@handleAceDivMouseEnter = () =>
-				@isOverAce = true
-
-			# Grab the elements (pdfDiv, aceDiv) and set the "hover" event listeners.
-			# If elements are already defined, clear existing event listeners and do
-			# the process again (grab elements, set listeners).	
-			@setListeners = () =>
-				@isOverAce = false
-
-				# If elements aren't null, remove existing listeners.
-				if @pdfDiv?
-					@pdfDiv.removeEventListener @handlePdfDivMouseLeave
-					@pdfDiv.removeEventListener @handlePdfDivMouseEnter
-
-				if @aceDiv?
-					@aceDiv.removeEventListener @handleAceDivMouseLeave
-					@aceDiv.removeEventListener @handleAceDivMouseEnter
-
-				# Grab elements.
-				@pdfDiv = document.querySelector ".pdfjs-viewer"	# Grab the PDF div.
-				@aceDiv = document.querySelector ".ace_content"		# Also the editor.
-
-				# Set hover-related listeners.
-				@pdfDiv.addEventListener "mouseleave", @handlePdfDivMouseLeave
-				@pdfDiv.addEventListener "mouseenter", @handlePdfDivMouseEnter
-				@aceDiv.addEventListener "mouseleave", @handleAceDivMouseLeave
-				@aceDiv.addEventListener "mouseenter", @handleAceDivMouseEnter
-
-			# Handler for wheel events on the PDF.
-			# If the pointer is over Ace, grab the event, prevent default behaviour
-			# and dispatch it to Ace.
-			@dispatchToAce = (e) =>
-				if @isOverAce
-					# If this is logged, the problem just happened: the event arrived
-					# here (the PDF wheel handler), but it should've gone to Ace.
-
-					# Small timeout - if we dispatch immediately, an exception is thrown.
-					window.setTimeout(() =>
-						# Dispatch the exact same event to Ace (this will keep values
-						# values e.g. `wheelDelta` consistent with user interaction).
-						@aceDiv.dispatchEvent e
-					, 1)
-
-					# Avoid scrolling the PDF, as we assume this was intended to the 
-					# editor.
-					e.preventDefault()
-
-			# "loaded" event is emitted from the pdfViewer controller $scope. This means
-			# that the previous PDF DOM element was destroyed and a new one is available,
-			# so we need to grab the elements and set the listeners again.
-			$scope.$on "loaded", () =>
-				@setListeners()
-
-

+ 0 - 21
services/web/public/coffee/ide/binary-files/BinaryFilesManager.coffee

@@ -1,21 +0,0 @@
-define [
-	"ide/binary-files/controllers/BinaryFileController"
-], () ->
-	class BinaryFilesManager
-		constructor: (@ide, @$scope) ->
-			@$scope.$on "entity:selected", (event, entity) =>
-				if (@$scope.ui.view != "track-changes" and entity.type == "file")
-					@openFile(entity)
-
-		openFile: (file) ->
-			@ide.fileTreeManager.selectEntity(file)
-			@$scope.ui.view = "file"
-			@$scope.openFile = null
-			@$scope.$apply()
-			window.setTimeout(
-				() =>
-					@$scope.openFile = file
-					@$scope.$apply()
-				, 0
-				, this
-			)

+ 0 - 125
services/web/public/coffee/ide/binary-files/controllers/BinaryFileController.coffee

@@ -1,125 +0,0 @@
-define [
-	"base"
-	"moment"
-], (App, moment) ->
-	App.controller "BinaryFileController", ["$scope", "$rootScope", "$http", "$timeout", "$element", "ide", "waitFor", ($scope, $rootScope, $http, $timeout, $element, ide, waitFor) ->
-
-		TWO_MEGABYTES = 2 * 1024 * 1024
-
-		textExtensions = ['bib', 'tex', 'txt', 'cls', 'sty']
-		imageExtentions = ['png', 'jpg', 'jpeg', 'gif']
-		previewableExtensions = ['eps', 'pdf']
-
-		extension = (file) ->
-			return file.name.split(".").pop()?.toLowerCase()
-
-		$scope.isTextFile = () =>
-			textExtensions.indexOf(extension($scope.openFile)) > -1
-		$scope.isImageFile = () =>
-			imageExtentions.indexOf(extension($scope.openFile)) > -1
-		$scope.isPreviewableFile = () =>
-			previewableExtensions.indexOf(extension($scope.openFile)) > -1
-		$scope.isUnpreviewableFile = () ->
-			!$scope.isTextFile() and
-			!$scope.isImageFile() and
-			!$scope.isPreviewableFile()
-
-		$scope.textPreview =
-			loading: false
-			shouldShowDots: false
-			error: false
-			data: null
-
-		$scope.refreshing = false
-		$scope.refreshError = null
-
-		MAX_URL_LENGTH = 60
-		FRONT_OF_URL_LENGTH = 35
-		FILLER = '...'
-		TAIL_OF_URL_LENGTH = MAX_URL_LENGTH - FRONT_OF_URL_LENGTH - FILLER.length
-		$scope.displayUrl = (url) ->
-			if !url?
-				return
-			if url.length > MAX_URL_LENGTH
-				front = url.slice(0, FRONT_OF_URL_LENGTH)
-				tail = url.slice(url.length - TAIL_OF_URL_LENGTH)
-				return front + FILLER + tail
-			else
-				return url
-
-		$scope.refreshFile = (file) ->
-			$scope.refreshing = true
-			$scope.refreshError = null
-			ide.fileTreeManager.refreshLinkedFile(file)
-				.then (response) ->
-					{ data } = response
-					{ new_file_id } = data
-					$timeout(
-						() ->
-							waitFor(
-								() ->
-									ide.fileTreeManager.findEntityById(new_file_id)
-								5000
-							)
-								.then (newFile) ->
-									ide.binaryFilesManager.openFile(newFile)
-								.catch (err) ->
-									console.warn(err)
-						, 0
-					)
-					$scope.refreshError = null
-				.catch (response) ->
-					$scope.refreshError = response.data
-				.finally () ->
-					$scope.refreshing = false
-
-		# Callback fired when the `img` tag fails to load,
-		# `failedLoad` used to show the "No Preview" message
-		$scope.failedLoad = false
-		window.sl_binaryFilePreviewError = () =>
-			$scope.failedLoad = true
-			$scope.$apply()
-
-		# Callback fired when the `img` tag is done loading,
-		# `imgLoaded` is used to show the spinner gif while loading
-		$scope.imgLoaded = false
-		window.sl_binaryFilePreviewLoaded = () =>
-			$scope.imgLoaded = true
-			$scope.$apply()
-
-		do loadTextFileFilePreview = () ->
-			return unless $scope.isTextFile()
-			url = "/project/#{project_id}/file/#{$scope.openFile.id}?range=0-#{TWO_MEGABYTES}"
-			$scope.textPreview.data = null
-			$scope.textPreview.loading = true
-			$scope.textPreview.shouldShowDots = false
-			$scope.$apply()
-			$http({
-				url: url,
-				method: 'GET',
-				transformResponse: null # Don't parse JSON
-			})
-				.then (response) ->
-					{ data } = response
-					$scope.textPreview.error = false
-					# show dots when payload is closs to cutoff
-					if data.length >= (TWO_MEGABYTES - 200)
-						$scope.textPreview.shouldShowDots = true
-						# remove last partial line
-						data = data?.replace?(/\n.*$/, '')
-					$scope.textPreview.data = data
-					$timeout(setHeight, 0)
-				.catch (error) ->
-					console.error(error)
-					$scope.textPreview.error = true
-					$scope.textPreview.loading = false
-
-		setHeight = () ->
-			$preview = $element.find('.text-preview .scroll-container')
-			$footer = $element.find('.binary-file-footer')
-			maxHeight = $element.height() - $footer.height() - 14 # borders + margin
-			$preview.css('max-height': maxHeight)
-			# Don't show the preview until we've set the height, otherwise we jump around
-			$scope.textPreview.loading = false
-
-	]

+ 0 - 46
services/web/public/coffee/ide/chat/controllers/ChatButtonController.coffee

@@ -1,46 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.controller "ChatButtonController", ($scope, ide) ->
-		$scope.toggleChat = () ->
-			$scope.ui.chatOpen = !$scope.ui.chatOpen
-			$scope.resetUnreadMessages()
-			
-		$scope.unreadMessages = 0
-		$scope.resetUnreadMessages = () ->
-			$scope.unreadMessages = 0
-			
-		$scope.$on "chat:resetUnreadMessages", (e) ->
-			$scope.resetUnreadMessages()
-			
-		$scope.$on "chat:newMessage", (e, message) ->
-			if message?
-				if message?.user?.id != ide.$scope.user.id
-					if !$scope.ui.chatOpen
-						$scope.unreadMessages += 1
-					flashTitle()
-
-		focussed = true
-		newMessageNotificationTimeout = null
-		originalTitle = null
-		$(window).on "focus", () ->
-			clearNewMessageNotification()
-			focussed = true
-		$(window).on "blur", () ->
-			focussed = false
-
-		flashTitle = () ->
-			if !focussed and !newMessageNotificationTimeout?
-				originalTitle ||= window.document.title
-				do changeTitle = () =>
-					if window.document.title == originalTitle
-						window.document.title = "New Message"
-					else
-						window.document.title = originalTitle
-					newMessageNotificationTimeout = setTimeout changeTitle, 800
-
-		clearNewMessageNotification = () ->
-			clearTimeout newMessageNotificationTimeout
-			newMessageNotificationTimeout = null
-			if originalTitle?
-				window.document.title = originalTitle

+ 0 - 33
services/web/public/coffee/ide/chat/controllers/ChatController.coffee

@@ -1,33 +0,0 @@
-define [
-	"base"
-	"ide/chat/services/chatMessages"
-], (App) ->
-	App.controller "ChatController", ($scope, chatMessages, ide, $location) ->
-		$scope.chat = chatMessages.state
-		
-		$scope.$watch "chat.messages", (messages) ->
-			if messages?
-				$scope.$emit "updateScrollPosition"
-		, true # Deep watch
-		
-		$scope.$on "layout:chat:resize", () ->
-			$scope.$emit "updateScrollPosition"
-			
-		$scope.$watch "chat.newMessage", (message) ->
-			if message?
-				ide.$scope.$broadcast "chat:newMessage", message
-				
-		$scope.resetUnreadMessages = () ->
-			ide.$scope.$broadcast "chat:resetUnreadMessages"
-				
-		$scope.sendMessage = ->
-			message = $scope.newMessageContent
-			$scope.newMessageContent = ""
-			chatMessages
-				.sendMessage message
-				
-		$scope.loadMoreMessages = ->
-			chatMessages.loadMoreMessages()
-			
-
-			

+ 0 - 24
services/web/public/coffee/ide/chat/controllers/ChatMessageController.coffee

@@ -1,24 +0,0 @@
-define [
-	"base"
-	"ide/colors/ColorManager"
-], (App, ColorManager) ->
-	App.controller "ChatMessageController", ["$scope", "ide", ($scope, ide) ->
-		hslColorConfigs =
-			borderSaturation: window.uiConfig?.chatMessageBorderSaturation or "70%"
-			borderLightness : window.uiConfig?.chatMessageBorderLightness or "70%"
-			bgSaturation    : window.uiConfig?.chatMessageBgSaturation or "60%"
-			bgLightness     : window.uiConfig?.chatMessageBgLightness or "97%"
-
-		hue = (user) ->
-			if !user?
-				return 0
-			else
-				return ColorManager.getHueForUserId(user.id)
-
-		$scope.getMessageStyle = (user) ->
-			"border-color"     : "hsl(#{ hue(user) }, #{ hslColorConfigs.borderSaturation }, #{ hslColorConfigs.borderLightness })"
-			"background-color" : "hsl(#{ hue(user) }, #{ hslColorConfigs.bgSaturation }, #{ hslColorConfigs.bgLightness })"
-
-		$scope.getArrowStyle = (user) ->
-			"border-color"     : "hsl(#{ hue(user) }, #{ hslColorConfigs.borderSaturation }, #{ hslColorConfigs.borderLightness })"
-	]

+ 0 - 7
services/web/public/coffee/ide/chat/index.coffee

@@ -1,7 +0,0 @@
-define [
-	"ide/chat/controllers/ChatButtonController"
-	"ide/chat/controllers/ChatController"
-	"ide/chat/controllers/ChatMessageController"
-	"directives/mathjax"
-	"filters/wrapLongWords"
-], () ->

+ 0 - 108
services/web/public/coffee/ide/chat/services/chatMessages.coffee

@@ -1,108 +0,0 @@
-define [
-	"base"
-	"libs/md5"
-], (App) ->
-	App.factory "chatMessages", ($http, ide) ->
-		MESSAGES_URL = "/project/#{ide.project_id}/messages"
-		MESSAGE_LIMIT = 50
-		CONNECTED_USER_URL = "/project/#{ide.project_id}/connected_users"
-
-		chat = {
-			state:
-				messages: []
-				loading: false
-				atEnd: false
-				errored:false
-				nextBeforeTimestamp: null
-				newMessage: null
-		}
-		
-		justSent = false	
-		ide.socket.on "new-chat-message", (message) =>
-			if message?.user?.id == ide.$scope.user.id and justSent
-				# Nothing to do
-			else
-				ide.$scope.$apply () ->
-					appendMessage(message)
-			justSent = false
-			
-		chat.sendMessage = (message) ->
-			body = 
-				content: message
-				_csrf : window.csrfToken
-			justSent = true
-			appendMessage({
-				user: ide.$scope.user
-				content: message
-				timestamp: Date.now()
-			})
-			return $http.post(MESSAGES_URL, body)
-			
-		chat.loadMoreMessages = () ->
-			return if chat.state.atEnd
-			return if chat.state.errored
-			url = MESSAGES_URL + "?limit=#{MESSAGE_LIMIT}"
-			if chat.state.nextBeforeTimestamp?
-				url += "&before=#{chat.state.nextBeforeTimestamp}"
-			chat.state.loading = true
-			return $http
-				.get(url)
-				.then (response) ->
-					messages = response.data ? []
-					chat.state.loading = false
-					if messages.length < MESSAGE_LIMIT
-						chat.state.atEnd = true
-					if !messages.reverse?
-						Raven?.captureException(new Error("messages has no reverse property #{typeof(messages)}"))
-					if typeof messages.reverse isnt 'function'
-						Raven?.captureException(new Error("messages.reverse not a function #{typeof(messages.reverse)} #{typeof(messages)}"))
-						chat.state.errored = true
-					else
-						messages.reverse()
-						prependMessages(messages)
-						chat.state.nextBeforeTimestamp = chat.state.messages[0]?.timestamp
-		
-		TIMESTAMP_GROUP_SIZE = 5 * 60 * 1000 # 5 minutes
-		
-		prependMessage = (message) ->
-			firstMessage = chat.state.messages[0]
-			shouldGroup = firstMessage? and
-				firstMessage.user.id == message?.user?.id and
-				firstMessage.timestamp - message.timestamp < TIMESTAMP_GROUP_SIZE
-			if shouldGroup
-				firstMessage.timestamp = message.timestamp
-				firstMessage.contents.unshift message.content
-			else
-				chat.state.messages.unshift({
-					user: formatUser(message.user)
-					timestamp: message.timestamp
-					contents: [message.content]
-				})
-				
-		prependMessages = (messages) ->
-			for message in messages.slice(0).reverse()
-				prependMessage(message)
-				
-		appendMessage = (message) ->
-			chat.state.newMessage = message
-			
-			lastMessage = chat.state.messages[chat.state.messages.length - 1]
-			shouldGroup = lastMessage? and
-				lastMessage.user.id == message?.user?.id and
-				message.timestamp - lastMessage.timestamp < TIMESTAMP_GROUP_SIZE
-			if shouldGroup
-				lastMessage.timestamp = message.timestamp
-				lastMessage.contents.push message.content
-			else
-				chat.state.messages.push({
-					user: formatUser(message.user)
-					timestamp: message.timestamp
-					contents: [message.content]
-				})
-				
-		formatUser = (user) ->
-			hash = CryptoJS.MD5(user.email.toLowerCase())
-			user.gravatar_url = "//www.gravatar.com/avatar/#{hash}"
-			return user
-
-		return chat

+ 0 - 9
services/web/public/coffee/ide/clone/controllers/CloneProjectController.coffee

@@ -1,9 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.controller 'CloneProjectController', ($scope, $modal) ->
-		$scope.openCloneProjectModal = () ->
-			$modal.open {
-				templateUrl: "cloneProjectModalTemplate"
-				controller:  "CloneProjectModalController"
-			}

+ 0 - 38
services/web/public/coffee/ide/clone/controllers/CloneProjectModalController.coffee

@@ -1,38 +0,0 @@
-define [
-	"base"
-], (App) ->
-	App.controller 'CloneProjectModalController', ($scope, $modalInstance, $timeout, $http, ide) ->
-		$scope.inputs = 
-			projectName: ide.$scope.project.name + " (Copy)"
-		$scope.state =
-			inflight: false
-			error: false
-
-		$modalInstance.opened.then () ->
-			$timeout () ->
-				$scope.$broadcast "open"
-			, 200
-			
-		cloneProject = (cloneName) ->
-			$http.post("/project/#{ide.$scope.project._id}/clone", {
-				_csrf: window.csrfToken
-				projectName: cloneName
-			})
-
-		$scope.clone = () ->
-			$scope.state.inflight = true
-			$scope.state.error = false
-			cloneProject($scope.inputs.projectName)
-				.then (response) ->
-					{ data } = response
-					window.location = "/project/#{data.project_id}"
-				.catch (response) ->
-					{ data, status } = response
-					$scope.state.inflight = false
-					if status == 400
-						$scope.state.error = { message: data }
-					else
-						$scope.state.error = true
-
-		$scope.cancel = () ->
-			$modalInstance.dismiss('cancel')

+ 0 - 4
services/web/public/coffee/ide/clone/index.coffee

@@ -1,4 +0,0 @@
-define [
-	"ide/clone/controllers/CloneProjectController"
-	"ide/clone/controllers/CloneProjectModalController"
-], () ->

+ 0 - 44
services/web/public/coffee/ide/colors/ColorManager.coffee

@@ -1,44 +0,0 @@
-define [], () ->
-	ColorManager =
-		getColorScheme: (hue, element) ->
-			if @isDarkTheme(element)
-				return {
-					cursor: "hsl(#{hue}, 70%, 50%)"
-					labelBackgroundColor: "hsl(#{hue}, 70%, 50%)"
-					highlightBackgroundColor: "hsl(#{hue}, 100%, 28%);"
-					strikeThroughBackgroundColor: "hsl(#{hue}, 100%, 20%);"
-					strikeThroughForegroundColor: "hsl(#{hue}, 100%, 60%);"
-				}
-			else
-				return {
-					cursor: "hsl(#{hue}, 70%, 50%)"
-					labelBackgroundColor: "hsl(#{hue}, 70%, 50%)"
-					highlightBackgroundColor: "hsl(#{hue}, 70%, 85%);"
-					strikeThroughBackgroundColor: "hsl(#{hue}, 70%, 95%);"
-					strikeThroughForegroundColor: "hsl(#{hue}, 70%, 40%);"
-				}
-
-		isDarkTheme: (element) ->
-			rgb = element.find(".ace_editor").css("background-color");
-			[m, r, g, b] = rgb.match(/rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)/)
-			r = parseInt(r, 10)
-			g = parseInt(g, 10)
-			b = parseInt(b, 10)
-			return r + g + b < 3 * 128
-		
-		OWN_HUE: 200 # We will always appear as this color to ourselves
-		ANONYMOUS_HUE: 100
-		getHueForUserId: (user_id) ->
-			if !user_id? or user_id == "anonymous-user"
-				return @ANONYMOUS_HUE
-
-			if window.user.id == user_id
-				return @OWN_HUE
-
-			hash = CryptoJS.MD5(user_id)
-			hue = parseInt(hash.toString().slice(0,8), 16) % 320
-			# Avoid 20 degrees either side of the personal hue
-			if hue > @OWNER_HUE - 20
-				hue = hue + 40
-			return hue
-		

+ 0 - 281
services/web/public/coffee/ide/connection/ConnectionManager.coffee

@@ -1,281 +0,0 @@
-
-define [], () ->
-	ONEHOUR = 1000 * 60 * 60
-	class ConnectionManager
-
-
-		disconnectAfterMs: ONEHOUR * 24
-
-		lastUserAction : new Date()
-
-		constructor: (@ide, @$scope) ->
-			if !io?
-				console.error "Socket.io javascript not loaded. Please check that the real-time service is running and accessible."
-				@ide.socket =
-					on: () ->
-				@$scope.$apply () =>
-					@$scope.state.error = "Could not connect to websocket server :("
-				return
-
-			setInterval(() =>
-				@disconnectIfInactive()
-			, ONEHOUR)
-
-			# trigger a reconnect immediately if network comes back online
-			window.addEventListener 'online', =>
-				sl_console.log "[online] browser notified online"
-				if !@connected
-					@tryReconnectWithRateLimit({force:true})
-
-			@userIsLeavingPage = false
-			window.addEventListener 'beforeunload', =>
-				@userIsLeavingPage = true
-				return # Don't return true or it will show a pop up
-
-			@connected = false
-			@userIsInactive = false
-			@gracefullyReconnecting = false
-
-			@$scope.connection =
-				reconnecting: false
-				# If we need to force everyone to reload the editor
-				forced_disconnect: false
-				inactive_disconnect: false
-
-			@$scope.tryReconnectNow = () =>
-				# user manually requested reconnection via "Try now" button
-				@tryReconnectWithRateLimit({force:true})
-
-			@$scope.$on 'cursor:editor:update', () =>
-				@lastUserAction = new Date()  # time of last edit
-				if !@connected
-					# user is editing, try to reconnect
-					@tryReconnectWithRateLimit()
-
-			document.querySelector('body').addEventListener 'click', (e) =>
-				if !@connected and e.target.id != 'try-reconnect-now-button'
-					# user is editing, try to reconnect
-					@tryReconnectWithRateLimit()
-
-			@ide.socket = io.connect null,
-				reconnect: false
-				'connect timeout': 30 * 1000
-				"force new connection": true
-
-			# The "connect" event is the first event we get back. It only
-			# indicates that the websocket is connected, we still need to
-			# pass authentication to join a project.
-
-			@ide.socket.on "connect", () =>
-				sl_console.log "[socket.io connect] Connected"
-
-			# The next event we should get is an authentication response
-			# from the server, either "connectionAccepted" or
-			# "connectionRejected".
-
-			@ide.socket.on 'connectionAccepted', (message) =>
-				sl_console.log "[socket.io connectionAccepted] allowed to connect"
-				@connected = true
-				@gracefullyReconnecting = false
-				@ide.pushEvent("connected")
-
-				@$scope.$apply () =>
-					@$scope.connection.reconnecting = false
-					@$scope.connection.inactive_disconnect = false
-					if @$scope.state.loading
-						@$scope.state.load_progress = 70
-
-				# we have passed authentication so we can now join the project
-				setTimeout(() =>
-					@joinProject()
-				, 100)
-
-			@ide.socket.on 'connectionRejected', (err) =>
-				sl_console.log "[socket.io connectionRejected] session not valid or other connection error"
-				# we have failed authentication, usually due to an invalid session cookie
-				return @reportConnectionError(err)
-
-			# Alternatively the attempt to connect can fail completely, so
-			# we never get into the "connect" state.
-
-			@ide.socket.on "connect_failed", () =>
-				@connected = false
-				@$scope.$apply () =>
-					@$scope.state.error = "Unable to connect, please view the <u><a href='/learn/Kb/Connection_problems'>connection problems guide</a></u> to fix the issue."
-
-			# We can get a "disconnect" event at any point after the
-			# "connect" event.
-
-			@ide.socket.on 'disconnect', () =>
-				sl_console.log "[socket.io disconnect] Disconnected"
-				@connected = false
-				@ide.pushEvent("disconnected")
-
-				@$scope.$apply () =>
-					@$scope.connection.reconnecting = false
-
-				if !@$scope.connection.forced_disconnect and !@userIsInactive and !@gracefullyReconnecting
-					@startAutoReconnectCountdown()
-
-			# Site administrators can send the forceDisconnect event to all users
-
-			@ide.socket.on 'forceDisconnect', (message) =>
-				@$scope.$apply () =>
-					@$scope.permissions.write = false
-					@$scope.connection.forced_disconnect = true
-				@ide.socket.disconnect()
-				@ide.showGenericMessageModal("Please Refresh", """
-					We're performing maintenance on ShareLaTeX and you need to refresh the editor.
-					Sorry for any inconvenience.
-					The editor will refresh in automatically in 10 seconds.
-				""")
-				setTimeout () ->
-					location.reload()
-				, 10 * 1000
-
-			@ide.socket.on "reconnectGracefully", () =>
-				sl_console.log "Reconnect gracefully"
-				@reconnectGracefully()
-
-		# Error reporting, which can reload the page if appropriate
-
-		reportConnectionError: (err) ->
-			sl_console.log "[socket.io] reporting connection error"
-			if err?.message == "not authorized" or err?.message == "invalid session"
-				window.location = "/login?redir=#{encodeURI(window.location.pathname)}"
-			else
-				@ide.socket.disconnect()
-				@ide.showGenericMessageModal("Something went wrong connecting", """
-					Something went wrong connecting to your project. Please refresh if this continues to happen.
-				""")
-
-		joinProject: () ->
-			sl_console.log "[joinProject] joining..."
-			# Note: if the "joinProject" message doesn't reach the server
-			# (e.g. if we are in a disconnected state at this point) the
-			# callback will never be executed
-			data = {
-				project_id: @ide.project_id
-			}
-			if window.anonymousAccessToken
-				data.anonymousAccessToken = window.anonymousAccessToken
-			@ide.socket.emit 'joinProject', data, (err, project, permissionsLevel, protocolVersion) =>
-				if err? or !project?
-					return @reportConnectionError(err)
-
-				if @$scope.protocolVersion? and @$scope.protocolVersion != protocolVersion
-					location.reload(true)
-
-				@$scope.$apply () =>
-					@$scope.protocolVersion = protocolVersion
-					@$scope.project = project
-					@$scope.permissionsLevel = permissionsLevel
-					@$scope.state.load_progress = 100
-					@$scope.state.loading = false
-					@$scope.$broadcast "project:joined"
-
-		reconnectImmediately: () ->
-			@disconnect()
-			@tryReconnect()
-
-		disconnect: () ->
-			sl_console.log "[socket.io] disconnecting client"
-			@ide.socket.disconnect()
-
-		startAutoReconnectCountdown: () ->
-			sl_console.log "[ConnectionManager] starting autoreconnect countdown"
-			twoMinutes = 2 * 60 * 1000
-			if @lastUserAction? and new Date() - @lastUserAction > twoMinutes
-				# between 1 minute and 3 minutes
-				countdown = 60 + Math.floor(Math.random() * 120)
-			else
-				countdown = 3 + Math.floor(Math.random() * 7)
-
-			if @userIsLeavingPage #user will have pressed refresh or back etc
-				return
-
-			@$scope.$apply () =>
-				@$scope.connection.reconnecting = false
-				@$scope.connection.reconnection_countdown = countdown
-
-			setTimeout(=>
-				if !@connected
-					@timeoutId = setTimeout (=> @decreaseCountdown()), 1000
-			, 200)
-
-		cancelReconnect: () ->
-			# clear timeout and set to null so we know there is no countdown running
-			if @timeoutId?
-				sl_console.log "[ConnectionManager] cancelling existing reconnect timer"
-				clearTimeout @timeoutId
-				@timeoutId = null
-
-		decreaseCountdown: () ->
-			@timeoutId = null
-			return if !@$scope.connection.reconnection_countdown?
-			sl_console.log "[ConnectionManager] decreasing countdown", @$scope.connection.reconnection_countdown
-			@$scope.$apply () =>
-				@$scope.connection.reconnection_countdown--
-
-			if @$scope.connection.reconnection_countdown <= 0
-				@$scope.$apply () =>
-					@tryReconnect()
-			else
-				@timeoutId = setTimeout (=> @decreaseCountdown()), 1000
-
-		tryReconnect: () ->
-			sl_console.log "[ConnectionManager] tryReconnect"
-			@cancelReconnect()
-			delete @$scope.connection.reconnection_countdown
-			return if @connected
-			@$scope.connection.reconnecting = true
-			# use socket.io connect() here to make a single attempt, the
-			# reconnect() method makes multiple attempts
-			@ide.socket.socket.connect()
-			# record the time of the last attempt to connect
-			@lastConnectionAttempt = new Date()
-			setTimeout (=> @startAutoReconnectCountdown() if !@connected), 2000
-
-		MIN_RETRY_INTERVAL: 1000 # ms, rate limit on reconnects for user clicking "try now"
-		BACKGROUND_RETRY_INTERVAL : 5 * 1000 # ms, rate limit on reconnects for other user activity (e.g. cursor moves)
-
-		tryReconnectWithRateLimit: (options) ->
-			# bail out if the reconnect is already in progress
-			return if @$scope.connection?.reconnecting
-			# bail out if we are going to reconnect soon anyway
-			reconnectingSoon = @$scope.connection?.reconnection_countdown? and @$scope.connection.reconnection_countdown <= 5
-			clickedTryNow = options?.force  # user requested reconnection
-			return if reconnectingSoon and not clickedTryNow
-			# bail out if we tried reconnecting recently
-			allowedInterval = if clickedTryNow then @MIN_RETRY_INTERVAL else @BACKGROUND_RETRY_INTERVAL
-			return if @lastConnectionAttempt? and new Date() - @lastConnectionAttempt < allowedInterval
-			@tryReconnect()
-
-		disconnectIfInactive: ()->
-			@userIsInactive = (new Date() - @lastUserAction) > @disconnectAfterMs
-			if @userIsInactive and @connected
-				@disconnect()
-				@$scope.$apply () =>
-					@$scope.connection.inactive_disconnect = true
-
-		RECONNECT_GRACEFULLY_RETRY_INTERVAL: 5000 # ms
-		MAX_RECONNECT_GRACEFULLY_INTERVAL: 60 * 5 * 1000 # 5 minutes
-		reconnectGracefully: () ->
-			@reconnectGracefullyStarted ?= new Date()
-			userIsInactive = (new Date() - @lastUserAction) > @RECONNECT_GRACEFULLY_RETRY_INTERVAL
-			maxIntervalReached = (new Date() - @reconnectGracefullyStarted) > @MAX_RECONNECT_GRACEFULLY_INTERVAL
-			if userIsInactive or maxIntervalReached
-				sl_console.log "[reconnectGracefully] User didn't do anything for last 5 seconds, reconnecting"
-				@_reconnectGracefullyNow()
-			else
-				sl_console.log "[reconnectGracefully] User is working, will try again in 5 seconds"
-				setTimeout () =>
-					@reconnectGracefully()
-				, @RECONNECT_GRACEFULLY_RETRY_INTERVAL
-
-		_reconnectGracefullyNow: () ->
-			@gracefullyReconnecting = true
-			@reconnectGracefullyStarted = null
-			# Clear cookie so we don't go to the same backend server
-			$.cookie("SERVERID", "", { expires: -1, path: "/" })
-			@reconnectImmediately()

+ 0 - 76
services/web/public/coffee/ide/directives/SafePath.coffee

@@ -1,76 +0,0 @@
-# This file is shared between the frontend and server code of web, so that
-# filename validation is the same in both implementations.
-# Both copies must be kept in sync:
-#   app/coffee/Features/Project/SafePath.coffee
-#   public/coffee/ide/directives/SafePath.coffee
-
-load = () ->
-	BADCHAR_RX = ///
-		[
-			\/ # no forward slashes
-			\\ # no back slashes
-			\* # no asterisk
-			\u0000-\u001F # no control characters (0-31)
-			\u007F        # no delete
-			\u0080-\u009F # no unicode control characters (C1)
-			\uD800-\uDFFF # no unicode surrogate characters
-		]
-		///g
-
-	BADFILE_RX = ///
-		(^\.$)      # reject . as a filename
-		|	(^\.\.$)  # reject .. as a filename
-		| (^\s+)    # reject leading space
-		| (\s+$)    # reject trailing space
-		///g
-
-	# Put a block on filenames which match javascript property names, as they
-	# can cause exceptions where the code puts filenames into a hash. This is a
-	# temporary workaround until the code in other places is made safe against
-	# property names.
-	#
-	# The list of property names is taken from
-	#   ['prototype'].concat(Object.getOwnPropertyNames(Object.prototype))
-	BLOCKEDFILE_RX = ///
-		^(
-			prototype
-			|constructor
-			|toString
-			|toLocaleString
-			|valueOf
-			|hasOwnProperty
-			|isPrototypeOf
-			|propertyIsEnumerable
-			|__defineGetter__
-			|__lookupGetter__
-			|__defineSetter__
-			|__lookupSetter__
-			|__proto__
-		)$
-	///
-
-	MAX_PATH = 1024 # Maximum path length, in characters. This is fairly arbitrary.
-
-	SafePath =
-		clean: (filename) ->
-			filename = filename.replace BADCHAR_RX, '_'
-			# for BADFILE_RX replace any matches with an equal number of underscores
-			filename = filename.replace BADFILE_RX, (match) ->
-				return new Array(match.length + 1).join("_")
-			# replace blocked filenames 'prototype' with '@prototype'
-			filename = filename.replace BLOCKEDFILE_RX, "@$1"
-			return filename
-
-		isCleanFilename: (filename) ->
-			return SafePath.isAllowedLength(filename) and
-				not BADCHAR_RX.test(filename) and
-				not BADFILE_RX.test(filename) and
-				not BLOCKEDFILE_RX.test(filename)
-
-		isAllowedLength: (pathname) ->
-			return pathname.length > 0 && pathname.length <= MAX_PATH
-
-if define?
-	define [], load
-else
-	module.exports = load()

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

@@ -1,203 +0,0 @@
-define [
-	"base"
-	"libs/jquery-layout"
-], (App) ->
-	App.directive "layout", ["$parse", "$compile", "ide", ($parse, $compile, ide) ->
-		return {
-			compile: () ->
-				pre: (scope, element, attrs) ->
-					name = attrs.layout
-
-					customTogglerPane = attrs.customTogglerPane
-					customTogglerMsgWhenOpen = attrs.customTogglerMsgWhenOpen
-					customTogglerMsgWhenClosed = attrs.customTogglerMsgWhenClosed
-					hasCustomToggler = customTogglerPane? and customTogglerMsgWhenOpen? and customTogglerMsgWhenClosed?
-
-					if attrs.spacingOpen?
-						spacingOpen = parseInt(attrs.spacingOpen, 10)
-					else
-						spacingOpen = window.uiConfig.defaultResizerSizeOpen
-
-					if attrs.spacingClosed?
-						spacingClosed = parseInt(attrs.spacingClosed, 10)
-					else
-						spacingClosed = window.uiConfig.defaultResizerSizeClosed
-
-					options =
-						spacing_open: spacingOpen
-						spacing_closed: spacingClosed
-						slidable: false
-						enableCursorHotkey: false
-						onopen: (pane) => 
-							onPaneOpen(pane)
-						onclose: (pane) => 
-							onPaneClose(pane)
-						onresize: () =>
-							onInternalResize()
-						maskIframesOnResize: scope.$eval(
-							attrs.maskIframesOnResize or "false"
-						)
-						east:
-							size: scope.$eval(attrs.initialSizeEast)
-							initClosed: scope.$eval(attrs.initClosedEast)
-						west:
-							size: scope.$eval(attrs.initialSizeEast)
-							initClosed: scope.$eval(attrs.initClosedWest)
-
-					# Restore previously recorded state
-					if (state = ide.localStorage("layout.#{name}"))?
-						if state.east?
-							if !attrs.minimumRestoreSizeEast? or (state.east.size >= attrs.minimumRestoreSizeEast and !state.east.initClosed)
-								options.east = state.east
-						if state.west?
-							if !attrs.minimumRestoreSizeWest? or (state.west.size >= attrs.minimumRestoreSizeWest and !state.west.initClosed)
-								options.west = state.west
-
-					if window.uiConfig.eastResizerCursor?
-						options.east.resizerCursor = window.uiConfig.eastResizerCursor
-
-					if window.uiConfig.westResizerCursor?
-						options.west.resizerCursor = window.uiConfig.westResizerCursor
-						
-					repositionControls = () ->
-						state = element.layout().readState()
-						if state.east?
-							controls = element.find("> .ui-layout-resizer-controls")
-							if state.east.initClosed
-								controls.hide()
-							else
-								controls.show()
-								controls.css({
-									right: state.east.size
-								})
-
-					repositionCustomToggler = () ->
-						if !customTogglerEl?
-							return
-						state = element.layout().readState()
-						positionAnchor = if customTogglerPane == "east" then "right" else "left"
-						paneState = state[customTogglerPane]
-						if paneState?
-							customTogglerEl.css(positionAnchor, if paneState.initClosed then 0 else paneState.size)
-	
-					resetOpenStates = () ->
-						state = element.layout().readState()
-						if attrs.openEast? and state.east?
-							openEast = $parse(attrs.openEast)
-							openEast.assign(scope, !state.east.initClosed)
-
-					# Someone moved the resizer
-					onInternalResize = () ->
-						state = element.layout().readState()
-						scope.$broadcast "layout:#{name}:resize", state
-						repositionControls()
-						if hasCustomToggler
-							repositionCustomToggler()
-						resetOpenStates()
-						
-					oldWidth = element.width()
-					# Something resized our parent element
-					onExternalResize = () ->
-						if attrs.resizeProportionally? and scope.$eval(attrs.resizeProportionally)
-							eastState = element.layout().readState().east
-							if eastState?
-								newInternalWidth = eastState.size / oldWidth * element.width()
-								oldWidth = element.width()
-								element.layout().sizePane("east", newInternalWidth)
-								return
-							
-						element.layout().resizeAll()
-
-					element.layout options
-					element.layout().resizeAll()
-
-					if attrs.resizeOn?
-						for event in attrs.resizeOn.split ","
-							scope.$on event, () -> onExternalResize()
-
-					if hasCustomToggler
-						state = element.layout().readState()
-						customTogglerScope = scope.$new()
-
-						customTogglerScope.isOpen = true
-						customTogglerScope.isVisible = true
-
-						if state[customTogglerPane]?.initClosed == true
-							customTogglerScope.isOpen = false
-
-						customTogglerScope.tooltipMsgWhenOpen = customTogglerMsgWhenOpen
-						customTogglerScope.tooltipMsgWhenClosed = customTogglerMsgWhenClosed
-							
-						customTogglerScope.tooltipPlacement = if customTogglerPane == "east" then "left" else "right"
-						customTogglerScope.handleClick = () ->
-							element.layout().toggle(customTogglerPane)
-							repositionCustomToggler()
-						customTogglerEl = $compile("
-							<a href 
-							   ng-show=\"isVisible\"
-							   class=\"custom-toggler #{ 'custom-toggler-' + customTogglerPane }\"
-							   ng-class=\"isOpen ? 'custom-toggler-open' : 'custom-toggler-closed'\"
-							   tooltip=\"{{ isOpen ? tooltipMsgWhenOpen : tooltipMsgWhenClosed }}\"
-							   tooltip-placement=\"{{ tooltipPlacement }}\"
-							   ng-click=\"handleClick()\">
-						")(customTogglerScope)
-						element.append(customTogglerEl)
-
-					onPaneOpen = (pane) ->
-						if !hasCustomToggler and pane != customTogglerPane
-							return
-						customTogglerEl.scope().$applyAsync () -> 
-							customTogglerEl.scope().isOpen = true
-
-					onPaneClose = (pane) ->
-						if !hasCustomToggler and pane != customTogglerPane
-							return
-						customTogglerEl.scope().$applyAsync () -> 
-							customTogglerEl.scope().isOpen = false
-
-					# Save state when exiting
-					$(window).unload () ->
-						ide.localStorage("layout.#{name}", element.layout().readState())
-
-					if attrs.openEast?
-						scope.$watch attrs.openEast, (value, oldValue) ->
-							if value? and value != oldValue
-								if value
-									element.layout().open("east")
-								else
-									element.layout().close("east")
-							setTimeout () ->
-								scope.$digest()
-							, 0
-
-					if attrs.allowOverflowOn?
-						layoutObj = element.layout()
-						overflowPane = scope.$eval(attrs.allowOverflowOn)
-						overflowPaneEl = layoutObj.panes[overflowPane]
-						# Set the panel as overflowing (gives it higher z-index and sets overflow rules)
-						layoutObj.allowOverflow overflowPane
-						# Read the given z-index value and increment it, so that it's higher than synctex controls.
-						overflowPaneZVal = overflowPaneEl.zIndex()
-						overflowPaneEl.css "z-index", overflowPaneZVal + 1
-
-					resetOpenStates()
-					onInternalResize()
-
-					if attrs.layoutDisabled?
-						scope.$watch attrs.layoutDisabled, (value) ->
-							if value
-								element.layout().hide("east")
-							else
-								element.layout().show("east")
-							if hasCustomToggler
-								customTogglerEl.scope().$applyAsync () ->
-									customTogglerEl.scope().isOpen = !value
-									customTogglerEl.scope().isVisible = !value
-
-
-				post: (scope, element, attrs) ->
-					name = attrs.layout
-					state = element.layout().readState()
-					scope.$broadcast "layout:#{name}:linked", state
-		}
-	]

+ 0 - 12
services/web/public/coffee/ide/directives/validFile.coffee

@@ -1,12 +0,0 @@
-define [
-	"base"
-	"ide/directives/SafePath"
-], (App, SafePath) ->
-
-	App.directive "validFile", () ->
-		return {
-			require: 'ngModel'
-			link: (scope, element, attrs, ngModelCtrl) ->
-				ngModelCtrl.$validators.validFile = (filename) ->
-					return SafePath.isCleanFilename filename
-		}

+ 0 - 29
services/web/public/coffee/ide/editor/AceShareJsCodec.coffee

@@ -1,29 +0,0 @@
-define [], () ->
-	AceShareJsCodec =
-		aceRangeToShareJs: (range, lines) ->
-			offset = 0
-			for line, i in lines
-				offset += if i < range.row
-					line.length
-				else
-					range.column
-			offset += range.row # Include newlines
-			return offset
-		
-		aceChangeToShareJs: (delta, lines) ->
-			offset = AceShareJsCodec.aceRangeToShareJs(delta.start, lines)
-
-			text = delta.lines.join('\n')
-			switch delta.action
-				when 'insert'
-					return { i: text, p: offset }
-				when 'remove'
-					return { d: text, p: offset }
-				else throw new Error "unknown action: #{delta.action}"
-		
-		shareJsOffsetToAcePosition: (offset, lines) ->
-			row = 0
-			for line, row in lines
-				break if offset <= line.length
-				offset -= lines[row].length + 1 # + 1 for newline char
-			return {row:row, column:offset}

+ 0 - 410
services/web/public/coffee/ide/editor/Document.coffee

@@ -1,410 +0,0 @@
-define [
-	"utils/EventEmitter"
-	"ide/editor/ShareJsDoc"
-	"ide/review-panel/RangesTracker"
-], (EventEmitter, ShareJsDoc, RangesTracker) ->
-	class Document extends EventEmitter
-		@getDocument: (ide, doc_id) ->
-			@openDocs ||= {}
-			if !@openDocs[doc_id]?
-				sl_console.log "[getDocument] Creating new document instance for #{doc_id}"
-				@openDocs[doc_id] = new Document(ide, doc_id)
-			else
-				sl_console.log "[getDocument] Returning existing document instance for #{doc_id}"
-			return @openDocs[doc_id]
-
-		@hasUnsavedChanges: () ->
-			for doc_id, doc of (@openDocs or {})
-				return true if doc.hasBufferedOps()
-			return false
-
-		@flushAll: () ->
-			for doc_id, doc of @openDocs
-				doc.flush()
-
-		constructor: (@ide, @doc_id) ->
-			@connected = @ide.socket.socket.connected
-			@joined = false
-			@wantToBeJoined = false
-			@_checkAceConsistency = _.bind(@_checkConsistency, @, @ace)
-			@_checkCMConsistency = _.bind(@_checkConsistency, @, @cm)
-			@inconsistentCount = 0
-			@_bindToEditorEvents()
-			@_bindToSocketEvents()
-
-		attachToAce: (@ace) ->
-			@doc?.attachToAce(@ace)
-			editorDoc = @ace.getSession().getDocument()
-			editorDoc.on "change", @_checkAceConsistency
-			@ide.$scope.$emit 'document:opened', @doc
-
-		detachFromAce: () ->
-			@doc?.detachFromAce()
-			editorDoc = @ace?.getSession().getDocument()
-			editorDoc?.off "change", @_checkAceConsistency
-			@ide.$scope.$emit 'document:closed', @doc
-
-		attachToCM: (@cm) ->
-			@doc?.attachToCM(@cm)
-			@cm?.on "change", @_checkCMConsistency
-			@ide.$scope.$emit 'document:opened', @doc
-
-		detachFromCM: () ->
-			@doc?.detachFromCM()
-			@cm?.off "change", @_checkCMConsistency
-			@ide.$scope.$emit 'document:closed', @doc
-
-		submitOp: (args...) -> @doc?.submitOp(args...)
-
-		_checkConsistency: (editor) ->
-			return () =>
-				# We've been seeing a lot of errors when I think there shouldn't be
-				# any, which may be related to this check happening before the change is
-				# applied. If we use a timeout, hopefully we can reduce this.
-				setTimeout () =>
-					editorValue = editor?.getValue()
-					sharejsValue = @doc?.getSnapshot()
-					if editorValue != sharejsValue
-						@inconsistentCount++
-					else
-						@inconsistentCount = 0
-
-					if @inconsistentCount >= 3
-						@_onError new Error("Editor text does not match server text")
-				, 0
-
-		getSnapshot: () ->
-			@doc?.getSnapshot()
-
-		getType: () ->
-			@doc?.getType()
-
-		getInflightOp: () ->
-			@doc?.getInflightOp()
-
-		getPendingOp: () ->
-			@doc?.getPendingOp()
-
-		getRecentAck: () ->
-			@doc?.getRecentAck()
-
-		getOpSize: (op) ->
-			@doc?.getOpSize(op)
-
-		hasBufferedOps: () ->
-			@doc?.hasBufferedOps()
-		
-		setTrackingChanges: (track_changes) ->
-			@doc.track_changes = track_changes
-		
-		getTrackingChanges: () ->
-			!!@doc.track_changes
-		
-		setTrackChangesIdSeeds: (id_seeds) ->
-			@doc.track_changes_id_seeds = id_seeds
-
-		_bindToSocketEvents: () ->
-			@_onUpdateAppliedHandler = (update) => @_onUpdateApplied(update)
-			@ide.socket.on "otUpdateApplied", @_onUpdateAppliedHandler
-			@_onErrorHandler = (error, update) => @_onError(error, update)
-			@ide.socket.on "otUpdateError", @_onErrorHandler
-			@_onDisconnectHandler = (error) => @_onDisconnect(error)
-			@ide.socket.on "disconnect", @_onDisconnectHandler
-
-		_bindToEditorEvents: () ->
-			onReconnectHandler = (update) =>
-				@_onReconnect(update)
-			@_unsubscribeReconnectHandler = @ide.$scope.$on "project:joined", onReconnectHandler
-
-		_unBindFromEditorEvents: () ->
-			@_unsubscribeReconnectHandler()
-
-		_unBindFromSocketEvents: () ->
-			@ide.socket.removeListener "otUpdateApplied", @_onUpdateAppliedHandler
-			@ide.socket.removeListener "otUpdateError", @_onErrorHandler
-			@ide.socket.removeListener "disconnect", @_onDisconnectHandler
-
-		leaveAndCleanUp: () ->
-			@leave (error) =>
-				@_cleanUp()
-
-		join: (callback = (error) ->) ->
-			@wantToBeJoined = true
-			@_cancelLeave()
-			if @connected
-				return @_joinDoc callback
-			else
-				@_joinCallbacks ||= []
-				@_joinCallbacks.push callback
-
-		leave: (callback = (error) ->) ->
-			@wantToBeJoined = false
-			@_cancelJoin()
-			if (@doc? and @doc.hasBufferedOps())
-				sl_console.log "[leave] Doc has buffered ops, pushing callback for later"
-				@_leaveCallbacks ||= []
-				@_leaveCallbacks.push callback
-			else if !@connected
-				sl_console.log "[leave] Not connected, returning now"
-				callback()
-			else
-				sl_console.log "[leave] Leaving now"
-				@_leaveDoc(callback)
-
-		flush: () ->
-			@doc?.flushPendingOps()
-
-		chaosMonkey: (line = 0, char = "a") ->
-			orig = char
-			copy = null
-			pos = 0
-			timer = () =>
-				unless copy? and copy.length
-					copy = orig.slice() + ' ' + new Date() + '\n'
-					line += if Math.random() > 0.1 then 1 else -2
-					line = 0 if line < 0
-					pos = 0
-				char = copy[0]
-				copy = copy.slice(1)
-				@ace.session.insert({row: line, column: pos}, char)
-				pos += 1
-				@_cm = setTimeout timer, 100 + if Math.random() < 0.1 then 1000 else 0
-			@_cm = timer()
-
-		clearChaosMonkey: () ->
-			clearTimeout @_cm
-
-		MAX_PENDING_OP_SIZE: 64 # pending ops bigger than this are always considered unsaved
-
-		pollSavedStatus: () ->
-			# returns false if doc has ops waiting to be acknowledged or
-			# sent that haven't changed since the last time we checked.
-			# Otherwise returns true.
-			inflightOp = @getInflightOp()
-			pendingOp = @getPendingOp()
-			recentAck = @getRecentAck()
-			pendingOpSize = pendingOp? && @getOpSize(pendingOp)
-			if !inflightOp? and !pendingOp?
-				# there's nothing going on, this is ok.
-				saved = true
-				sl_console.log "[pollSavedStatus] no inflight or pending ops"
-			else if inflightOp? and inflightOp == @oldInflightOp
-				# The same inflight op has been sitting unacked since we
-				# last checked, this is bad.
-				saved = false
-				sl_console.log "[pollSavedStatus] inflight op is same as before"
-			else if pendingOp? and recentAck && pendingOpSize < @MAX_PENDING_OP_SIZE
-				# There is an op waiting to go to server but it is small and
-				# within the flushDelay, this is ok for now.
-				saved = true
-				sl_console.log "[pollSavedStatus] pending op (small with recent ack) assume ok", pendingOp, pendingOpSize
-			else
-				# In any other situation, assume the document is unsaved.
-				saved = false
-				sl_console.log "[pollSavedStatus] assuming not saved (inflightOp?: #{inflightOp?}, pendingOp?: #{pendingOp?})"
-
-			@oldInflightOp = inflightOp
-			return saved
-
-		_cancelLeave: () ->
-			if @_leaveCallbacks?
-				delete @_leaveCallbacks
-
-		_cancelJoin: () ->
-			if @_joinCallbacks?
-				delete @_joinCallbacks
-
-		_onUpdateApplied: (update) ->
-			@ide.pushEvent "received-update",
-				doc_id: @doc_id
-				remote_doc_id: update?.doc
-				wantToBeJoined: @wantToBeJoined
-				update: update
-
-			if window.disconnectOnAck? and Math.random() < window.disconnectOnAck
-				sl_console.log "Disconnecting on ack", update
-				window._ide.socket.socket.disconnect()
-				# Pretend we never received the ack
-				return
-
-			if window.dropAcks? and Math.random() < window.dropAcks
-				if !update.op? # Only drop our own acks, not collaborator updates
-					sl_console.log "Simulating a lost ack", update
-					return
-
-			if update?.doc == @doc_id and @doc?
-				@doc.processUpdateFromServer update
-
-				if !@wantToBeJoined
-					@leave()
-
-		_onDisconnect: () ->
-			sl_console.log '[onDisconnect] disconnecting'
-			@connected = false
-			@joined = false
-			@doc?.updateConnectionState "disconnected"
-
-		_onReconnect: () ->
-			sl_console.log "[onReconnect] reconnected (joined project)"
-			@ide.pushEvent "reconnected:afterJoinProject"
-
-			@connected = true
-			if @wantToBeJoined or @doc?.hasBufferedOps()
-				sl_console.log "[onReconnect] Rejoining (wantToBeJoined: #{@wantToBeJoined} OR hasBufferedOps: #{@doc?.hasBufferedOps()})"
-				@_joinDoc (error) =>
-					return @_onError(error) if error?
-					@doc.updateConnectionState "ok"
-					@doc.flushPendingOps()
-					@_callJoinCallbacks()
-
-		_callJoinCallbacks: () ->
-			for callback in @_joinCallbacks or []
-				callback()
-			delete @_joinCallbacks
-
-		_joinDoc: (callback = (error) ->) ->
-			if @doc?
-				@ide.socket.emit 'joinDoc', @doc_id, @doc.getVersion(), { encodeRanges: true }, (error, docLines, version, updates, ranges) =>
-					return callback(error) if error?
-					@joined = true
-					@doc.catchUp( updates )
-					@_decodeRanges(ranges)
-					@_catchUpRanges(ranges?.changes, ranges?.comments)
-					callback()
-			else
-				@ide.socket.emit 'joinDoc', @doc_id, { encodeRanges: true }, (error, docLines, version, updates, ranges) =>
-					return callback(error) if error?
-					@joined = true
-					@doc = new ShareJsDoc @doc_id, docLines, version, @ide.socket
-					@_decodeRanges(ranges)
-					@ranges = new RangesTracker(ranges?.changes, ranges?.comments)
-					@_bindToShareJsDocEvents()
-					callback()
-
-		_decodeRanges: (ranges) ->
-			decodeFromWebsockets = (text) -> decodeURIComponent(escape(text))
-			try
-				for change in ranges.changes or []
-					change.op.i = decodeFromWebsockets(change.op.i) if change.op.i?
-					change.op.d = decodeFromWebsockets(change.op.d) if change.op.d?
-				for comment in ranges.comments or []
-					comment.op.c = decodeFromWebsockets(comment.op.c) if comment.op.c?
-			catch err
-				console.log(err)
-
-		_leaveDoc: (callback = (error) ->) ->
-			sl_console.log '[_leaveDoc] Sending leaveDoc request'
-			@ide.socket.emit 'leaveDoc', @doc_id, (error) =>
-				return callback(error) if error?
-				@joined = false
-				for callback in @_leaveCallbacks or []
-					sl_console.log '[_leaveDoc] Calling buffered callback', callback
-					callback(error)
-				delete @_leaveCallbacks
-				callback(error)
-
-		_cleanUp: () ->
-			if Document.openDocs[@doc_id] == @
-				sl_console.log "[_cleanUp] Removing self (#{@doc_id}) from in openDocs"
-				delete Document.openDocs[@doc_id]
-			else
-				# It's possible that this instance has error, and the doc has been reloaded.
-				# This creates a new instance in Document.openDoc with the same id. We shouldn't
-				# clear it because it's not this instance.
-				sl_console.log "[_cleanUp] New instance of (#{@doc_id}) created. Not removing"
-			@_unBindFromEditorEvents()
-			@_unBindFromSocketEvents()
-
-		_bindToShareJsDocEvents: () ->
-			@doc.on "error", (error, meta) => @_onError error, meta
-			@doc.on "externalUpdate", (update) => 
-				@ide.pushEvent "externalUpdate",
-					doc_id: @doc_id
-				@trigger "externalUpdate", update
-			@doc.on "remoteop", (args...) => 
-				@ide.pushEvent "remoteop",
-					doc_id: @doc_id
-				@trigger "remoteop", args...
-			@doc.on "op:sent", (op) =>
-				@ide.pushEvent "op:sent",
-					doc_id: @doc_id
-					op: op
-				@trigger "op:sent"
-			@doc.on "op:acknowledged", (op) =>
-				@ide.pushEvent "op:acknowledged",
-					doc_id: @doc_id
-					op: op
-				@ide.$scope.$emit "ide:opAcknowledged",
-					doc_id: @doc_id
-					op: op
-				@trigger "op:acknowledged"
-			@doc.on "op:timeout", (op) =>
-				@ide.pushEvent "op:timeout",
-					doc_id: @doc_id
-					op: op
-				@trigger "op:timeout"
-				@_onError new Error("op timed out"), {op: op}
-			@doc.on "flush", (inflightOp, pendingOp, version) =>
-				@ide.pushEvent "flush",
-					doc_id: @doc_id,
-					inflightOp: inflightOp,
-					pendingOp: pendingOp
-					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
-			sl_console.log "ShareJS error", error, meta
-			ga?('send', 'event', 'error', "shareJsError", "#{error.message} - #{@ide.socket.socket.transport.name}" )
-			@doc?.clearInflightAndPendingOps()
-			@trigger "error", error, meta
-			# The clean up should run after the error is triggered because the error triggers a
-			# disconnect. If we run the clean up first, we remove our event handlers and miss
-			# the disconnect event, which means we try to leaveDoc when the connection comes back.
-			# This could intefere with the new connection of a new instance of this document.
-			@_cleanUp()
-
-		_applyOpsToRanges: (ops = [], oldSnapshot, msg) ->
-			track_changes_as = null
-			remote_op = msg?
-			if msg?.meta?.tc?
-				old_id_seed = @ranges.getIdSeed()
-				@ranges.setIdSeed(msg.meta.tc)
-			if remote_op and msg.meta?.tc
-				track_changes_as = msg.meta.user_id
-			else if !remote_op and @track_changes_as?
-				track_changes_as = @track_changes_as
-			@ranges.track_changes = track_changes_as?
-			for op in ops
-				@ranges.applyOp op, { user_id: track_changes_as }
-			if old_id_seed?
-				@ranges.setIdSeed(old_id_seed)
-			if remote_op
-				# With remote ops, Ace hasn't been updated when we receive this op,
-				# so defer updating track changes until it has
-				setTimeout () => @emit "ranges:dirty"
-			else
-				@emit "ranges:dirty"
-		
-		_catchUpRanges: (changes = [], comments = []) ->
-			# We've just been given the current server's ranges, but need to apply any local ops we have.
-			# Reset to the server state then apply our local ops again.
-			@emit "ranges:clear"
-			@ranges.changes = changes
-			@ranges.comments = comments
-			@ranges.track_changes = @doc.track_changes
-			for op in @doc.getInflightOp() or []
-				@ranges.setIdSeed(@doc.track_changes_id_seeds.inflight)
-				@ranges.applyOp(op, { user_id: @track_changes_as })
-			for op in @doc.getPendingOp() or []
-				@ranges.setIdSeed(@doc.track_changes_id_seeds.pending)
-				@ranges.applyOp(op, { user_id: @track_changes_as })
-			@emit "ranges:redraw"

+ 0 - 196
services/web/public/coffee/ide/editor/EditorManager.coffee

@@ -1,196 +0,0 @@
-define [
-	"ide/editor/Document"
-	"ide/editor/components/spellMenu"
-	"ide/editor/directives/aceEditor"
-	"ide/editor/directives/toggleSwitch"
-	"ide/editor/controllers/SavingNotificationController"
-], (Document) ->
-	class EditorManager
-		constructor: (@ide, @$scope, @localStorage) ->
-			@$scope.editor = {
-				sharejs_doc: null
-				open_doc_id: null
-				open_doc_name: null
-				opening: true
-				trackChanges: false
-				wantTrackChanges: false
-				showRichText: @showRichText()
-			}
-
-			@$scope.$on "entity:selected", (event, entity) =>
-				if (@$scope.ui.view != "history" and entity.type == "doc")
-					@openDoc(entity)
-
-			@$scope.$on "entity:deleted", (event, entity) =>
-				if @$scope.editor.open_doc_id == entity.id
-					return if !@$scope.project.rootDoc_id
-					doc = @ide.fileTreeManager.findEntityById(@$scope.project.rootDoc_id)
-					return if !doc?
-					@openDoc(doc)
-
-			initialized = false
-			@$scope.$on "file-tree:initialized", () =>
-				if !initialized
-					initialized = true
-					@autoOpenDoc()
-
-			@$scope.$on "flush-changes", () =>
-				Document.flushAll()
-
-			@$scope.$watch "editor.wantTrackChanges", (value) =>
-				return if !value?
-				@_syncTrackChangesState(@$scope.editor.sharejs_doc)
-
-		showRichText: () ->
-			if !window.richTextEnabled
-				return false
-			@localStorage("editor.mode.#{@$scope.project_id}") == 'rich-text'
-
-		autoOpenDoc: () ->
-			open_doc_id =
-				@ide.localStorage("doc.open_id.#{@$scope.project_id}") or
-				@$scope.project.rootDoc_id
-			return if !open_doc_id?
-			doc = @ide.fileTreeManager.findEntityById(open_doc_id)
-			return if !doc?
-			@openDoc(doc)
-
-		openDocId: (doc_id, options = {}) ->
-			doc = @ide.fileTreeManager.findEntityById(doc_id)
-			return if !doc?
-			@openDoc(doc, options)
-
-		openDoc: (doc, options = {}) ->
-			sl_console.log "[openDoc] Opening #{doc.id}"
-			@$scope.ui.view = "editor"
-
-			done = () =>
-				if options.gotoLine?
-					# allow Ace to display document before moving, delay until next tick
-					# added delay to make this happen later that gotoStoredPosition in
-					# CursorPositionManager
-					setTimeout () =>
-						@$scope.$broadcast "editor:gotoLine", options.gotoLine, options.gotoColumn
-					, 0
-				else if options.gotoOffset?
-					setTimeout () =>
-						@$scope.$broadcast "editor:gotoOffset", options.gotoOffset
-					, 0
-
-
-			if doc.id == @$scope.editor.open_doc_id and !options.forceReopen
-				@$scope.$apply () =>
-					done()
-				return
-
-			@$scope.editor.open_doc_id = doc.id
-			@$scope.editor.open_doc_name = doc.name
-
-			@ide.localStorage "doc.open_id.#{@$scope.project_id}", doc.id
-			@ide.fileTreeManager.selectEntity(doc)
-
-			@$scope.editor.opening = true
-			@_openNewDocument doc, (error, sharejs_doc) =>
-				if error?
-					@ide.showGenericMessageModal(
-						"Error opening document"
-						"Sorry, something went wrong opening this document. Please try again."
-					)
-					return
-
-				@_syncTrackChangesState(sharejs_doc)
-
-				@$scope.$broadcast "doc:opened"
-
-				@$scope.$apply () =>
-					@$scope.editor.opening = false
-					@$scope.editor.sharejs_doc = sharejs_doc
-					done()
-
-		_openNewDocument: (doc, callback = (error, sharejs_doc) ->) ->
-			sl_console.log "[_openNewDocument] Opening..."
-			current_sharejs_doc = @$scope.editor.sharejs_doc
-			if current_sharejs_doc?
-				sl_console.log "[_openNewDocument] Leaving existing open doc..."
-				current_sharejs_doc.leaveAndCleanUp()
-				@_unbindFromDocumentEvents(current_sharejs_doc)
-
-			new_sharejs_doc = Document.getDocument @ide, doc.id
-
-			new_sharejs_doc.join (error) =>
-				return callback(error) if error?
-				@_bindToDocumentEvents(doc, new_sharejs_doc)
-				callback null, new_sharejs_doc
-
-
-		_bindToDocumentEvents: (doc, sharejs_doc) ->
-			sharejs_doc.on "error", (error, meta) =>
-				if error?.message?
-					message = error.message
-				else if typeof error == "string"
-					message = error
-				else
-					message = ""
-				if /maxDocLength/.test(message)
-					@ide.showGenericMessageModal(
-						"Document Too Long"
-						"Sorry, this file is too long to be edited manually. Please upload it directly."
-					)
-				else if /too many comments or tracked changes/.test(message)
-					@ide.showGenericMessageModal(
-						"Too many comments or tracked changes"
-						"Sorry, this file has too many comments or tracked changes. Please try accepting or rejecting some existing changes, or resolving and deleting some comments."
-					)
-				else
-					@ide.socket.disconnect()
-					@ide.reportError(error, meta)
-					@ide.showGenericMessageModal(
-						"Out of sync"
-						"Sorry, this file has gone out of sync and we need to do a full refresh. <br> <a href='/learn/Kb/Editor_out_of_sync_problems'>Please see this help guide for more information</a>"
-					)
-				@openDoc(doc, forceReopen: true)
-
-			sharejs_doc.on "externalUpdate", (update) =>
-				return if @_ignoreExternalUpdates
-				@ide.showGenericMessageModal(
-					"Document Updated Externally"
-					"This document was just updated externally. Any recent changes you have made may have been overwritten. To see previous versions please look in the history."
-				)
-
-		_unbindFromDocumentEvents: (document) ->
-			document.off()
-
-		getCurrentDocValue: () ->
-			@$scope.editor.sharejs_doc?.getSnapshot()
-
-		getCurrentDocId: () ->
-			@$scope.editor.open_doc_id
-
-		startIgnoringExternalUpdates: () ->
-			@_ignoreExternalUpdates = true
-
-		stopIgnoringExternalUpdates: () ->
-			@_ignoreExternalUpdates = false
-
-		_syncTimeout: null
-		_syncTrackChangesState: (doc) ->
-			return if !doc?
-
-			if @_syncTimeout?
-				clearTimeout @_syncTimeout
-				@_syncTimeout = null
-
-			want = @$scope.editor.wantTrackChanges
-			have = doc.getTrackingChanges()
-			if want == have
-				@$scope.editor.trackChanges = want
-				return
-
-			do tryToggle = () =>
-				saved = !doc.getInflightOp()? and !doc.getPendingOp()?
-				if saved
-					doc.setTrackingChanges(want)
-					@$scope.$apply () =>
-						@$scope.editor.trackChanges = want
-				else
-					@_syncTimeout = setTimeout tryToggle, 100

+ 0 - 190
services/web/public/coffee/ide/editor/ShareJsDoc.coffee

@@ -1,190 +0,0 @@
-define [
-	"utils/EventEmitter"
-	"libs/sharejs"
-], (EventEmitter, ShareJs) ->
-	SINGLE_USER_FLUSH_DELAY = 1000 #ms
-
-	class ShareJsDoc extends EventEmitter
-		constructor: (@doc_id, docLines, version, @socket) ->
-			# Dencode any binary bits of data
-			# See http://ecmanaut.blogspot.co.uk/2006/07/encoding-decoding-utf8-in-javascript.html
-			@type = "text"
-			docLines = (decodeURIComponent(escape(line)) for line in docLines)
-			snapshot = docLines.join("\n")
-			@track_changes = false
-
-			@connection = {
-				send: (update) =>
-					@_startInflightOpTimeout(update)
-					if window.disconnectOnUpdate? and Math.random() < window.disconnectOnUpdate
-						sl_console.log "Disconnecting on update", update
-						window._ide.socket.socket.disconnect()
-					if window.dropUpdates? and Math.random() < window.dropUpdates
-						sl_console.log "Simulating a lost update", update
-						return
-					if @track_changes
-						update.meta ?= {}
-						update.meta.tc = @track_changes_id_seeds.inflight
-					@socket.emit "applyOtUpdate", @doc_id, update, (error) =>
-						return @_handleError(error) if error?
-				state: "ok"
-				id:    @socket.socket.sessionid
-			}
-
-			@_doc = new ShareJs.Doc @connection, @doc_id,
-				type: @type
-			@_doc.setFlushDelay(SINGLE_USER_FLUSH_DELAY)
-			@_doc.on "change", (args...) =>
-				@trigger "change", args...
-			@_doc.on "acknowledge", () =>
-				@lastAcked = new Date() # note time of last ack from server for an op we sent
-				@trigger "acknowledge"
-			@_doc.on "remoteop", (args...) =>
-				# As soon as we're working with a collaborator, start sending
-				# ops as quickly as possible for low latency.
-				@_doc.setFlushDelay(0)
-				@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)
-
-			@_bindToDocChanges(@_doc)
-
-			@processUpdateFromServer
-				open: true
-				v: version
-				snapshot: snapshot
-
-		submitOp: (args...) -> @_doc.submitOp(args...)
-
-		processUpdateFromServer: (message) ->
-			try
-				@_doc._onMessage message
-			catch error
-				# Version mismatches are thrown as errors
-				console.log error
-				@_handleError(error)
-
-			if message?.meta?.type == "external"
-				@trigger "externalUpdate", message
-
-		catchUp: (updates) ->
-			for update, i in updates
-				update.v   = @_doc.version
-				update.doc = @doc_id
-				@processUpdateFromServer(update)
-
-		getSnapshot: () -> @_doc.snapshot
-		getVersion: () -> @_doc.version
-		getType: () -> @type
-
-		clearInflightAndPendingOps: () ->
-			@_doc.inflightOp = null
-			@_doc.inflightCallbacks = []
-			@_doc.pendingOp = null
-			@_doc.pendingCallbacks = []
-
-		flushPendingOps: () ->
-			# This will flush any ops that are pending.
-			# If there is an inflight op it will do nothing.
-			@_doc.flush()
-
-		updateConnectionState: (state) ->
-			sl_console.log "[updateConnectionState] Setting state to #{state}"
-			@connection.state = state
-			@connection.id = @socket.socket.sessionid
-			@_doc.autoOpen = false
-			@_doc._connectionStateChanged(state)
-			@lastAcked = null # reset the last ack time when connection changes
-
-		hasBufferedOps: () ->
-			@_doc.inflightOp? or @_doc.pendingOp?
-
-		getInflightOp: () -> @_doc.inflightOp
-		getPendingOp: () -> @_doc.pendingOp
-		getRecentAck: () ->
-			# check if we have received an ack recently (within a factor of two of the single user flush delay)
-			@lastAcked? and new Date() - @lastAcked <  2 * SINGLE_USER_FLUSH_DELAY
-		getOpSize: (op) ->
-			# compute size of an op from its components
-			# (total number of characters inserted and deleted)
-			size = 0
-			for component in op or []
-				if component?.i?
-					size += component.i.length
-				if component?.d?
-					size += component.d.length
-			return size
-
-		attachToAce: (ace) -> @_doc.attach_ace(ace, false, window.maxDocLength)
-		detachFromAce: () -> @_doc.detach_ace?()
-
-		attachToCM: (cm) -> @_doc.attach_cm(cm, false)
-		detachFromCM: () -> @_doc.detach_cm?()
-
-		INFLIGHT_OP_TIMEOUT: 5000 # Retry sending ops after 5 seconds without an ack
-		WAIT_FOR_CONNECTION_TIMEOUT: 500 # If we're waiting for the project to join, try again in 0.5 seconds
-		_startInflightOpTimeout: (update) ->
-			@_startFatalTimeoutTimer(update)
-			retryOp = () =>
-				# Only send the update again if inflightOp is still populated
-				# This can be cleared when hard reloading the document in which
-				# case we don't want to keep trying to send it.
-				sl_console.log "[inflightOpTimeout] Trying op again"
-				if @_doc.inflightOp?
-					# When there is a socket.io disconnect, @_doc.inflightSubmittedIds
-					# is updated with the socket.io client id of the current op in flight
-					# (meta.source of the op).
-					# @connection.id is the client id of the current socket.io session.
-					# So we need both depending on whether the op was submitted before
-					# one or more disconnects, or if it was submitted during the current session.
-					update.dupIfSource = [@connection.id, @_doc.inflightSubmittedIds...]
-					
-					# We must be joined to a project for applyOtUpdate to work on the real-time
-					# service, so don't send an op if we're not. Connection state is set to 'ok'
-					# when we've joined the project
-					if @connection.state != "ok"
-						sl_console.log "[inflightOpTimeout] Not connected, retrying in 0.5s"
-						timer = setTimeout retryOp, @WAIT_FOR_CONNECTION_TIMEOUT
-					else
-						sl_console.log "[inflightOpTimeout] Sending"
-						@connection.send(update)
-			
-			timer = setTimeout retryOp, @INFLIGHT_OP_TIMEOUT
-			@_doc.inflightCallbacks.push () =>
-				@_clearFatalTimeoutTimer()
-				clearTimeout timer
-
-		FATAL_OP_TIMEOUT: 30000 # 30 seconds
-		_startFatalTimeoutTimer: (update) ->
-			# If an op doesn't get acked within FATAL_OP_TIMEOUT, something has
-			# gone unrecoverably wrong (the op will have been retried multiple times)
-			return if @_timeoutTimer?
-			@_timeoutTimer = setTimeout () =>
-				@_clearFatalTimeoutTimer()
-				@trigger "op:timeout", update
-			, @FATAL_OP_TIMEOUT
-		
-		_clearFatalTimeoutTimer: () ->
-			return if !@_timeoutTimer?
-			clearTimeout @_timeoutTimer
-			@_timeoutTimer = null
-
-		_handleError: (error, meta = {}) ->
-			@trigger "error", error, meta
-
-		_bindToDocChanges: (doc) ->
-			submitOp = doc.submitOp
-			doc.submitOp = (args...) =>
-				@trigger "op:sent", args...
-				doc.pendingCallbacks.push () =>
-					@trigger "op:acknowledged", args...
-				submitOp.apply(doc, args)
-
-			flush = doc.flush
-			doc.flush = (args...) =>
-				@trigger "flush", doc.inflightOp, doc.pendingOp, doc.version
-				flush.apply(doc, args)

+ 0 - 35
services/web/public/coffee/ide/editor/components/spellMenu.coffee

@@ -1,35 +0,0 @@
-define ["base"], (App) ->
-  App.component "spellMenu", {
-    bindings: {
-      open: "<"
-      top: "<"
-      left: "<"
-      layoutFromBottom: "<"
-      highlight: "<"
-      replaceWord: "&"
-      learnWord: "&"
-    }
-    template: """
-  <div
-    class="dropdown context-menu spell-check-menu"
-    ng-show="$ctrl.open"
-    ng-style="{top: $ctrl.top, left: $ctrl.left}"
-    ng-class="{open: $ctrl.open, 'spell-check-menu-from-bottom': $ctrl.layoutFromBottom}"
-  >
-    <ul class="dropdown-menu">
-      <li ng-repeat="suggestion in $ctrl.highlight.suggestions | limitTo:8">
-        <a
-          href
-          ng-click="$ctrl.replaceWord({ highlight: $ctrl.highlight, suggestion: suggestion })"
-        >
-          {{ suggestion }}
-        </a>
-      </li>
-      <li class="divider"></li>
-      <li>
-        <a href ng-click="$ctrl.learnWord({ highlight: $ctrl.highlight })">Add to Dictionary</a>
-      </li>
-    </ul>
-  </div>
-    """
-  }

+ 0 - 62
services/web/public/coffee/ide/editor/controllers/SavingNotificationController.coffee

@@ -1,62 +0,0 @@
-define [
-	"base"
-	"ide/editor/Document"
-], (App, Document) ->
-	App.controller "SavingNotificationController", ["$scope", "$interval", "ide", ($scope, $interval, ide) ->
-		setInterval () ->
-			pollSavedStatus()
-		, 1000
-
-		$(window).bind 'beforeunload', () =>
-			warnAboutUnsavedChanges()
-
-		lockEditorModal = null # modal showing "connection lost"
-		MAX_UNSAVED_SECONDS = 15 # lock the editor after this time if unsaved
-
-		$scope.docSavingStatus = {}
-		pollSavedStatus = () ->
-			oldStatus = $scope.docSavingStatus
-			oldUnsavedCount = $scope.docSavingStatusCount
-			newStatus = {}
-			newUnsavedCount = 0
-			maxUnsavedSeconds = 0
-
-			for doc_id, doc of Document.openDocs
-				saving = doc.pollSavedStatus()
-				if !saving
-					newUnsavedCount++
-					if oldStatus[doc_id]?
-						newStatus[doc_id] = oldStatus[doc_id]
-						t = newStatus[doc_id].unsavedSeconds += 1
-						if t > maxUnsavedSeconds
-							maxUnsavedSeconds = t
-					else
-						newStatus[doc_id] = {
-							unsavedSeconds: 0
-							doc: ide.fileTreeManager.findEntityById(doc_id)
-						}
-
-			if newUnsavedCount > 0 and t > MAX_UNSAVED_SECONDS and not lockEditorModal
-				lockEditorModal = ide.showLockEditorMessageModal(
-						"Connection lost"
-						"Sorry, the connection to the server is down."
-					)
-				lockEditorModal.result.finally () ->
-					lockEditorModal = null # unset the modal if connection comes back
-
-			if lockEditorModal and newUnsavedCount is 0
-				lockEditorModal.dismiss "connection back up"
-
-			# for performance, only update the display if the old or new
-			# counts of unsaved files are nonzeror.  If both old and new
-			# unsaved counts are zero then we know we are in a good state
-			# and don't need to do anything to the UI.
-			if newUnsavedCount or oldUnsavedCount
-				$scope.docSavingStatus = newStatus
-				$scope.docSavingStatusCount = newUnsavedCount
-				$scope.$apply()
-
-		warnAboutUnsavedChanges = () ->
-			if Document.hasUnsavedChanges()
-				return "You have unsaved changes. If you leave now they will not be saved."
-	]

+ 0 - 633
services/web/public/coffee/ide/editor/directives/aceEditor.coffee

@@ -1,633 +0,0 @@
-define [
-	"base"
-	"ace/ace"
-	"ace/ext-searchbox"
-	"ace/ext-modelist"
-	"ace/keybinding-vim"
-	"ide/editor/directives/aceEditor/undo/UndoManager"
-	"ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager"
-	"ide/editor/directives/aceEditor/spell-check/SpellCheckManager"
-	"ide/editor/directives/aceEditor/spell-check/SpellCheckAdapter"
-	"ide/editor/directives/aceEditor/highlights/HighlightsManager"
-	"ide/editor/directives/aceEditor/cursor-position/CursorPositionManager"
-	"ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter"
-	"ide/editor/directives/aceEditor/track-changes/TrackChangesManager"
-	"ide/editor/directives/aceEditor/metadata/MetadataManager"
-	"ide/metadata/services/metadata"
-	"ide/graphics/services/graphics"
-	"ide/preamble/services/preamble"
-	"ide/files/services/files"
-], (App, Ace, SearchBox, Vim, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, SpellCheckAdapter, HighlightsManager, CursorPositionManager, CursorPositionAdapter, TrackChangesManager, MetadataManager) ->
-	EditSession = ace.require('ace/edit_session').EditSession
-	ModeList = ace.require('ace/ext/modelist')
-	Vim = ace.require('ace/keyboard/vim').Vim
-
-	# set the path for ace workers if using a CDN (from editor.pug)
-	if window.aceWorkerPath != ""
-		syntaxValidationEnabled = true
-		ace.config.set('workerPath', "#{window.aceWorkerPath}")
-	else
-		syntaxValidationEnabled = false
-
-	# By default, don't use workers - enable them per-session as required
-	ace.config.setDefaultValue("session", "useWorker", false)
-
-	# Ace loads its script itself, so we need to hook in to be able to clear
-	# the cache.
-	if !ace.config._moduleUrl?
-		ace.config._moduleUrl = ace.config.moduleUrl
-		ace.config.moduleUrl = (args...) ->
-			url = ace.config._moduleUrl(args...)
-			return url
-
-	App.directive "aceEditor", ($timeout, $compile, $rootScope, event_tracking, localStorage, $cacheFactory, metadata, graphics, preamble, files, $http, $q, $window) ->
-		monkeyPatchSearch($rootScope, $compile)
-
-		return  {
-			scope: {
-				theme: "="
-				showPrintMargin: "="
-				keybindings: "="
-				fontSize: "="
-				autoComplete: "="
-				autoPairDelimiters: "="
-				sharejsDoc: "="
-				spellCheck: "="
-				spellCheckLanguage: "="
-				highlights: "="
-				text: "="
-				readOnly: "="
-				annotations: "="
-				navigateHighlights: "="
-				fileName: "="
-				onCtrlEnter: "="   # Compile
-				onCtrlJ: "="       # Toggle the review panel
-				onCtrlShiftC: "="  # Add a new comment
-				onCtrlShiftA: "="  # Toggle track-changes on/off
-				onSave: "="        # Cmd/Ctrl-S or :w in Vim
-				syntaxValidation: "="
-				reviewPanel: "="
-				eventsBridge: "="
-				trackChanges: "="
-				trackChangesEnabled: "="
-				docId: "="
-				rendererData: "="
-				lineHeight: "="
-				fontFamily: "="
-			}
-			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])
-				editor.$blockScrolling = Infinity
-
-				# auto-insertion of braces, brackets, dollars
-				editor.setOption('behavioursEnabled', scope.autoPairDelimiters || false)
-				editor.setOption('wrapBehavioursEnabled', false)
-
-				scope.$watch "autoPairDelimiters", (autoPairDelimiters) =>
-					if autoPairDelimiters
-						editor.setOption('behavioursEnabled', true)
-					else
-						editor.setOption('behavioursEnabled', false)
-
-				window._debug_editors ||= []
-				window._debug_editors.push editor
-
-				scope.name = attrs.aceEditor
-
-				if scope.spellCheck # only enable spellcheck when explicitly required
-					spellCheckCache = $cacheFactory.get("spellCheck-#{scope.name}") || $cacheFactory("spellCheck-#{scope.name}", {capacity: 1000})
-					spellCheckManager = new SpellCheckManager(scope, spellCheckCache, $http, $q, new SpellCheckAdapter(editor))
-
-				undoManager           = new UndoManager(scope, editor, element)
-				highlightsManager     = new HighlightsManager(scope, editor, element)
-				cursorPositionManager = new CursorPositionManager(scope, new CursorPositionAdapter(editor), localStorage)
-				trackChangesManager   = new TrackChangesManager(scope, editor, element)
-				metadataManager = new MetadataManager(scope, editor, element, metadata)
-				autoCompleteManager = new AutoCompleteManager(scope, editor, element, metadataManager, graphics, preamble, files)
-
-				scope.$watch "onSave", (callback) ->
-					if callback?
-						Vim.defineEx 'write', 'w', callback
-						editor.commands.addCommand
-							name: "save",
-							bindKey: win: "Ctrl-S", mac: "Command-S"
-							exec: callback
-							readOnly: true
-						# Not technically 'save', but Ctrl-. recompiles in OL v1
-						# so maintain compatibility
-						editor.commands.addCommand
-							name: "recompile_v1",
-							bindKey: win: "Ctrl-.", mac: "Ctrl-."
-							exec: callback
-							readOnly: true
-				editor.commands.removeCommand "transposeletters"
-				editor.commands.removeCommand "showSettingsMenu"
-				editor.commands.removeCommand "foldall"
-
-
-				# For European keyboards, the / is above 7 so needs Shift pressing.
-				# This comes through as Command-Shift-/ on OS X, which is mapped to
-				# toggleBlockComment.
-				# This doesn't do anything for LaTeX, so remap this to togglecomment to
-				# work for European keyboards as normal.
-				# On Windows, the key combo comes as Ctrl-Shift-7.
-				editor.commands.removeCommand "toggleBlockComment"
-				editor.commands.removeCommand "togglecomment"
-
-				editor.commands.addCommand {
-					name: "togglecomment",
-					bindKey: { win: "Ctrl-/|Ctrl-Shift-7", mac: "Command-/|Command-Shift-/" },
-					exec: (editor) -> editor.toggleCommentLines(),
-					multiSelectAction: "forEachLine",
-					scrollIntoView: "selectionPart"
-				}
-
-				# Trigger search AND replace on CMD+F
-				editor.commands.addCommand
-					name: "find",
-					bindKey: win: "Ctrl-F", mac: "Command-F"
-					exec: (editor) ->
-						ace.require("ace/ext/searchbox").Search(editor, true)
-					readOnly: true
-
-				# Bold text on CMD+B
-				editor.commands.addCommand
-					name: "bold",
-					bindKey: win: "Ctrl-B", mac: "Command-B"
-					exec: (editor) ->
-						selection = editor.getSelection()
-						if selection.isEmpty()
-							editor.insert("\\textbf{}")
-							editor.navigateLeft(1)
-						else
-							text = editor.getCopyText()
-							editor.insert("\\textbf{" + text + "}")
-					readOnly: false
-
-				# Italicise text on CMD+I
-				editor.commands.addCommand
-					name: "italics",
-					bindKey: win: "Ctrl-I", mac: "Command-I"
-					exec: (editor) ->
-						selection = editor.getSelection()
-						if selection.isEmpty()
-							editor.insert("\\textit{}")
-							editor.navigateLeft(1)
-						else
-							text = editor.getCopyText()
-							editor.insert("\\textit{" + text + "}")
-					readOnly: false
-
-				scope.$watch "onCtrlEnter", (callback) ->
-					if callback?
-						editor.commands.addCommand
-							name: "compile",
-							bindKey: win: "Ctrl-Enter", mac: "Command-Enter"
-							exec: (editor) =>
-								callback()
-							readOnly: true
-
-				scope.$watch "onCtrlJ", (callback) ->
-					if callback?
-						editor.commands.addCommand
-							name: "toggle-review-panel",
-							bindKey: win: "Ctrl-J", mac: "Command-J"
-							exec: (editor) =>
-								callback()
-							readOnly: true
-
-				scope.$watch "onCtrlShiftC", (callback) ->
-					if callback?
-						editor.commands.addCommand
-							name: "add-new-comment",
-							bindKey: win: "Ctrl-Shift-C", mac: "Command-Shift-C"
-							exec: (editor) =>
-								callback()
-							readOnly: true
-
-				scope.$watch "onCtrlShiftA", (callback) ->
-					if callback?
-						editor.commands.addCommand
-							name: "toggle-track-changes",
-							bindKey: win: "Ctrl-Shift-A", mac: "Command-Shift-A"
-							exec: (editor) =>
-								callback()
-							readOnly: true
-
-				# Make '/' work for search in vim mode.
-				editor.showCommandLine = (arg) =>
-					if arg == "/"
-						ace.require("ace/ext/searchbox").Search(editor, true)
-
-				getCursorScreenPosition = () ->
-					session = editor.getSession()
-					cursorPosition = session.selection.getCursor()
-					sessionPos = session.documentToScreenPosition(cursorPosition.row, cursorPosition.column)
-					screenPos = editor.renderer.textToScreenCoordinates(sessionPos.row, sessionPos.column)
-					return sessionPos.row * editor.renderer.lineHeight - session.getScrollTop()
-
-				if attrs.resizeOn?
-					for event in attrs.resizeOn.split(",")
-						scope.$on event, () ->
-							previousScreenPosition = getCursorScreenPosition()
-							editor.resize()
-							# Put cursor back to same vertical position on screen
-							newScreenPosition = getCursorScreenPosition()
-							session = editor.getSession()
-							session.setScrollTop(session.getScrollTop() + newScreenPosition - previousScreenPosition)
-
-				scope.$on "#{scope.name}:set-scroll-size", (e, size) ->
-					# Make sure that the editor has enough scroll margin above and below
-					# to scroll the review panel with the given size
-					marginTop = size.overflowTop
-					maxHeight = editor.renderer.layerConfig.maxHeight
-					marginBottom = Math.max(size.height - maxHeight, 0)
-					setScrollMargins(marginTop, marginBottom)
-
-				setScrollMargins = (marginTop, marginBottom) ->
-					marginChanged = false
-					if editor.renderer.scrollMargin.top != marginTop
-						editor.renderer.scrollMargin.top = marginTop
-						marginChanged = true
-					if editor.renderer.scrollMargin.bottom != marginBottom
-						editor.renderer.scrollMargin.bottom = marginBottom
-						marginChanged = true
-					if marginChanged
-						editor.renderer.updateFull()
-
-				resetScrollMargins = () ->
-					setScrollMargins(0,0)
-
-				scope.$watch "theme", (value) ->
-					editor.setTheme("ace/theme/#{value}")
-
-				scope.$watch "showPrintMargin", (value) ->
-					editor.setShowPrintMargin(value)
-
-				scope.$watch "keybindings", (value) ->
-					if value in ["vim", "emacs"]
-						editor.setKeyboardHandler("ace/keyboard/#{value}")
-					else
-						editor.setKeyboardHandler(null)
-
-				scope.$watch "fontSize", (value) ->
-					element.find(".ace_editor, .ace_content").css({
-						"font-size": value + "px"
-					})
-
-				scope.$watch "fontFamily", (value) ->
-					if value?
-						switch value
-							when 'monaco'
-								editor.setOption('fontFamily', '"Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro", monospace')
-							when 'lucida'
-								editor.setOption('fontFamily', '"Lucida Console", monospace')
-							else
-								editor.setOption('fontFamily', null)
-
-				scope.$watch "lineHeight", (value) ->
-					if value?
-						switch value
-							when 'compact'
-								editor.container.style.lineHeight = 1.33
-							when 'normal'
-								editor.container.style.lineHeight = 1.6
-							when 'wide'
-								editor.container.style.lineHeight = 2
-							else
-								editor.container.style.lineHeight = 1.6
-						editor.renderer.updateFontSize()
-
-				scope.$watch "sharejsDoc", (sharejs_doc, old_sharejs_doc) ->
-					if old_sharejs_doc?
-						scope.$broadcast('beforeChangeDocument')
-						detachFromAce(old_sharejs_doc)
-					if sharejs_doc?
-						attachToAce(sharejs_doc)
-					if sharejs_doc? and old_sharejs_doc?
-						scope.$broadcast('afterChangeDocument')
-
-				scope.$watch "text", (text) ->
-					if text?
-						editor.setValue(text, -1)
-						session = editor.getSession()
-						session.setUseWrapMode(true)
-
-				scope.$watch "annotations", (annotations) ->
-					session = editor.getSession()
-					session.setAnnotations annotations
-
-				scope.$watch "readOnly", (value) ->
-					editor.setReadOnly !!value
-
-				scope.$watch "syntaxValidation", (value) ->
-					# ignore undefined settings here
-					# only instances of ace with an explicit value should set useWorker
-					# the history instance will have syntaxValidation undefined
-					if value? and syntaxValidationEnabled
-						session = editor.getSession()
-						session.setOption("useWorker", value);
-
-				editor.setOption("scrollPastEnd", true)
-
-				updateCount = 0
-				onChange = () ->
-					updateCount++
-
-					if updateCount == 100
-						event_tracking.send 'editor-interaction', 'multi-doc-update'
-					scope.$emit "#{scope.name}:change"
-
-				onScroll = (scrollTop) ->
-					return if !scope.eventsBridge?
-					height = editor.renderer.layerConfig.maxHeight
-					scope.eventsBridge.emit "aceScroll", scrollTop, height
-
-				onScrollbarVisibilityChanged = (event, vRenderer) ->
-					return if !scope.eventsBridge?
-					scope.eventsBridge.emit "aceScrollbarVisibilityChanged", vRenderer.scrollBarV.isVisible, vRenderer.scrollBarV.width
-
-				if scope.eventsBridge?
-					editor.renderer.on "scrollbarVisibilityChanged", onScrollbarVisibilityChanged
-
-					scope.eventsBridge.on "externalScroll", (position) ->
-						editor.getSession().setScrollTop(position)
-					scope.eventsBridge.on "refreshScrollPosition", () ->
-						session = editor.getSession()
-						session.setScrollTop(session.getScrollTop() + 1)
-						session.setScrollTop(session.getScrollTop() - 1)
-
-				onSessionChangeForSpellCheck = (e) ->
-					spellCheckManager.onSessionChange()
-					e.oldSession?.getDocument().off "change", spellCheckManager.onChange
-					e.session.getDocument().on "change", spellCheckManager.onChange
-					e.oldSession?.off "changeScrollTop", spellCheckManager.onScroll
-					e.session.on "changeScrollTop", spellCheckManager.onScroll
-
-				initSpellCheck = () ->
-					spellCheckManager.init()
-					editor.on 'changeSession', onSessionChangeForSpellCheck
-					onSessionChangeForSpellCheck({ session: editor.getSession() }) # Force initial setup
-					editor.on 'nativecontextmenu', spellCheckManager.onContextMenu
-
-				tearDownSpellCheck = () ->
-					editor.off 'changeSession', onSessionChangeForSpellCheck
-					editor.off 'nativecontextmenu', spellCheckManager.onContextMenu
-
-				onSessionChangeForCursorPosition = (e) ->
-					e.oldSession?.selection.off 'changeCursor', cursorPositionManager.onCursorChange
-					e.session.selection.on 'changeCursor', cursorPositionManager.onCursorChange
-
-				onUnloadForCursorPosition = () ->
-					cursorPositionManager.onUnload(editor.getSession())
-
-				initCursorPosition = () ->
-					editor.on 'changeSession', onSessionChangeForCursorPosition
-					onSessionChangeForCursorPosition({ session: editor.getSession() }) # Force initial setup
-					$(window).on "unload", onUnloadForCursorPosition
-
-				tearDownCursorPosition = () ->
-					editor.off 'changeSession', onSessionChangeForCursorPosition
-					$(window).off "unload", onUnloadForCursorPosition
-
-				initCursorPosition()
-
-				# Trigger the event once *only* - this is called after Ace is connected
-				# to the ShareJs instance but this event should only be triggered the
-				# first time the editor is opened. Not every time the docs opened
-				triggerEditorInitEvent = _.once () ->
-					scope.$broadcast('editorInit')
-
-				attachToAce = (sharejs_doc) ->
-					lines = sharejs_doc.getSnapshot().split("\n")
-					session = editor.getSession()
-					if session?
-						session.destroy()
-
-					# see if we can lookup a suitable mode from ace
-					# but fall back to text by default
-					try
-						if /\.(Rtex|bbl)$/i.test(scope.fileName)
-							# recognise Rtex and bbl as latex
-							mode = "ace/mode/latex"
-						else if /\.(sty|cls|clo)$/.test(scope.fileName)
-							# recognise some common files as tex
-							mode = "ace/mode/tex"
-						else
-							mode = ModeList.getModeForPath(scope.fileName).mode
-							# we prefer plain_text mode over text mode because ace's
-							# text mode is actually for code and has unwanted
-							# indenting (see wrapMethod in ace edit_session.js)
-							if mode is "ace/mode/text"
-								mode = "ace/mode/plain_text"
-					catch
-						mode = "ace/mode/plain_text"
-
-					# create our new session
-					session = new EditSession(lines, mode)
-
-					session.setUseWrapMode(true)
-					# use syntax validation only when explicitly set
-					if scope.syntaxValidation? and syntaxValidationEnabled and !/\.bib$/.test(scope.fileName)
-						session.setOption("useWorker", scope.syntaxValidation);
-
-					# now attach session to editor
-					editor.setReadOnly(true) # set to readonly until document change handlers are attached
-					editor.setSession(session)
-
-					doc = session.getDocument()
-					doc.on "change", onChange
-
-					editor.initing = true
-					sharejs_doc.attachToAce(editor)
-					editor.initing = false
-					# now ready to edit document
-					editor.setReadOnly(scope.readOnly) # respect the readOnly setting, normally false
-					triggerEditorInitEvent()
-					initSpellCheck()
-
-					resetScrollMargins()
-
-					# need to set annotations after attaching because attaching
-					# deletes and then inserts document content
-					session.setAnnotations scope.annotations
-
-
-					session.on "changeScrollTop", event_tracking.editingSessionHeartbeat
-
-					angular.element($window).on('click',
-						event_tracking.editingSessionHeartbeat)
-
-					scope.$on "$destroy", () ->
-						angular.element($window).off('click',
-							event_tracking.editingSessionHeartbeat)
-
-					if scope.eventsBridge?
-						session.on "changeScrollTop", onScroll
-
-					$rootScope.hasLintingError = false
-					session.on('changeAnnotation', () ->
-						# Both linter errors and compile logs are set as error annotations,
-						# however when the user types something, the compile logs are
-						# replaced with linter errors. When we check for lint errors before
-						# autocompile we are guaranteed to get linter errors
-						hasErrors = session
-							.getAnnotations()
-							.filter((annotation) -> annotation.type != 'info')
-							.length > 0
-
-						if ($rootScope.hasLintingError != hasErrors)
-							$rootScope.hasLintingError = hasErrors
-					)
-
-					setTimeout () ->
-						# Let any listeners init themselves
-						onScroll(editor.renderer.getScrollTop())
-
-					editor.focus()
-
-				detachFromAce = (sharejs_doc) ->
-					tearDownSpellCheck()
-					sharejs_doc.detachFromAce()
-					sharejs_doc.off "remoteop.recordRemote"
-
-					session = editor.getSession()
-					session.off "changeScrollTop"
-
-					doc = session.getDocument()
-					doc.off "change", onChange
-
-				editor.renderer.on "changeCharacterSize", () ->
-					scope.$apply () ->
-						scope.rendererData.lineHeight = editor.renderer.lineHeight
-
-				scope.$watch "rendererData", (rendererData) ->
-					if rendererData?
-						rendererData.lineHeight = editor.renderer.lineHeight
-
-				scope.$on '$destroy', () ->
-					if scope.sharejsDoc?
-						scope.$broadcast('changeEditor')
-						tearDownSpellCheck()
-						tearDownCursorPosition()
-						detachFromAce(scope.sharejsDoc)
-						session = editor.getSession()
-						session?.destroy()
-						scope.eventsBridge.emit "aceScrollbarVisibilityChanged", false, 0
-
-				scope.$emit "#{scope.name}:inited", editor
-
-			template: """
-				<div class="ace-editor-wrapper">
-					<div
-						class="undo-conflict-warning alert alert-danger small"
-						ng-show="undo.show_remote_warning"
-					>
-						<strong>Watch out!</strong>
-						We had to undo some of your collaborators changes before we could undo yours.
-						<a
-							href="#"
-							class="pull-right"
-							ng-click="undo.show_remote_warning = false"
-						>Dismiss</a>
-					</div>
-					<div class="ace-editor-body"></div>
-					<spell-menu
-						open="spellMenu.open"
-						top="spellMenu.top"
-						left="spellMenu.left"
-						layout-from-bottom="spellMenu.layoutFromBottom"
-						highlight="spellMenu.highlight"
-						replace-word="replaceWord(highlight, suggestion)"
-						learn-word="learnWord(highlight)"
-					></spell-menu>
-					<div
-						class="annotation-label"
-						ng-show="annotationLabel.show"
-						ng-style="{
-							position: 'absolute',
-							left:     annotationLabel.left,
-							right:    annotationLabel.right,
-							bottom:   annotationLabel.bottom,
-							top:      annotationLabel.top,
-							'background-color': annotationLabel.backgroundColor
-						}"
-					>
-						{{ annotationLabel.text }}
-					</div>
-
-					<a
-						href
-						class="highlights-before-label btn btn-info btn-xs"
-						ng-show="updateLabels.highlightsBefore > 0"
-						ng-click="gotoHighlightAbove()"
-					>
-						<i class="fa fa-fw fa-arrow-up"></i>
-						{{ updateLabels.highlightsBefore }} more update{{ updateLabels.highlightsBefore > 1 && "" || "s" }} above
-					</a>
-
-					<a
-						href
-						class="highlights-after-label btn btn-info btn-xs"
-						ng-show="updateLabels.highlightsAfter > 0"
-						ng-click="gotoHighlightBelow()"
-					>
-						<i class="fa fa-fw fa-arrow-down"></i>
-						{{ updateLabels.highlightsAfter }} more update{{ updateLabels.highlightsAfter > 1 && "" || "s" }} below
-
-					</a>
-				</div>
-			"""
-		}
-
-	monkeyPatchSearch = ($rootScope, $compile) ->
-		SearchBox = ace.require("ace/ext/searchbox").SearchBox
-		searchHtml = """
-			<div class="ace_search right">
-				<a href type="button" action="hide" class="ace_searchbtn_close">
-					<i class="fa fa-fw fa-times"></i>
-				</a>
-				<div class="ace_search_form">
-					<input class="ace_search_field form-control input-sm" placeholder="Search for" spellcheck="false"></input>
-					<div class="btn-group">
-						<button type="button" action="findNext" class="ace_searchbtn next btn btn-default btn-sm">
-							<i class="fa fa-chevron-down fa-fw"></i>
-						</button>
-						<button type="button" action="findPrev" class="ace_searchbtn prev btn btn-default btn-sm">
-							<i class="fa fa-chevron-up fa-fw"></i>
-						</button>
-					</div>
-				</div>
-				<div class="ace_replace_form">
-					<input class="ace_search_field form-control input-sm" placeholder="Replace with" spellcheck="false"></input>
-					<div class="btn-group">
-						<button type="button" action="replaceAndFindNext" class="ace_replacebtn btn btn-default btn-sm">Replace</button>
-						<button type="button" action="replaceAll" class="ace_replacebtn btn btn-default btn-sm">All</button>
-					</div>
-				</div>
-				<div class="ace_search_options">
-					<div class="btn-group">
-						<span action="toggleRegexpMode" class="btn btn-default btn-sm" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip="RegExp Search">.*</span>
-						<span action="toggleCaseSensitive" class="btn btn-default btn-sm" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip="CaseSensitive Search">Aa</span>
-						<span action="toggleWholeWords" class="btn btn-default btn-sm" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip="Whole Word Search">"..."</span>
-					</div>
-				</div>
-			</div>
-		"""
-
-		# Remove Ace CSS
-		$("#ace_searchbox").remove()
-
-		$init = SearchBox::$init
-		SearchBox::$init = () ->
-			@element = $compile(searchHtml)($rootScope.$new())[0];
-			$init.apply(@)

+ 0 - 320
services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/AutoCompleteManager.coffee

@@ -1,320 +0,0 @@
-define [
-	"ide/editor/directives/aceEditor/auto-complete/CommandManager"
-	"ide/editor/directives/aceEditor/auto-complete/EnvironmentManager"
-	"ide/editor/directives/aceEditor/auto-complete/PackageManager"
-	"ide/editor/directives/aceEditor/auto-complete/Helpers"
-	"ace/ace"
-	"ace/ext-language_tools"
-], (CommandManager, EnvironmentManager, PackageManager, Helpers) ->
-	Range = ace.require("ace/range").Range
-	aceSnippetManager = ace.require('ace/snippets').snippetManager
-
-	class AutoCompleteManager
-		constructor: (@$scope, @editor, @element, @metadataManager, @graphics, @preamble, @files) ->
-
-			@monkeyPatchAutocomplete()
-
-			@$scope.$watch "autoComplete", (autocomplete) =>
-				if autocomplete
-					@enable()
-				else
-					@disable()
-
-			onChange = (change) =>
-				@onChange(change)
-
-			@editor.on "changeSession", (e) =>
-				e.oldSession.off "change", onChange
-				e.session.on "change", onChange
-
-		enable: () ->
-			@editor.setOptions({
-				enableBasicAutocompletion: true
-				enableSnippets: true
-				enableLiveAutocompletion: false
-			})
-
-			CommandCompleter = new CommandManager(@metadataManager)
-			SnippetCompleter = new EnvironmentManager()
-			PackageCompleter = new PackageManager(@metadataManager, Helpers)
-
-			Graphics = @graphics
-			Preamble = @preamble
-			Files = @files
-
-			GraphicsCompleter =
-				getCompletions: (editor, session, pos, prefix, callback) ->
-					{commandFragment} = Helpers.getContext(editor, pos)
-					if commandFragment
-						match = commandFragment.match(/^~?\\(includegraphics(?:\[.*])?){([^}]*, *)?(\w*)/)
-						if match
-							[_, commandName, _, currentArg] = match
-							graphicsPaths = Preamble.getGraphicsPaths()
-							result = []
-							for graphic in Graphics.getGraphicsFiles()
-								path = graphic.path
-								for graphicsPath in graphicsPaths
-									if path.indexOf(graphicsPath) == 0
-										path = path.slice(graphicsPath.length)
-										break
-								result.push {
-									caption: "\\#{commandName}{#{path}}"
-									value: "\\#{commandName}{#{path}}"
-									meta: "graphic"
-									score: 50
-								}
-							callback null, result
-
-			metadataManager = @metadataManager
-			FilesCompleter =
-				getCompletions: (editor, session, pos, prefix, callback) =>
-					{commandFragment} = Helpers.getContext(editor, pos)
-					if commandFragment
-						match = commandFragment.match(/^\\(input|include){(\w*)/)
-						if match
-							[_, commandName, currentArg] = match
-							result = []
-							for file in Files.getTeXFiles()
-								if file.id != @$scope.docId
-									path = file.path
-									result.push {
-										caption: "\\#{commandName}{#{path}}"
-										value: "\\#{commandName}{#{path}}"
-										meta: "file"
-										score: 50
-									}
-							callback null, result
-
-			LabelsCompleter =
-				getCompletions: (editor, session, pos, prefix, callback) ->
-					{commandFragment} = Helpers.getContext(editor, pos)
-					if commandFragment
-						refMatch = commandFragment.match(/^~?\\([a-zA-Z]*ref){([^}]*, *)?(\w*)/)
-						if refMatch
-							[_, commandName, currentArg] = refMatch
-							result = []
-							if commandName != 'ref' # ref is in top 100 commands
-								result.push {
-									caption: "\\#{commandName}{}"
-									snippet: "\\#{commandName}{}"
-									meta: "cross-reference"
-									score: 60
-								}
-							for label in metadataManager.getAllLabels()
-								result.push {
-									caption: "\\#{commandName}{#{label}}"
-									value: "\\#{commandName}{#{label}}"
-									meta: "cross-reference"
-									score: 50
-								}
-							callback null, result
-
-			references = @$scope.$root._references
-			ReferencesCompleter =
-				getCompletions: (editor, session, pos, prefix, callback) ->
-					{commandFragment} = Helpers.getContext(editor, pos)
-					if commandFragment
-						citeMatch = commandFragment.match(
-							/^~?\\([a-z]*cite[a-z]*(?:\[.*])?){([^}]*, *)?(\w*)/
-						)
-						if citeMatch
-							[_, commandName, previousArgs, currentArg] = citeMatch
-							if !previousArgs?
-								previousArgs = ""
-							previousArgsCaption = if previousArgs.length > 8 then "…," else previousArgs
-							result = []
-							result.push {
-								caption: "\\#{commandName}{}"
-								snippet: "\\#{commandName}{}"
-								meta: "reference"
-								score: 60
-							}
-							if references.keys and references.keys.length > 0
-								references.keys.forEach (key) ->
-									if key?
-										result.push({
-											caption: "\\#{commandName}{#{previousArgsCaption}#{key}}"
-											value: "\\#{commandName}{#{previousArgs}#{key}}"
-											meta: "reference"
-											score: 50
-										})
-								callback null, result
-							else
-								callback null, result
-
-			@editor.completers = [
-				CommandCompleter
-				SnippetCompleter
-				PackageCompleter
-				ReferencesCompleter
-				LabelsCompleter
-				GraphicsCompleter
-				FilesCompleter
-			]
-
-		disable: () ->
-			@editor.setOptions({
-				enableBasicAutocompletion: false,
-				enableSnippets: false
-			})
-
-		onChange: (change) ->
-			cursorPosition = @editor.getCursorPosition()
-			end = change.end
-			{lineUpToCursor, commandFragment} = Helpers.getContext(@editor, end)
-			if ((i = lineUpToCursor.indexOf('%')) > -1 and lineUpToCursor[i-1] != '\\')
-				return
-			lastCharIsBackslash = lineUpToCursor.slice(-1) == "\\"
-			lastTwoChars = lineUpToCursor.slice(-2)
-			# Don't offer autocomplete on double-backslash, backslash-colon, etc
-			if /^\\[^a-zA-Z]$/.test(lastTwoChars)
-				@editor?.completer?.detach?()
-				return
-			# Check that this change was made by us, not a collaborator
-			# (Cursor is still one place behind)
-			# NOTE: this is also the case when a user backspaces over a highlighted region
-			if (
-				change.action == "insert" and
-				end.row == cursorPosition.row and
-				end.column == cursorPosition.column + 1
-			)
-				if commandFragment?.length > 2 or lastCharIsBackslash
-					setTimeout () =>
-						@editor.execCommand("startAutocomplete")
-					, 0
-			if (
-				change.action == "insert" and
-				/(begin|end|[a-zA-Z]*ref|usepackage|[a-z]*cite[a-z]*|input|include)/.test(
-					change.lines[0].match(/\\(\w+){}/)?[1]
-				)
-			)
-				setTimeout () =>
-					@editor.execCommand("startAutocomplete")
-				, 0
-
-		monkeyPatchAutocomplete: () ->
-			Autocomplete = ace.require("ace/autocomplete").Autocomplete
-			Util = ace.require("ace/autocomplete/util")
-			editor = @editor
-
-			if !Autocomplete::_insertMatch?
-				# Only override this once since it's global but we may create multiple
-				# autocomplete handlers
-				Autocomplete::_insertMatch = Autocomplete::insertMatch
-				Autocomplete::insertMatch = (data) ->
-					pos = editor.getCursorPosition()
-					range = new Range(pos.row, pos.column, pos.row, pos.column + 1)
-					nextChar = editor.session.getTextRange(range)
-
-					# If we are in \begin{it|}, then we need to remove the trailing }
-					# since it will be adding in with the autocomplete of \begin{item}...
-					if /^\\\w+{/.test(this.completions.filterText) and nextChar == "}"
-						editor.session.remove(range)
-
-					# Provide our own `insertMatch` implementation.
-					# See the `insertMatch` method of Autocomplete in `ext-language_tools.js`.
-					# We need this to account for editing existing commands, particularly when
-					# adding a prefix.
-					# We fix this by detecting when the cursor is in the middle of an existing
-					# command, and adjusting the insertions/deletions accordingly.
-					# Example:
-					#   when changing `\ref{}` to `\href{}`, ace default behaviour
-					#   is likely to end up with `\href{}ref{}`
-					if !data?
-						completions = this.completions
-						popup = this.popup
-						data = popup.getData(popup.getRow())
-						data.completer =
-							insertMatch: (editor, matchData) ->
-								for range in editor.selection.getAllRanges()
-									leftRange = _.clone(range)
-									rightRange = _.clone(range)
-									# trim to left of cursor
-									lineUpToCursor = editor.getSession().getTextRange(
-										new Range(
-											range.start.row,
-											0,
-											range.start.row,
-											range.start.column,
-										)
-									)
-									# Delete back to command start, as appropriate
-									commandStartIndex = Helpers.getLastCommandFragmentIndex(lineUpToCursor)
-									if commandStartIndex != -1
-										leftRange.start.column = commandStartIndex
-									else
-										leftRange.start.column -= completions.filterText.length
-									editor.session.remove(leftRange)
-									# look at text after cursor
-									lineBeyondCursor = editor.getSession().getTextRange(
-										new Range(
-											rightRange.start.row,
-											rightRange.start.column,
-											rightRange.end.row,
-											99999
-										)
-									)
-
-									if lineBeyondCursor
-										if partialCommandMatch = lineBeyondCursor.match(/^([a-zA-Z0-9]+)\{/)
-											# We've got a partial command after the cursor
-											commandTail = partialCommandMatch[1]
-											# remove rest of the partial command, right of cursor
-											rightRange.end.column += commandTail.length - completions.filterText.length
-											editor.session.remove(rightRange);
-											# trim the completion text to just the command, without braces or brackets
-											# example: '\cite{}' -> '\cite'
-											if matchData.snippet?
-												matchData.snippet = matchData.snippet.replace(/[{\[].*[}\]]/, '')
-											if matchData.caption?
-												matchData.caption = matchData.caption.replace(/[{\[].*[}\]]/, '')
-											if matchData.value?
-												matchData.value = matchData.value.replace(/[{\[].*[}\]]/, '')
-								# finally, insert the match
-								if matchData.snippet
-									aceSnippetManager.insertSnippet(editor, matchData.snippet);
-								else
-									editor.execCommand("insertstring", matchData.value || matchData);
-
-					Autocomplete::_insertMatch.call this, data
-
-				# Overwrite this to set autoInsert = false and set font size
-				Autocomplete.startCommand = {
-					name: "startAutocomplete",
-					exec: (editor) =>
-						if (!editor.completer)
-							editor.completer = new Autocomplete()
-						editor.completer.autoInsert = false
-						editor.completer.autoSelect = true
-						editor.completer.showPopup(editor)
-						editor.completer.cancelContextMenu()
-						container = $(editor.completer.popup?.container)
-						container.css({'font-size': @$scope.fontSize + 'px'})
-						# Dynamically set width of autocomplete popup
-						if filtered = editor?.completer?.completions?.filtered
-							longestCaption = _.max(filtered.map( (c) -> c.caption.length ))
-							longestMeta = _.max(filtered.map( (c) -> c.meta.length ))
-							charWidth = editor.renderer.characterWidth
-							# between 280 and 700 px
-							width = Math.max(
-								Math.min(
-									Math.round(longestCaption*charWidth + longestMeta*charWidth + 5*charWidth),
-									700
-								),
-								280
-							)
-							container.css({width: "#{width}px"})
-						if editor.completer?.completions?.filtered?.length == 0
-							editor.completer.detach()
-					bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space"
-				}
-
-			Util.retrievePrecedingIdentifier = (text, pos, regex) ->
-				currentLineOffset = 0
-				for i in [(pos-1)..0]
-					if text[i] == "\n"
-						currentLineOffset = i + 1
-						break
-				currentLine = text.slice(currentLineOffset, pos)
-				fragment = Helpers.getLastCommandFragment(currentLine) or ""
-				return fragment

+ 0 - 167
services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/CommandManager.coffee

@@ -1,167 +0,0 @@
-define [
-	"./snippets/TopHundredSnippets"
-], (topHundred) ->
-
-	class Parser
-		constructor: (@doc, @prefix) ->
-
-		parse: () ->
-			# Safari regex is super slow, freezes browser for minutes on end,
-			# hacky solution: limit iterations
-			limit = null
-			if window?._ide?.browserIsSafari
-				limit = 5000
-
-			# fully formed commands
-			realCommands = []
-			# commands which match the prefix exactly,
-			# and could be partially typed or malformed
-			incidentalCommands = []
-			seen = {}
-			iterations = 0
-			while command = @nextCommand()
-				iterations += 1
-				if limit && iterations > limit
-					return realCommands
-
-				docState = @doc
-
-				optionalArgs = 0
-				while @consumeArgument("[", "]")
-					optionalArgs++
-
-				args = 0
-				while @consumeArgument("{", "}")
-					args++
-
-				commandHash = "#{command}\\#{optionalArgs}\\#{args}"
-
-				if @prefix? && "\\#{command}" == @prefix
-					incidentalCommands.push [command, optionalArgs, args]
-				else
-					if !seen[commandHash]?
-						seen[commandHash] = true
-						realCommands.push [command, optionalArgs, args]
-
-				# Reset to before argument to handle nested commands
-				@doc = docState
-
-			# check incidentals, see if we should pluck out a match
-			if incidentalCommands.length > 1
-				bestMatch = incidentalCommands.sort((a, b) =>  a[1]+a[2] < b[1]+b[2])[0]
-				realCommands.push bestMatch
-
-			return realCommands
-
-		# Ignore single letter commands since auto complete is moot then.
-		commandRegex: /\\([a-zA-Z]{2,})/
-
-		nextCommand: () ->
-			i = @doc.search @commandRegex
-			if i == -1
-				return false
-			else
-				match = @doc.match(@commandRegex)[1]
-				@doc = @doc.substr(i + match.length + 1)
-				return match
-
-		consumeWhitespace: () ->
-			match = @doc.match(/^[ \t\n]*/m)[0]
-			@doc = @doc.substr(match.length)
-
-		consumeArgument: (openingBracket, closingBracket) ->
-			@consumeWhitespace()
-
-			if @doc[0] == openingBracket
-				i = 1
-				bracketParity = 1
-				while bracketParity > 0 and i < @doc.length
-					if @doc[i] == openingBracket
-						bracketParity++
-					else if @doc[i] == closingBracket
-						bracketParity--
-					i++
-
-				if bracketParity == 0
-					@doc = @doc.substr(i)
-					return true
-				else
-					return false
-			else
-				return false
-
-	class CommandManager
-		constructor: (@metadataManager) ->
-
-		getCompletions: (editor, session, pos, prefix, callback) ->
-			commandNames = {}
-			for snippet in topHundred
-				commandNames[snippet.caption.match(/\w+/)[0]] = true
-
-			packages = @metadataManager.getAllPackages()
-			packageCommands = []
-			for pkg, snippets of packages
-				for snippet in snippets
-					packageCommands.push snippet
-					commandNames[snippet.caption.match(/\w+/)[0]] = true
-
-			doc = session.getValue()
-			parser = new Parser(doc, prefix)
-			commands = parser.parse()
-			completions = []
-			for command in commands
-				if not commandNames[command[0]]
-					caption = "\\#{command[0]}"
-					score = if caption == prefix then 99 else 50
-					snippet = caption
-					i = 1
-					_.times command[1], () ->
-						snippet += "[${#{i}}]"
-						caption += "[]"
-						i++
-					_.times command[2], () ->
-						snippet += "{${#{i}}}"
-						caption += "{}"
-						i++
-					completions.push {
-						caption: caption
-						snippet: snippet
-						meta: "cmd"
-						score: score
-					}
-			completions = completions.concat topHundred, packageCommands
-
-			callback null, completions
-
-		loadCommandsFromDoc: (doc) ->
-			parser = new Parser(doc)
-			@commands = parser.parse()
-
-		getSuggestions: (commandFragment) ->
-			matchingCommands = _.filter @commands, (command) ->
-				command[0].slice(0, commandFragment.length) == commandFragment
-
-			return _.map matchingCommands, (command) ->
-				base = "\\" + commandFragment
-
-				args = ""
-				_.times command[1], () -> args = args + "[]"
-				_.times command[2], () -> args = args + "{}"
-				completionBase = command[0].slice(commandFragment.length)
-
-				squareArgsNo = command[1]
-				curlyArgsNo = command[2]
-				totalArgs = squareArgsNo + curlyArgsNo
-				if totalArgs == 0
-					completionBeforeCursor = completionBase
-					completionAfterCursor = ""
-				else
-					completionBeforeCursor = completionBase + args[0]
-					completionAfterCursor = args.slice(1)
-
-				return {
-					base: base
-					completion: completionBase + args
-					completionBeforeCursor: completionBeforeCursor
-					completionAfterCursor: completionAfterCursor
-				}

+ 0 - 194
services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/EnvironmentManager.coffee

@@ -1,194 +0,0 @@
-define [
-	'ide/editor/directives/aceEditor/auto-complete/snippets/Environments'
-], (Environments) ->
-	staticSnippets = for env in Environments.withoutSnippets
-		{
-			caption: "\\begin{#{env}}..."
-			snippet: """
-				\\begin{#{env}}
-				\t$1
-				\\end{#{env}}
-			"""
-			meta: "env"
-		}
-
-	staticSnippets = staticSnippets.concat [{
-		caption: "\\begin{array}..."
-		snippet: """
-			\\begin{array}{${1:cc}}
-			\t$2 & $3 \\\\\\\\
-			\t$4 & $5
-			\\end{array}
-		"""
-		meta: "env"
-	}, {
-		caption: "\\begin{figure}..."
-		snippet: """
-			\\begin{figure}
-			\t\\centering
-			\t\\includegraphics{$1}
-			\t\\caption{${2:Caption}}
-			\t\\label{${3:fig:my_label}}
-			\\end{figure}
-		"""
-		meta: "env"
-	}, {
-		caption: "\\begin{tabular}..."
-		snippet: """
-			\\begin{tabular}{${1:c|c}}
-			\t$2 & $3 \\\\\\\\
-			\t$4 & $5
-			\\end{tabular}
-		"""
-		meta: "env"
-	}, {
-		caption: "\\begin{table}..."
-		snippet: """
-			\\begin{table}[$1]
-			\t\\centering
-			\t\\begin{tabular}{${2:c|c}}
-			\t\t$3 & $4 \\\\\\\\
-			\t\t$5 & $6
-			\t\\end{tabular}
-			\t\\caption{${7:Caption}}
-			\t\\label{${8:tab:my_label}}
-			\\end{table}
-		"""
-		meta: "env"
-	}, {
-		caption: "\\begin{list}..."
-		snippet: """
-			\\begin{list}
-			\t\\item $1
-			\\end{list}
-		"""
-		meta: "env"
-	}, {
-		caption: "\\begin{enumerate}..."
-		snippet: """
-			\\begin{enumerate}
-			\t\\item $1
-			\\end{enumerate}
-		"""
-		meta: "env"
-	}, {
-		caption: "\\begin{itemize}..."
-		snippet: """
-			\\begin{itemize}
-			\t\\item $1
-			\\end{itemize}
-		"""
-		meta: "env"
-	}, {
-		caption: "\\begin{frame}..."
-		snippet: """
-			\\begin{frame}{${1:Frame Title}}
-			\t$2
-			\\end{frame}
-		"""
-		meta: "env"
-	}]
-
-	documentSnippet = {
-		caption: "\\begin{document}..."
-		snippet: """
-			\\begin{document}
-			$1
-			\\end{document}
-		"""
-		meta: "env"
-	}
-
-	bibliographySnippet = {
-		caption: "\\begin{thebibliography}..."
-		snippet: """
-			\\begin{thebibliography}{$1}
-			\\bibitem{$2}
-			$3
-			\\end{thebibliography}
-		"""
-		meta: "env"
-	}
-	staticSnippets.push(documentSnippet)
-
-	parseCustomEnvironments = (text) ->
-		re = /^\\newenvironment{(\w+)}.*$/gm
-		result = []
-		iterations = 0
-		while match = re.exec(text)
-			result.push {name: match[1], whitespace: null}
-			iterations += 1
-			if iterations >= 1000
-				return result
-		return result
-
-	parseBeginCommands = (text) ->
-		re = /^\\begin{(\w+)}.*\n([\t ]*).*$/gm
-		result = []
-		iterations = 0
-		while match = re.exec(text)
-			if match[1] not in Environments.all and match[1] != "document"
-				result.push {name: match[1], whitespace: match[2]}
-				iterations += 1
-				if iterations >= 1000
-					return result
-		return result
-
-	hasDocumentEnvironment = (text) ->
-		re = /^\\begin{document}/m
-		return re.exec(text) != null
-
-	hasBibliographyEnvironment = (text) ->
-		re = /^\\begin{thebibliography}/m
-		return re.exec(text) != null
-
-	class EnvironmentManager
-		getCompletions: (editor, session, pos, prefix, callback) ->
-			docText = session.getValue()
-			customEnvironments = parseCustomEnvironments(docText)
-			beginCommands = parseBeginCommands(docText)
-			if hasDocumentEnvironment(docText)
-				ind = staticSnippets.indexOf(documentSnippet)
-				if ind != -1
-					staticSnippets.splice(ind, 1)
-			else
-				staticSnippets.push documentSnippet
-
-			if hasBibliographyEnvironment(docText)
-				ind = staticSnippets.indexOf(bibliographySnippet)
-				if ind != -1
-					staticSnippets.splice(ind, 1)
-			else
-				staticSnippets.push bibliographySnippet
-
-			parsedItemsMap = {}
-			for environment in customEnvironments
-				parsedItemsMap[environment.name] = environment
-			for command in beginCommands
-				parsedItemsMap[command.name] = command
-			parsedItems = _.values(parsedItemsMap)
-			snippets = staticSnippets.concat(
-				parsedItems.map (item) ->
-					{
-						caption: "\\begin{#{item.name}}..."
-						snippet: """
-							\\begin{#{item.name}}
-							#{item.whitespace || ''}$0
-							\\end{#{item.name}}
-						"""
-						meta: "env"
-					}
-			).concat(
-				# arguably these `end` commands shouldn't be here, as they're not snippets
-				# but this is where we have access to the `begin` environment names
-				# *shrug*
-				parsedItems.map (item) ->
-					{
-						caption: "\\end{#{item.name}}"
-						value: "\\end{#{item.name}}"
-						meta: "env"
-					}
-			)
-			callback null, snippets
-
-	return EnvironmentManager

+ 0 - 43
services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/Helpers.coffee

@@ -1,43 +0,0 @@
-define [
-	"ace/ace"
-	"ace/ext-language_tools"
-], () ->
-	Range = ace.require("ace/range").Range
-
-	Helpers =
-		getLastCommandFragment: (lineUpToCursor) ->
-			if (index = Helpers.getLastCommandFragmentIndex(lineUpToCursor)) > -1
-				return lineUpToCursor.slice(index)
-			else
-				return null
-
-		getLastCommandFragmentIndex: (lineUpToCursor) ->
-			# This is hack to let us skip over commands in arguments, and
-			# go to the command on the same 'level' as us. E.g.
-			#    \includegraphics[width=\textwidth]{..
-			# should not match the \textwidth.
-			blankArguments = lineUpToCursor.replace /\[([^\]]*)\]/g, (args) ->
-				Array(args.length + 1).join('.')
-			if m = blankArguments.match(/(\\[^\\]*)$/)
-				return m.index
-			else
-				return -1
-
-		getCommandNameFromFragment: (commandFragment) ->
-			commandFragment?.match(/\\(\w+)\{/)?[1]
-
-		getContext: (editor, pos) ->
-			upToCursorRange = new Range(pos.row, 0, pos.row, pos.column)
-			lineUpToCursor = editor.getSession().getTextRange(upToCursorRange)
-			commandFragment = Helpers.getLastCommandFragment(lineUpToCursor)
-			commandName = Helpers.getCommandNameFromFragment(commandFragment)
-			beyondCursorRange = new Range(pos.row, pos.column, pos.row, 99999)
-			lineBeyondCursor = editor.getSession().getTextRange(beyondCursorRange)
-			return {
-				lineUpToCursor,
-				commandFragment,
-				commandName,
-				lineBeyondCursor
-			}
-
-	return Helpers

+ 0 - 45
services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/PackageManager.coffee

@@ -1,45 +0,0 @@
-define [], () ->
-	packages = [
-		'inputenc', 'graphicx', 'amsmath', 'geometry', 'amssymb', 'hyperref',
-		'babel', 'color', 'xcolor', 'url', 'natbib', 'fontenc', 'fancyhdr',
-		'amsfonts', 'booktabs', 'amsthm', 'float', 'tikz', 'caption',
-		'setspace', 'multirow', 'array', 'multicol', 'titlesec', 'enumitem',
-		'ifthen', 'listings', 'blindtext', 'subcaption', 'times', 'bm',
-		'subfigure', 'algorithm', 'fontspec', 'biblatex', 'tabularx',
-		'microtype', 'etoolbox', 'parskip', 'calc', 'verbatim', 'mathtools',
-		'epsfig', 'wrapfig', 'lipsum', 'cite', 'textcomp', 'longtable',
-		'textpos', 'algpseudocode', 'enumerate', 'subfig', 'pdfpages',
-		'epstopdf', 'latexsym', 'lmodern', 'pifont', 'ragged2e', 'rotating',
-		'dcolumn', 'xltxtra', 'marvosym', 'indentfirst', 'xspace', 'csquotes',
-		'xparse', 'changepage', 'soul', 'xunicode', 'comment', 'mathrsfs',
-		'tocbibind', 'lastpage', 'algorithm2e', 'pgfplots', 'lineno',
-		'graphics', 'algorithmic', 'fullpage', 'mathptmx', 'todonotes',
-		'ulem', 'tweaklist', 'moderncvstyleclassic', 'collection',
-		'moderncvcompatibility', 'gensymb', 'helvet', 'siunitx', 'adjustbox',
-		'placeins', 'colortbl', 'appendix', 'makeidx', 'supertabular', 'ifpdf',
-		'framed', 'aliascnt', 'layaureo', 'authblk'
-	]
-
-	class PackageManager
-		constructor: (@metadataManager) ->
-
-		getCompletions: (editor, session, pos, prefix, callback) ->
-			usedPackages = Object.keys(@metadataManager.getAllPackages())
-			packageSnippets = []
-			for pkg in packages
-				if pkg not in usedPackages
-					packageSnippets.push {
-						caption: "\\usepackage{#{pkg}}"
-						snippet: "\\usepackage{#{pkg}}"
-						meta: "pkg"
-					}
-
-			packageSnippets.push {
-				caption: "\\usepackage{}"
-				snippet: "\\usepackage{$1}"
-				meta: "pkg"
-				score: 70
-			}
-			callback null, packageSnippets
-
-	return PackageManager

+ 0 - 29
services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/snippets/Environments.coffee

@@ -1,29 +0,0 @@
-define () ->
-	envs = [
-		"abstract",
-		"align", "align*",
-		"equation", "equation*",
-		"gather", "gather*",
-		"multline", "multline*",
-		"split",
-		"verbatim",
-		"quote",
-		"center"
-	]
-
-	envsWithSnippets = [
-		"array",
-		"figure",
-		"tabular",
-		"table",
-		"list",
-		"enumerate",
-		"itemize",
-		"frame",
-		"thebibliography"
-	]
-
-	return {
-		all: envs.concat(envsWithSnippets)
-		withoutSnippets: envs
-	}

+ 0 - 691
services/web/public/coffee/ide/editor/directives/aceEditor/auto-complete/snippets/TopHundredSnippets.coffee

@@ -1,691 +0,0 @@
-define -> [{
-    "caption": "\\begin{}",
-    "snippet": "\\begin{$1}",
-    "meta": "env",
-    "score": 7.849662248028187
-}, {
-    "caption": "\\begin{}[]",
-    "snippet": "\\begin{$1}[$2]",
-    "meta": "env",
-    "score": 7.849662248028187
-}, {
-    "caption": "\\begin{}{}",
-    "snippet": "\\begin{$1}{$2}",
-    "meta": "env",
-    "score": 7.849662248028187
-}, {
-    "caption": "\\end{}",
-    "snippet": "\\end{$1}",
-    "meta": "env",
-    "score": 7.847906405228455
-}, {
-    "caption": "\\usepackage[]{}",
-    "snippet": "\\usepackage[$1]{$2}",
-    "meta": "pkg",
-    "score": 5.427890758130527
-}, {
-    "caption": "\\item",
-    "snippet": "\\item",
-    "meta": "cmd",
-    "score": 3.800886892251021
-}, {
-    "caption": "\\item[]",
-    "snippet": "\\item[$1]",
-    "meta": "cmd",
-    "score": 3.800886892251021
-}, {
-    "caption": "\\section{}",
-    "snippet": "\\section{$1}",
-    "meta": "cmd",
-    "score": 3.0952612541683835
-}, {
-    "caption": "\\textbf{}",
-    "snippet": "\\textbf{$1}",
-    "meta": "cmd",
-    "score": 2.627755982816738
-}, {
-    "caption": "\\cite{}",
-    "snippet": "\\cite{$1}",
-    "meta": "cmd",
-    "score": 2.341195220791228
-}, {
-    "caption": "\\label{}",
-    "snippet": "\\label{$1}",
-    "meta": "cmd",
-    "score": 1.897791904799601
-}, {
-    "caption": "\\textit{}",
-    "snippet": "\\textit{$1}",
-    "meta": "cmd",
-    "score": 1.6842996195493385
-}, {
-    "caption": "\\includegraphics[]{}",
-    "snippet": "\\includegraphics[$1]{$2}",
-    "meta": "cmd",
-    "score": 1.4595731795525781
-}, {
-    "caption": "\\documentclass[]{}",
-    "snippet": "\\documentclass[$1]{$2}",
-    "meta": "cmd",
-    "score": 1.4425339817971206
-}, {
-    "caption": "\\documentclass{}",
-    "snippet": "\\documentclass{$1}",
-    "meta": "cmd",
-    "score": 1.4425339817971206
-}, {
-    "caption": "\\ref{}",
-    "snippet": "\\ref{$1}",
-    "meta": "cross-reference",
-    "score": 0.014379554883991673
-}, {
-    "caption": "\\frac{}{}",
-    "snippet": "\\frac{$1}{$2}",
-    "meta": "cmd",
-    "score": 1.4341091141105058
-}, {
-    "caption": "\\subsection{}",
-    "snippet": "\\subsection{$1}",
-    "meta": "cmd",
-    "score": 1.3890912739512353
-}, {
-    "caption": "\\hline",
-    "snippet": "\\hline",
-    "meta": "cmd",
-    "score": 1.3209538327406387
-}, {
-    "caption": "\\caption{}",
-    "snippet": "\\caption{$1}",
-    "meta": "cmd",
-    "score": 1.2569477427490174
-}, {
-    "caption": "\\centering",
-    "snippet": "\\centering",
-    "meta": "cmd",
-    "score": 1.1642881814937829
-}, {
-    "caption": "\\vspace{}",
-    "snippet": "\\vspace{$1}",
-    "meta": "cmd",
-    "score": 0.9533807826673939
-}, {
-    "caption": "\\title{}",
-    "snippet": "\\title{$1}",
-    "meta": "cmd",
-    "score": 0.9202908262245683
-}, {
-    "caption": "\\author{}",
-    "snippet": "\\author{$1}",
-    "meta": "cmd",
-    "score": 0.8973590434087177
-}, {
-    "caption": "\\author[]{}",
-    "snippet": "\\author[$1]{$2}",
-    "meta": "cmd",
-    "score": 0.8973590434087177
-}, {
-    "caption": "\\maketitle",
-    "snippet": "\\maketitle",
-    "meta": "cmd",
-    "score": 0.7504160124360846
-}, {
-    "caption": "\\textwidth",
-    "snippet": "\\textwidth",
-    "meta": "cmd",
-    "score": 0.7355328080889112
-}, {
-    "caption": "\\newcommand{}{}",
-    "snippet": "\\newcommand{$1}{$2}",
-    "meta": "cmd",
-    "score": 0.7264891987129375
-}, {
-    "caption": "\\newcommand{}[]{}",
-    "snippet": "\\newcommand{$1}[$2]{$3}",
-    "meta": "cmd",
-    "score": 0.7264891987129375
-}, {
-    "caption": "\\date{}",
-    "snippet": "\\date{$1}",
-    "meta": "cmd",
-    "score": 0.7225518453076786
-}, {
-    "caption": "\\emph{}",
-    "snippet": "\\emph{$1}",
-    "meta": "cmd",
-    "score": 0.7060308784832261
-}, {
-    "caption": "\\textsc{}",
-    "snippet": "\\textsc{$1}",
-    "meta": "cmd",
-    "score": 0.6926466355384758
-}, {
-    "caption": "\\multicolumn{}{}{}",
-    "snippet": "\\multicolumn{$1}{$2}{$3}",
-    "meta": "cmd",
-    "score": 0.5473606021405326
-}, {
-    "caption": "\\input{}",
-    "snippet": "\\input{$1}",
-    "meta": "cmd",
-    "score": 0.4966021927742672
-}, {
-    "caption": "\\alpha",
-    "snippet": "\\alpha",
-    "meta": "cmd",
-    "score": 0.49520006391384913
-}, {
-    "caption": "\\in",
-    "snippet": "\\in",
-    "meta": "cmd",
-    "score": 0.4716039670146658
-}, {
-    "caption": "\\mathbf{}",
-    "snippet": "\\mathbf{$1}",
-    "meta": "cmd",
-    "score": 0.4682018419466319
-}, {
-    "caption": "\\right",
-    "snippet": "\\right",
-    "meta": "cmd",
-    "score": 0.4299239459457309
-}, {
-    "caption": "\\left",
-    "snippet": "\\left",
-    "meta": "cmd",
-    "score": 0.42937815279867964
-}, {
-    "caption": "\\left[]",
-    "snippet": "\\left[$1]",
-    "meta": "cmd",
-    "score": 0.42937815279867964
-}, {
-    "caption": "\\sum",
-    "snippet": "\\sum",
-    "meta": "cmd",
-    "score": 0.42607994509619934
-}, {
-    "caption": "\\noindent",
-    "snippet": "\\noindent",
-    "meta": "cmd",
-    "score": 0.42355747798114207
-}, {
-    "caption": "\\chapter{}",
-    "snippet": "\\chapter{$1}",
-    "meta": "cmd",
-    "score": 0.422097569591803
-}, {
-    "caption": "\\par",
-    "snippet": "\\par",
-    "meta": "cmd",
-    "score": 0.413853376001159
-}, {
-    "caption": "\\lambda",
-    "snippet": "\\lambda",
-    "meta": "cmd",
-    "score": 0.39389600578684125
-}, {
-    "caption": "\\subsubsection{}",
-    "snippet": "\\subsubsection{$1}",
-    "meta": "cmd",
-    "score": 0.3727781330132016
-}, {
-    "caption": "\\bibitem{}",
-    "snippet": "\\bibitem{$1}",
-    "meta": "cmd",
-    "score": 0.3689547570562042
-}, {
-    "caption": "\\bibitem[]{}",
-    "snippet": "\\bibitem[$1]{$2}",
-    "meta": "cmd",
-    "score": 0.3689547570562042
-}, {
-    "caption": "\\text{}",
-    "snippet": "\\text{$1}",
-    "meta": "cmd",
-    "score": 0.3608680734736821
-}, {
-    "caption": "\\setlength{}{}",
-    "snippet": "\\setlength{$1}{$2}",
-    "meta": "cmd",
-    "score": 0.354445763583904
-}, {
-    "caption": "\\setlength",
-    "snippet": "\\setlength",
-    "meta": "cmd",
-    "score": 0.354445763583904
-}, {
-    "caption": "\\mathcal{}",
-    "snippet": "\\mathcal{$1}",
-    "meta": "cmd",
-    "score": 0.35084018920966636
-}, {
-    "caption": "\\newline",
-    "snippet": "\\newline",
-    "meta": "cmd",
-    "score": 0.3311721696201715
-}, {
-    "caption": "\\newpage",
-    "snippet": "\\newpage",
-    "meta": "cmd",
-    "score": 0.3277033727934986
-}, {
-    "caption": "\\renewcommand{}{}",
-    "snippet": "\\renewcommand{$1}{$2}",
-    "meta": "cmd",
-    "score": 0.3267437011085663
-}, {
-    "caption": "\\renewcommand",
-    "snippet": "\\renewcommand",
-    "meta": "cmd",
-    "score": 0.3267437011085663
-}, {
-    "caption": "\\theta",
-    "snippet": "\\theta",
-    "meta": "cmd",
-    "score": 0.3210417159232142
-}, {
-    "caption": "\\hspace{}",
-    "snippet": "\\hspace{$1}",
-    "meta": "cmd",
-    "score": 0.3147206476372336
-}, {
-    "caption": "\\beta",
-    "snippet": "\\beta",
-    "meta": "cmd",
-    "score": 0.3061799530337638
-}, {
-    "caption": "\\texttt{}",
-    "snippet": "\\texttt{$1}",
-    "meta": "cmd",
-    "score": 0.3019066753744355
-}, {
-    "caption": "\\times",
-    "snippet": "\\times",
-    "meta": "cmd",
-    "score": 0.2957960629411553
-}, {
-    "caption": "\\citep{}",
-    "snippet": "\\citep{$1}",
-    "meta": "cmd",
-    "score": 0.2941882834697057
-}, {
-    "caption": "\\color[]{}",
-    "snippet": "\\color[$1]{$2}",
-    "meta": "cmd",
-    "score": 0.2864294797053033
-}, {
-    "caption": "\\color{}",
-    "snippet": "\\color{$1}",
-    "meta": "cmd",
-    "score": 0.2864294797053033
-}, {
-    "caption": "\\mu",
-    "snippet": "\\mu",
-    "meta": "cmd",
-    "score": 0.27635652476799255
-}, {
-    "caption": "\\bibliography{}",
-    "snippet": "\\bibliography{$1}",
-    "meta": "cmd",
-    "score": 0.2659628337907604
-}, {
-    "caption": "\\linewidth",
-    "snippet": "\\linewidth",
-    "meta": "cmd",
-    "score": 0.2639498312518439
-}, {
-    "caption": "\\delta",
-    "snippet": "\\delta",
-    "meta": "cmd",
-    "score": 0.2620578600722735
-}, {
-    "caption": "\\sigma",
-    "snippet": "\\sigma",
-    "meta": "cmd",
-    "score": 0.25940147926344487
-}, {
-    "caption": "\\pi",
-    "snippet": "\\pi",
-    "meta": "cmd",
-    "score": 0.25920934567729714
-}, {
-    "caption": "\\hat{}",
-    "snippet": "\\hat{$1}",
-    "meta": "cmd",
-    "score": 0.25264309033778715
-}, {
-    "caption": "\\hat",
-    "snippet": "\\hat",
-    "meta": "cmd",
-    "score": 0.25264309033778715
-}, {
-    "caption": "\\bibliographystyle{}",
-    "snippet": "\\bibliographystyle{$1}",
-    "meta": "cmd",
-    "score": 0.25122317941387773
-}, {
-    "caption": "\\small",
-    "snippet": "\\small",
-    "meta": "cmd",
-    "score": 0.2447632045426295
-}, {
-    "caption": "\\small{}",
-    "snippet": "\\small{$1}",
-    "meta": "cmd",
-    "score": 0.2447632045426295
-}, {
-    "caption": "\\LaTeX",
-    "snippet": "\\LaTeX",
-    "meta": "cmd",
-    "score": 0.2334089308452787
-}, {
-    "caption": "\\LaTeX{}",
-    "snippet": "\\LaTeX{$1}",
-    "meta": "cmd",
-    "score": 0.2334089308452787
-}, {
-    "caption": "\\cdot",
-    "snippet": "\\cdot",
-    "meta": "cmd",
-    "score": 0.23029085545522762
-}, {
-    "caption": "\\footnote{}",
-    "snippet": "\\footnote{$1}",
-    "meta": "cmd",
-    "score": 0.2253056071787701
-}, {
-    "caption": "\\newtheorem{}[]{}",
-    "snippet": "\\newtheorem{$1}[$2]{$3}",
-    "meta": "cmd",
-    "score": 0.215689795055434
-}, {
-    "caption": "\\newtheorem{}{}",
-    "snippet": "\\newtheorem{$1}{$2}",
-    "meta": "cmd",
-    "score": 0.215689795055434
-}, {
-    "caption": "\\newtheorem{}{}[]",
-    "snippet": "\\newtheorem{$1}{$2}[$3]",
-    "meta": "cmd",
-    "score": 0.215689795055434
-}, {
-    "caption": "\\Delta",
-    "snippet": "\\Delta",
-    "meta": "cmd",
-    "score": 0.21386475063892618
-}, {
-    "caption": "\\tau",
-    "snippet": "\\tau",
-    "meta": "cmd",
-    "score": 0.21236188205859796
-}, {
-    "caption": "\\hfill",
-    "snippet": "\\hfill",
-    "meta": "cmd",
-    "score": 0.2058248088519886
-}, {
-    "caption": "\\leq",
-    "snippet": "\\leq",
-    "meta": "cmd",
-    "score": 0.20498894440637172
-}, {
-    "caption": "\\footnotesize",
-    "snippet": "\\footnotesize",
-    "meta": "cmd",
-    "score": 0.2038592081252624
-}, {
-    "caption": "\\footnotesize{}",
-    "snippet": "\\footnotesize{$1}",
-    "meta": "cmd",
-    "score": 0.2038592081252624
-}, {
-    "caption": "\\large",
-    "snippet": "\\large",
-    "meta": "cmd",
-    "score": 0.20377416734108866
-}, {
-    "caption": "\\large{}",
-    "snippet": "\\large{$1}",
-    "meta": "cmd",
-    "score": 0.20377416734108866
-}, {
-    "caption": "\\sqrt{}",
-    "snippet": "\\sqrt{$1}",
-    "meta": "cmd",
-    "score": 0.20240160977404634
-}, {
-    "caption": "\\epsilon",
-    "snippet": "\\epsilon",
-    "meta": "cmd",
-    "score": 0.2005136761359043
-}, {
-    "caption": "\\Large",
-    "snippet": "\\Large",
-    "meta": "cmd",
-    "score": 0.1987771081149759
-}, {
-    "caption": "\\Large{}",
-    "snippet": "\\Large{$1}",
-    "meta": "cmd",
-    "score": 0.1987771081149759
-}, {
-    "caption": "\\cvitem{}{}",
-    "snippet": "\\cvitem{$1}{$2}",
-    "meta": "cmd",
-    "score": 0.19605476980016281
-}, {
-    "caption": "\\rho",
-    "snippet": "\\rho",
-    "meta": "cmd",
-    "score": 0.1959287380541684
-}, {
-    "caption": "\\omega",
-    "snippet": "\\omega",
-    "meta": "cmd",
-    "score": 0.19326783415115262
-}, {
-    "caption": "\\mathrm{}",
-    "snippet": "\\mathrm{$1}",
-    "meta": "cmd",
-    "score": 0.19117752976172653
-}, {
-    "caption": "\\boldsymbol{}",
-    "snippet": "\\boldsymbol{$1}",
-    "meta": "cmd",
-    "score": 0.18137737738638837
-}, {
-    "caption": "\\boldsymbol",
-    "snippet": "\\boldsymbol",
-    "meta": "cmd",
-    "score": 0.18137737738638837
-}, {
-    "caption": "\\gamma",
-    "snippet": "\\gamma",
-    "meta": "cmd",
-    "score": 0.17940276535431304
-}, {
-    "caption": "\\clearpage",
-    "snippet": "\\clearpage",
-    "meta": "cmd",
-    "score": 0.1789117552185788
-}, {
-    "caption": "\\infty",
-    "snippet": "\\infty",
-    "meta": "cmd",
-    "score": 0.17837290019711305
-}, {
-    "caption": "\\phi",
-    "snippet": "\\phi",
-    "meta": "cmd",
-    "score": 0.17405809173097808
-}, {
-    "caption": "\\partial",
-    "snippet": "\\partial",
-    "meta": "cmd",
-    "score": 0.17168102367966637
-}, {
-    "caption": "\\include{}",
-    "snippet": "\\include{$1}",
-    "meta": "cmd",
-    "score": 0.1547080054979312
-}, {
-    "caption": "\\address{}",
-    "snippet": "\\address{$1}",
-    "meta": "cmd",
-    "score": 0.1525055392611109
-}, {
-    "caption": "\\address[]{}",
-    "snippet": "\\address[$1]{$2}",
-    "meta": "cmd",
-    "score": 0.1525055392611109
-}, {
-    "caption": "\\address{}{}{}",
-    "snippet": "\\address{$1}{$2}{$3}",
-    "meta": "cmd",
-    "score": 0.1525055392611109
-}, {
-    "caption": "\\quad",
-    "snippet": "\\quad",
-    "meta": "cmd",
-    "score": 0.15242755832392743
-}, {
-    "caption": "\\email{}",
-    "snippet": "\\email{$1}",
-    "meta": "cmd",
-    "score": 0.1522294670109857
-}, {
-    "caption": "\\paragraph{}",
-    "snippet": "\\paragraph{$1}",
-    "meta": "cmd",
-    "score": 0.152074250347974
-}, {
-    "caption": "\\varepsilon",
-    "snippet": "\\varepsilon",
-    "meta": "cmd",
-    "score": 0.05411564201390573
-}, {
-    "caption": "\\zeta",
-    "snippet": "\\zeta",
-    "meta": "cmd",
-    "score": 0.023330249803752954
-}, {
-    "caption": "\\eta",
-    "snippet": "\\eta",
-    "meta": "cmd",
-    "score": 0.11088718379889091
-}, {
-    "caption": "\\vartheta",
-    "snippet": "\\vartheta",
-    "meta": "cmd",
-    "score": 0.0025822992078068712
-}, {
-    "caption": "\\iota",
-    "snippet": "\\iota",
-    "meta": "cmd",
-    "score": 0.0024774003791525486
-}, {
-    "caption": "\\kappa",
-    "snippet": "\\kappa",
-    "meta": "cmd",
-    "score": 0.04887876299369008
-}, {
-    "caption": "\\nu",
-    "snippet": "\\nu",
-    "meta": "cmd",
-    "score": 0.09206962821059342
-}, {
-    "caption": "\\xi",
-    "snippet": "\\xi",
-    "meta": "cmd",
-    "score": 0.06496042899265699
-}, {
-    "caption": "\\varpi",
-    "snippet": "\\varpi",
-    "meta": "cmd",
-    "score": 0.0007039358167790341
-}, {
-    "caption": "\\varrho",
-    "snippet": "\\varrho",
-    "meta": "cmd",
-    "score": 0.0011279491613898612
-}, {
-    "caption": "\\varsigma",
-    "snippet": "\\varsigma",
-    "meta": "cmd",
-    "score": 0.0010424880711234978
-}, {
-    "caption": "\\varsigma{}",
-    "snippet": "\\varsigma{$1}",
-    "meta": "cmd",
-    "score": 0.0010424880711234978
-}, {
-    "caption": "\\upsilon",
-    "snippet": "\\upsilon",
-    "meta": "cmd",
-    "score": 0.00420715572598688
-}, {
-    "caption": "\\varphi",
-    "snippet": "\\varphi",
-    "meta": "cmd",
-    "score": 0.03351251516668212
-}, {
-    "caption": "\\chi",
-    "snippet": "\\chi",
-    "meta": "cmd",
-    "score": 0.043373492287805675
-}, {
-    "caption": "\\psi",
-    "snippet": "\\psi",
-    "meta": "cmd",
-    "score": 0.09994508706163642
-}, {
-    "caption": "\\Gamma",
-    "snippet": "\\Gamma",
-    "meta": "cmd",
-    "score": 0.04801549269801977
-}, {
-    "caption": "\\Theta",
-    "snippet": "\\Theta",
-    "meta": "cmd",
-    "score": 0.038090902146599444
-}, {
-    "caption": "\\Lambda",
-    "snippet": "\\Lambda",
-    "meta": "cmd",
-    "score": 0.032206594305977686
-}, {
-    "caption": "\\Xi",
-    "snippet": "\\Xi",
-    "meta": "cmd",
-    "score": 0.01060997225400494
-}, {
-    "caption": "\\Pi",
-    "snippet": "\\Pi",
-    "meta": "cmd",
-    "score": 0.021264671817473237
-}, {
-    "caption": "\\Sigma",
-    "snippet": "\\Sigma",
-    "meta": "cmd",
-    "score": 0.05769642802079917
-}, {
-    "caption": "\\Upsilon",
-    "snippet": "\\Upsilon",
-    "meta": "cmd",
-    "score": 0.00032875192955749566
-}, {
-    "caption": "\\Phi",
-    "snippet": "\\Phi",
-    "meta": "cmd",
-    "score": 0.0538724950042562
-}, {
-    "caption": "\\Psi",
-    "snippet": "\\Psi",
-    "meta": "cmd",
-    "score": 0.03056589143021648
-}, {
-    "caption": "\\Omega",
-    "snippet": "\\Omega",
-    "meta": "cmd",
-    "score": 0.09490387997853639
-}]

+ 0 - 32
services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionAdapter.coffee

@@ -1,32 +0,0 @@
-define [
-	"ide/editor/AceShareJsCodec"
-], (AceShareJsCodec) ->
-	class CursorPositionAdapter
-		constructor: (@editor) ->
-
-		getCursor: () ->
-			@editor.getCursorPosition()
-
-		getEditorScrollPosition: () ->
-			@editor.getFirstVisibleRow()
-
-		setCursor: (pos) ->
-			pos = pos.cursorPosition or { row: 0, column: 0 }
-			@editor.moveCursorToPosition(pos)
-
-		setEditorScrollPosition: (pos) ->
-			pos = pos.firstVisibleLine or 0
-			@editor.scrollToLine(pos)
-
-		clearSelection: () ->
-			@editor.selection.clearSelection()
-
-		gotoLine: (line, column) ->
-			@editor.gotoLine(line, column)
-			@editor.scrollToLine(line, true, true) # centre and animate
-			@editor.focus()
-
-		gotoOffset: (offset) ->
-			lines = @editor.getSession().getDocument().getAllLines()
-			position = AceShareJsCodec.shareJsOffsetToAcePosition(offset, lines)
-			@gotoLine(position.row + 1, position.column)

+ 0 - 67
services/web/public/coffee/ide/editor/directives/aceEditor/cursor-position/CursorPositionManager.coffee

@@ -1,67 +0,0 @@
-define [], () ->
-	class CursorPositionManager
-		constructor: (@$scope, @adapter, @localStorage) ->
-			@$scope.$on 'editorInit', @jumpToPositionInNewDoc
-
-			@$scope.$on 'beforeChangeDocument', @storePositionAndLine
-
-			@$scope.$on 'afterChangeDocument', @jumpToPositionInNewDoc
-
-			@$scope.$on 'changeEditor', @storePositionAndLine
-
-			@$scope.$on "#{@$scope.name}:gotoLine", (e, line, column) =>
-				if line?
-					setTimeout () =>
-						@adapter.gotoLine(line, column)
-					, 10 # Hack: Must happen after @gotoStoredPosition
-			
-			@$scope.$on "#{@$scope.name}:gotoOffset", (e, offset) =>
-				if offset?
-					setTimeout () =>
-						@adapter.gotoOffset(offset)
-					, 10 # Hack: Must happen after @gotoStoredPosition
-
-			@$scope.$on "#{@$scope.name}:clearSelection", (e) =>
-				@adapter.clearSelection()
-
-		storePositionAndLine: () =>
-			@storeCursorPosition()
-			@storeFirstVisibleLine()
-
-		jumpToPositionInNewDoc: () =>
-			@doc_id = @$scope.sharejsDoc?.doc_id
-			setTimeout () =>
-				@gotoStoredPosition()
-			, 0
-
-		onUnload: () =>
-			@storeCursorPosition()
-			@storeFirstVisibleLine()
-
-		onCursorChange: () =>
-			@emitCursorUpdateEvent()
-
-		onSyncToPdf: () =>
-			@$scope.$emit "cursor:#{@$scope.name}:syncToPdf"
-
-		storeFirstVisibleLine: () ->
-			if @doc_id?
-				docPosition = @localStorage("doc.position.#{@doc_id}") || {}
-				docPosition.firstVisibleLine = @adapter.getEditorScrollPosition()
-				@localStorage("doc.position.#{@doc_id}", docPosition)
-
-		storeCursorPosition: () ->
-			if @doc_id?
-				docPosition = @localStorage("doc.position.#{@doc_id}") || {}
-				docPosition.cursorPosition = @adapter.getCursor()
-				@localStorage("doc.position.#{@doc_id}", docPosition)
-
-		emitCursorUpdateEvent: () ->
-			cursor = @adapter.getCursor()
-			@$scope.$emit "cursor:#{@$scope.name}:update", cursor
-
-		gotoStoredPosition: () ->
-			return if !@doc_id?
-			pos = @localStorage("doc.position.#{@doc_id}") || {}
-			@adapter.setCursor(pos)
-			@adapter.setEditorScrollPosition(pos)

+ 0 - 265
services/web/public/coffee/ide/editor/directives/aceEditor/highlights/HighlightsManager.coffee

@@ -1,265 +0,0 @@
-define [
-	"ace/ace"
-	"ide/colors/ColorManager"
-], (_, ColorManager) ->
-	Range = ace.require("ace/range").Range
-
-	class HighlightsManager
-		constructor: (@$scope, @editor, @element) ->
-			@markerIds = []
-			@labels = []
-
-			@$scope.annotationLabel = {
-				show: false
-				right:  "auto"
-				left:   "auto"
-				top:    "auto"
-				bottom: "auto"
-				backgroundColor: "black"
-				text: ""
-			}
-
-			@$scope.updateLabels = {
-				updatesAbove: 0
-				updatesBelow: 0
-			}
-
-			@$scope.$watch "highlights", (value) =>
-				@redrawAnnotations()
-
-			@$scope.$watch "theme", (value) =>
-				@redrawAnnotations()
-
-			@editor.on "mousemove", (e) =>
-				position = @editor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
-				e.position = position
-				@showAnnotationLabels(position)
-
-			onChangeScrollTop = () =>
-				@updateShowMoreLabels()
-
-			@editor.getSession().on "changeScrollTop", onChangeScrollTop
-
-			@$scope.$watch "text", () =>
-				if @$scope.navigateHighlights
-					setTimeout () =>
-						@scrollToFirstHighlight()
-					, 0
-
-			@editor.on "changeSession", (e) =>
-				e.oldSession?.off "changeScrollTop", onChangeScrollTop
-				e.session.on "changeScrollTop", onChangeScrollTop
-				@redrawAnnotations()
-
-			@$scope.gotoHighlightBelow = () =>
-				return if !@firstHiddenHighlightAfter?
-				@editor.scrollToLine(@firstHiddenHighlightAfter.end.row, true, false)
-
-			@$scope.gotoHighlightAbove = () =>
-				return if !@lastHiddenHighlightBefore?
-				@editor.scrollToLine(@lastHiddenHighlightBefore.start.row, true, false)
-
-		redrawAnnotations: () ->
-			@_clearMarkers()
-			@_clearLabels()
-
-			for annotation in @$scope.highlights or []
-				do (annotation) =>
-					colorScheme = ColorManager.getColorScheme(annotation.hue, @element)
-					if annotation.cursor?
-						@labels.push {
-							text: annotation.label
-							range: new Range(
-								annotation.cursor.row, annotation.cursor.column,
-								annotation.cursor.row, annotation.cursor.column + 1
-							)
-							colorScheme: colorScheme
-							snapToStartOfRange: true
-						}
-						@_drawCursor(annotation, colorScheme)
-					else if annotation.highlight?
-						@labels.push {
-							text: annotation.label
-							range: new Range(
-								annotation.highlight.start.row, annotation.highlight.start.column,
-								annotation.highlight.end.row,   annotation.highlight.end.column
-							)
-							colorScheme: colorScheme
-						}
-						@_drawHighlight(annotation, colorScheme)
-					else if annotation.strikeThrough?
-						@labels.push {
-							text: annotation.label
-							range: new Range(
-								annotation.strikeThrough.start.row, annotation.strikeThrough.start.column,
-								annotation.strikeThrough.end.row,   annotation.strikeThrough.end.column
-							)
-							colorScheme: colorScheme
-						}
-						@_drawStrikeThrough(annotation, colorScheme)
-
-			@updateShowMoreLabels()
-
-		showAnnotationLabels: (position) ->
-			labelToShow = null
-			for label in @labels or []
-				if label.range.contains(position.row, position.column)
-					labelToShow = label
-
-			if !labelToShow?
-				# this is the most common path, triggered on mousemove, so
-				# for performance only apply setting when it changes
-				if @$scope?.annotationLabel?.show != false
-					@$scope.$apply () =>
-						@$scope.annotationLabel.show = false
-			else
-				$ace = $(@editor.renderer.container).find(".ace_scroller")
-				# Move the label into the Ace content area so that offsets and positions are easy to calculate.
-				$ace.append(@element.find(".annotation-label"))
-
-				if labelToShow.snapToStartOfRange
-					coords = @editor.renderer.textToScreenCoordinates(labelToShow.range.start.row, labelToShow.range.start.column)
-				else
-					coords = @editor.renderer.textToScreenCoordinates(position.row, position.column)
-
-				offset = $ace.offset()
-				height = $ace.height()
-				coords.pageX = coords.pageX - offset.left
-				coords.pageY = coords.pageY - offset.top
-
-				if coords.pageY > @editor.renderer.lineHeight * 2
-					top    = "auto"
-					bottom = height - coords.pageY
-				else
-					top    = coords.pageY + @editor.renderer.lineHeight
-					bottom = "auto"
-
-				# Apply this first that the label has the correct width when calculating below
-				@$scope.$apply () =>
-					@$scope.annotationLabel.text = labelToShow.text
-					@$scope.annotationLabel.show = true
-
-				$label = @element.find(".annotation-label")
-
-				if coords.pageX + $label.outerWidth() < $ace.width()
-					left  = coords.pageX
-					right = "auto"
-				else
-					right = 0
-					left = "auto"
-
-				@$scope.$apply () =>
-					@$scope.annotationLabel = {
-						show:   true
-						left:   left
-						right:  right
-						bottom: bottom
-						top:    top
-						backgroundColor: labelToShow.colorScheme.labelBackgroundColor
-						text:   labelToShow.text
-					}
-
-		updateShowMoreLabels: () ->
-			return if !@$scope.navigateHighlights
-			setTimeout () =>
-				firstRow = @editor.getFirstVisibleRow()
-				lastRow  = @editor.getLastVisibleRow()
-				highlightsBefore = 0
-				highlightsAfter = 0
-				@lastHiddenHighlightBefore = null
-				@firstHiddenHighlightAfter = null
-				for annotation in @$scope.highlights or []
-					range = annotation.highlight or annotation.strikeThrough
-					continue if !range?
-					if range.start.row < firstRow
-						highlightsBefore += 1
-						@lastHiddenHighlightBefore = range
-					if range.end.row > lastRow
-						highlightsAfter += 1
-						@firstHiddenHighlightAfter ||= range
-
-				@$scope.$apply =>
-					@$scope.updateLabels = {
-						highlightsBefore: highlightsBefore
-						highlightsAfter:  highlightsAfter
-					}
-			, 100
-
-		scrollToFirstHighlight: () ->
-			for annotation in @$scope.highlights or []
-				range = annotation.highlight or annotation.strikeThrough
-				continue if !range?
-				@editor.scrollToLine(range.start.row, true, false)
-				break
-
-		_clearMarkers: () ->
-			for marker_id in @markerIds
-				@editor.getSession().removeMarker(marker_id)
-			@markerIds = []
-
-		_clearLabels: () ->
-			@labels = []
-
-		_drawCursor: (annotation, colorScheme) ->
-			@markerIds.push @editor.getSession().addMarker new Range(
-				annotation.cursor.row, annotation.cursor.column,
-				annotation.cursor.row, annotation.cursor.column + 1
-			), "annotation remote-cursor", (html, range, left, top, config) ->
-				div = """
-					<div
-						class='remote-cursor custom ace_start'
-						style='height: #{config.lineHeight}px; top:#{top}px; left:#{left}px; border-color: #{colorScheme.cursor};'
-					>
-						<div class="nubbin" style="bottom: #{config.lineHeight}px; background-color: #{colorScheme.cursor};"></div>
-					</div>
-				"""
-				html.push div
-			, true
-
-		_drawHighlight: (annotation, colorScheme) ->
-			@_addMarkerWithCustomStyle(
-				new Range(
-					annotation.highlight.start.row, annotation.highlight.start.column,
-					annotation.highlight.end.row,   annotation.highlight.end.column
-				),
-				"annotation highlight",
-				false,
-				"background-color: #{colorScheme.highlightBackgroundColor}"
-			)
-
-		_drawStrikeThrough: (annotation, colorScheme) ->
-			lineHeight = @editor.renderer.lineHeight
-			@_addMarkerWithCustomStyle(
-				new Range(
-					annotation.strikeThrough.start.row, annotation.strikeThrough.start.column,
-					annotation.strikeThrough.end.row,   annotation.strikeThrough.end.column
-				),
-				"annotation strike-through-background",
-				false,
-				"background-color: #{colorScheme.strikeThroughBackgroundColor}"
-			)
-			@_addMarkerWithCustomStyle(
-				new Range(
-					annotation.strikeThrough.start.row, annotation.strikeThrough.start.column,
-					annotation.strikeThrough.end.row,   annotation.strikeThrough.end.column
-				),
-				"annotation strike-through-foreground",
-				true,
-				"""
-					height: #{Math.round(lineHeight/2) + 2}px;
-					border-bottom: 2px solid #{colorScheme.strikeThroughForegroundColor};
-				"""
-			)
-
-		_addMarkerWithCustomStyle: (range, klass, foreground, style) ->
-			if foreground?
-				markerLayer = @editor.renderer.$markerBack
-			else
-				markerLayer = @editor.renderer.$markerFront
-
-			@markerIds.push @editor.getSession().addMarker range, klass, (html, range, left, top, config) ->
-				if range.isMultiLine()
-					markerLayer.drawTextMarker(html, range, klass, config, style)
-				else
-					markerLayer.drawSingleLineMarker(html, range, "#{klass} ace_start", config, 0, style)
-			, foreground

+ 0 - 66
services/web/public/coffee/ide/editor/directives/aceEditor/metadata/MetadataManager.coffee

@@ -1,66 +0,0 @@
-define [
-	"ace/ace"
-], () ->
-	Range = ace.require("ace/range").Range
-
-	getLastCommandFragment = (lineUpToCursor) ->
-		if m = lineUpToCursor.match(/(\\[^\\]+)$/)
-			return m[1]
-		else
-			return null
-
-	class MetadataManager
-		constructor: (@$scope, @editor, @element, @Metadata) ->
-			@debouncer = {}  # DocId => Timeout
-
-			onChange = (change) =>
-				if change.remote
-					return
-				if change.action not in ['remove', 'insert']
-					return
-				cursorPosition = @editor.getCursorPosition()
-				end = change.end
-				range = new Range(end.row, 0, end.row, end.column)
-				lineUpToCursor = @editor.getSession().getTextRange range
-				if lineUpToCursor.trim() == '%' or lineUpToCursor.slice(0, 1) == '\\'
-					range = new Range(end.row, 0, end.row, end.column + 80)
-					lineUpToCursor = @editor.getSession().getTextRange range
-				commandFragment = getLastCommandFragment lineUpToCursor
-
-				linesContainPackage = _.any(
-					change.lines,
-					(line) -> line.match(/^\\usepackage(?:\[.{0,80}?])?{(.{0,80}?)}/)
-				)
-				linesContainReqPackage = _.any(
-					change.lines,
-					(line) -> line.match(/^\\RequirePackage(?:\[.{0,80}?])?{(.{0,80}?)}/)
-				)
-				linesContainLabel = _.any(
-					change.lines,
-					(line) -> line.match(/\\label{(.{0,80}?)}/)
-				)
-				linesContainMeta =
-					linesContainPackage or
-					linesContainLabel or
-					linesContainReqPackage
-
-				lastCommandFragmentIsLabel = commandFragment?.slice(0, 7) == '\\label{'
-				lastCommandFragmentIsPackage = commandFragment?.slice(0, 11) == '\\usepackage'
-				lastCommandFragmentIsReqPack = commandFragment?.slice(0, 15) == '\\RequirePackage'
-				lastCommandFragmentIsMeta =
-					lastCommandFragmentIsPackage or
-					lastCommandFragmentIsLabel or
-					lastCommandFragmentIsReqPack
-
-				if linesContainMeta or lastCommandFragmentIsMeta
-					@Metadata.scheduleLoadDocMetaFromServer @$scope.docId
-
-			@editor.on "changeSession", (e) =>
-				e.oldSession.off "change", onChange
-				e.session.on "change", onChange
-
-		getAllLabels: () ->
-			@Metadata.getAllLabels()
-
-		getAllPackages: () ->
-			@Metadata.getAllPackages()

+ 0 - 96
services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/HighlightedWordManager.coffee

@@ -1,96 +0,0 @@
-define [
-	"ace/ace"
-], () ->
-	Range = ace.require("ace/range").Range
-
-	class Highlight
-		constructor: (@markerId, @range, options) ->
-			@word = options.word
-			@suggestions = options.suggestions
-
-	class HighlightedWordManager
-		constructor: (@editor) ->
-			@reset()
-
-		reset: () ->
-			@highlights?.forEach (highlight) =>
-				@editor.getSession().removeMarker(highlight.markerId)
-			@highlights = []
-
-		addHighlight: (options) ->
-			session = @editor.getSession()
-			doc = session.getDocument()
-			# Set up Range that will automatically update it's positions when the
-			# document changes
-			range = new Range()
-			range.start = doc.createAnchor({
-				row: options.row,
-				column: options.column
-			})
-			range.end = doc.createAnchor({
-				row: options.row,
-				column: options.column + options.word.length
-			})
-			# Prevent range from adding newly typed characters to the end of the word.
-			# This makes it appear as if the spelling error continues to the next word
-			# even after a space
-			range.end.$insertRight = true
-
-			markerId = session.addMarker range, "spelling-highlight", 'text', false
-
-			@highlights.push new Highlight(markerId, range, options)
-
-		removeHighlight: (highlight) ->
-			@editor.getSession().removeMarker(highlight.markerId)
-			@highlights = @highlights.filter (hl) ->
-				hl != highlight
-
-		removeWord: (word) ->
-			@highlights.filter (highlight) ->
-				highlight.word == word
-			.forEach (highlight) =>
-				@removeHighlight(highlight)
-
-		clearRow: (row) ->
-			@highlights.filter (highlight) ->
-				highlight.range.start.row == row
-			.forEach (highlight) =>
-				@removeHighlight(highlight)
-
-		findHighlightWithinRange: (range) ->
-			_.find @highlights, (highlight) =>
-				@_doesHighlightOverlapRange highlight, range.start, range.end
-
-		_doesHighlightOverlapRange: (highlight, start, end) ->
-			highlightRow = highlight.range.start.row
-			highlightStartColumn = highlight.range.start.column
-			highlightEndColumn = highlight.range.end.column
-
-			highlightIsAllBeforeRange =
-				highlightRow < start.row or
-				(highlightRow == start.row and highlightEndColumn <= start.column)
-			highlightIsAllAfterRange =
-				highlightRow > end.row or
-				(highlightRow == end.row and highlightStartColumn >= end.column)
-			!(highlightIsAllBeforeRange or highlightIsAllAfterRange)
-
-		clearHighlightTouchingRange: (range) ->
-			highlight = _.find @highlights, (hl) =>
-				@_doesHighlightTouchRange hl, range.start, range.end
-			if highlight
-				@removeHighlight highlight
-
-		_doesHighlightTouchRange: (highlight, start, end) ->
-			highlightRow = highlight.range.start.row
-			highlightStartColumn = highlight.range.start.column
-			highlightEndColumn = highlight.range.end.column
-
-			rangeStartIsWithinHighlight =
-				highlightStartColumn <= start.column and
-				highlightEndColumn >= start.column
-			rangeEndIsWithinHighlight =
-				highlightStartColumn <= end.column and
-				highlightEndColumn >= end.column
-
-			highlightRow == start.row and
-				(rangeStartIsWithinHighlight or rangeEndIsWithinHighlight)

+ 0 - 62
services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckAdapter.coffee

@@ -1,62 +0,0 @@
-define [
-	"ace/ace"
-	"ide/editor/directives/aceEditor/spell-check/HighlightedWordManager"
-], (Ace, HighlightedWordManager) ->
-	Range = ace.require('ace/range').Range
-
-	class SpellCheckAdapter
-		constructor: (@editor) ->
-			@highlightedWordManager = new HighlightedWordManager(@editor)
-
-		getLines: () ->
-			@editor.getValue().split('\n')
-
-		normalizeChangeEvent: (e) -> e
-
-		getCoordsFromContextMenuEvent: (e) ->
-			e.domEvent.stopPropagation()
-			return {
-				x: e.domEvent.clientX,
-				y: e.domEvent.clientY
-			}
-
-		preventContextMenuEventDefault: (e) ->
-			e.domEvent.preventDefault()
-
-		getHighlightFromCoords: (coords) ->
-			position = @editor.renderer.screenToTextCoordinates(coords.x, coords.y)
-			@highlightedWordManager.findHighlightWithinRange({
-				start: position
-				end: position
-			})
-
-		isContextMenuEventOnBottomHalf: (e) ->
-			clientY = e.domEvent.clientY
-			editorBoundingRect = e.target.container.getBoundingClientRect()
-			relativeYPos = (clientY - editorBoundingRect.top) / editorBoundingRect.height
-			return relativeYPos > 0.5
-
-		selectHighlightedWord: (highlight) ->
-			row = highlight.range.start.row
-			startColumn = highlight.range.start.column
-			endColumn = highlight.range.end.column
-
-			@editor.getSession().getSelection().setSelectionRange(
-				new Range(
-					row, startColumn,
-					row, endColumn
-				)
-			)
-
-		replaceWord: (highlight, newWord) =>
-			row = highlight.range.start.row
-			startColumn = highlight.range.start.column
-			endColumn = highlight.range.end.column
-
-			@editor.getSession().replace(new Range(
-				row, startColumn,
-				row, endColumn
-			), newWord)
-
-			# Bring editor back into focus after clicking on suggestion
-			@editor.focus()

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 209
services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee


+ 0 - 565
services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee

@@ -1,565 +0,0 @@
-define [
-	"ace/ace"
-	"utils/EventEmitter"
-	"ide/colors/ColorManager"
-	"ide/editor/AceShareJsCodec"
-], (_, EventEmitter, ColorManager, AceShareJsCodec) ->
-	class TrackChangesManager
-		Range = ace.require("ace/range").Range
-
-		constructor: (@$scope, @editor, @element) ->
-			window.trackChangesManager ?= @
-
-			@$scope.$watch "trackChanges", (track_changes) =>
-				return if !track_changes?
-				@setTrackChanges(track_changes)
-
-			@$scope.$watch "sharejsDoc", (doc, oldDoc) =>
-				return if !doc?
-				if oldDoc?
-					@disconnectFromDoc(oldDoc)
-				@connectToDoc(doc)
-
-			@$scope.$on "comment:add", (e, thread_id, offset, length) =>
-				@addCommentToSelection(thread_id, offset, length)
-
-			@$scope.$on "comment:select_line", (e) =>
-				@selectLineIfNoSelection()
-
-			@$scope.$on "changes:accept", (e, change_ids) =>
-				@acceptChangeIds(change_ids)
-
-			@$scope.$on "changes:reject", (e, change_ids) =>
-				@rejectChangeIds(change_ids)
-
-			@$scope.$on "comment:remove", (e, comment_id) =>
-				@removeCommentId(comment_id)
-
-			@$scope.$on "comment:resolve_threads", (e, thread_ids) =>
-				@hideCommentsByThreadIds(thread_ids)
-
-			@$scope.$on "comment:unresolve_thread", (e, thread_id) =>
-				@showCommentByThreadId(thread_id)
-
-			@$scope.$on "review-panel:recalculate-screen-positions", () =>
-				@recalculateReviewEntriesScreenPositions()
-
-			changingSelection = false
-			onChangeSelection = () =>
-				# Deletes can send about 5 changeSelection events, so
-				# just act on the last one.
-				if !changingSelection
-					changingSelection = true
-					@$scope.$evalAsync () =>
-						changingSelection = false
-						@updateFocus()
-
-			onResize = () =>
-				@recalculateReviewEntriesScreenPositions()
-
-			onChangeSession = (e) =>
-				@clearAnnotations()
-				@redrawAnnotations()
-				@editor.session.on "changeScrollTop", onChangeScroll
-
-			_scrollTimeout = null
-			onChangeScroll = () =>
-				if _scrollTimeout?
-					return
-				else
-					_scrollTimeout = setTimeout () =>
-						@recalculateVisibleEntries()
-						@$scope.$apply()
-						_scrollTimeout = null
-					, 200
-
-			@_resetCutState()
-			onCut = () => @onCut()
-			onPaste = () => @onPaste()
-
-			bindToAce = () =>
-				@editor.on "changeSelection", onChangeSelection
-				@editor.on "change", onChangeSelection # Selection also moves with updates elsewhere in the document
-				@editor.on "changeSession", onChangeSession
-				@editor.on "cut", onCut
-				@editor.on "paste", onPaste
-				@editor.renderer.on "resize", onResize
-
-			unbindFromAce = () =>
-				@editor.off "changeSelection", onChangeSelection
-				@editor.off "change", onChangeSelection
-				@editor.off "changeSession", onChangeSession
-				@editor.off "cut", onCut
-				@editor.off "paste", onPaste
-				@editor.renderer.off "resize", onResize
-
-			@$scope.$watch "trackChangesEnabled", (enabled) =>
-				return if !enabled?
-				if enabled
-					bindToAce()
-				else
-					unbindFromAce()
-
-		disconnectFromDoc: (doc) ->
-			@changeIdToMarkerIdMap = {}
-			doc.off "ranges:clear"
-			doc.off "ranges:redraw"
-			doc.off "ranges:dirty"
-
-		setTrackChanges: (value) ->
-			if value
-				@$scope.sharejsDoc?.track_changes_as = window.user.id or "anonymous"
-			else
-				@$scope.sharejsDoc?.track_changes_as = null
-
-		connectToDoc: (doc) ->
-			@rangesTracker = doc.ranges
-			@setTrackChanges(@$scope.trackChanges)
-
-			doc.on "ranges:dirty", () =>
-				@updateAnnotations()
-			doc.on "ranges:clear", () =>
-				@clearAnnotations()
-			doc.on "ranges:redraw", () =>
-				@redrawAnnotations()
-
-		clearAnnotations: () ->
-			session = @editor.getSession()
-			for change_id, markers of @changeIdToMarkerIdMap
-				for marker_name, marker_id of markers
-					session.removeMarker marker_id
-			@changeIdToMarkerIdMap = {}
-
-		redrawAnnotations: () ->
-			for change in @rangesTracker.changes
-				if change.op.i?
-					@_onInsertAdded(change)
-				else if change.op.d?
-					@_onDeleteAdded(change)
-
-			for comment in @rangesTracker.comments
-				@_onCommentAdded(comment)
-
-			@broadcastChange()
-
-		_doneUpdateThisLoop: false
-		_pendingUpdates: false
-		updateAnnotations: () ->
-			# Doc updates with multiple ops, like search/replace or block comments
-			# will call this with every individual op in a single event loop. So only
-			# do the first this loop, then schedule an update for the next loop for the rest.
-			if !@_doneUpdateThisLoop
-				@_doUpdateAnnotations()
-				@_doneUpdateThisLoop = true
-				setTimeout () =>
-					if @_pendingUpdates
-						@_doUpdateAnnotations()
-					@_doneUpdateThisLoop = false
-					@_pendingUpdates = false
-			else
-				@_pendingUpdates = true
-
-		_doUpdateAnnotations: () ->
-			dirty = @rangesTracker.getDirtyState()
-
-			updateMarkers = false
-
-			for id, change of dirty.change.added
-				if change.op.i?
-					@_onInsertAdded(change)
-				else if change.op.d?
-					@_onDeleteAdded(change)
-			for id, change of dirty.change.removed
-				if change.op.i?
-					@_onInsertRemoved(change)
-				else if change.op.d?
-					@_onDeleteRemoved(change)
-			for id, change of dirty.change.moved
-				updateMarkers = true
-				@_onChangeMoved(change)
-
-			for id, comment of dirty.comment.added
-				@_onCommentAdded(comment)
-			for id, comment of dirty.comment.removed
-				@_onCommentRemoved(comment)
-			for id, comment of dirty.comment.moved
-				updateMarkers = true
-				@_onCommentMoved(comment)
-
-			@rangesTracker.resetDirtyState()
-			if updateMarkers
-				@editor.renderer.updateBackMarkers()
-			@broadcastChange()
-
-		addComment: (offset, content, thread_id) ->
-			op = { c: content, p: offset, t: thread_id }
-			# @rangesTracker.applyOp op # Will apply via sharejs
-			@$scope.sharejsDoc.submitOp op
-
-		addCommentToSelection: (thread_id, offset, length) ->
-			start = @_shareJsOffsetToAcePosition(offset)
-			end = @_shareJsOffsetToAcePosition(offset + length)
-			range = new Range(start.row, start.column, end.row, end.column)
-			content = @editor.session.getTextRange(range)
-			@addComment(offset, content, thread_id)
-
-		selectLineIfNoSelection: () ->
-			if @editor.selection.isEmpty()
-				@editor.selection.selectLine()
-
-		acceptChangeIds: (change_ids) ->
-			@rangesTracker.removeChangeIds(change_ids)
-			@updateAnnotations()
-			@updateFocus()
-
-		rejectChangeIds: (change_ids) ->
-			changes = @rangesTracker.getChanges(change_ids)
-			return if changes.length == 0
-
-			# When doing bulk rejections, adjacent changes might interact with each other.
-			# Consider an insertion with an adjacent deletion (which is a common use-case, replacing words):
-			#
-			#     "foo bar baz" -> "foo quux baz"
-			#
-			# The change above will be modeled with two ops, with the insertion going first:
-			#
-			#     foo quux baz
-			#         |--| -> insertion of "quux", op 1, at position 4
-			#             | -> deletion of "bar", op 2, pushed forward by "quux" to position 8
-			#
-			# When rejecting these changes at once, if the insertion is rejected first, we get unexpected
-			# results. What happens is:
-			#
-			#     1) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars
-			#        starting from position 4;
-			#
-			#           "foo quux baz" -> "foo  baz"
-			#                |--| -> 4 characters to be removed
-			#
-			#     2) Rejecting the deletion adds the deleted word "bar" at position 8 (i.e. it will act as if
-			#        the word "quuux" was still present).
-			#
-			#            "foo  baz" -> "foo  bazbar"
-			#                     | -> deletion of "bar" is reverted by reinserting "bar" at position 8
-			#
-			# While the intended result would be "foo bar baz", what we get is:
-			#
-			#      "foo  bazbar" (note "bar" readded at position 8)
-			#
-			# The issue happens because of step 1. To revert the insertion of "quux", 4 characters are deleted
-			# from position 4. This includes the position where the deletion exists; when that position is
-			# cleared, the RangesTracker considers that the deletion is gone and stops tracking/updating it.
-			# As we still hold a reference to it, the code tries to revert it by readding the deleted text, but
-			# does so at the outdated position (position 8, which was valid when "quux" was present).
-			#
-			# To avoid this kind of problem, we need to make sure that reverting operations doesn't affect
-			# subsequent operations that come after. Reverse sorting the operations based on position will
-			# achieve it; in the case above, it makes sure that the the deletion is reverted first:
-			#
-			#     1) Rejecting the deletion adds the deleted word "bar" at position 8
-			#
-			#            "foo quux baz" -> "foo quuxbar baz"
-			#                                       | -> deletion of "bar" is reverted by
-			#                                            reinserting "bar" at position 8
-			#
-			#     2) Rejecting the insertion deletes the added word "quux", i.e., it removes 4 chars
-			#        starting from position 4 and achieves the expected result:
-			#
-			#           "foo quuxbar baz" -> "foo bar baz"
-			#                |--| -> 4 characters to be removed
-
-			changes.sort((a, b) -> b.op.p - a.op.p)
-
-			session = @editor.getSession()
-			for change in changes
-				if change.op.d?
-					content = change.op.d
-					position = @_shareJsOffsetToAcePosition(change.op.p)
-					session.$fromReject = true # Tell track changes to cancel out delete
-					session.insert(position, content)
-					session.$fromReject = false
-				else if change.op.i?
-					start = @_shareJsOffsetToAcePosition(change.op.p)
-					end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length)
-					editor_text = session.getDocument().getTextRange({start, end})
-					if editor_text != change.op.i
-						throw new Error("Op to be removed (#{JSON.stringify(change.op)}), does not match editor text, '#{editor_text}'")
-					session.$fromReject = true
-					session.remove({start, end})
-					session.$fromReject = false
-				else
-					throw new Error("unknown change: #{JSON.stringify(change)}")
-			setTimeout () => @updateFocus()
-
-		removeCommentId: (comment_id) ->
-			@rangesTracker.removeCommentId(comment_id)
-			@updateAnnotations()
-
-		hideCommentsByThreadIds: (thread_ids) ->
-			resolve_ids = {}
-			for id in thread_ids
-				resolve_ids[id] = true
-			for comment in @rangesTracker?.comments or []
-				if resolve_ids[comment.op.t]
-					@_onCommentRemoved(comment)
-			@broadcastChange()
-
-		showCommentByThreadId: (thread_id) ->
-			for comment in @rangesTracker?.comments or []
-				if comment.op.t == thread_id
-					@_onCommentAdded(comment)
-			@broadcastChange()
-
-		_resetCutState: () ->
-			@_cutState = {
-				text: null
-				comments: []
-				docId: null
-			}
-
-		onCut: () ->
-			@_resetCutState()
-			selection = @editor.getSelectionRange()
-			selection_start = @_aceRangeToShareJs(selection.start)
-			selection_end = @_aceRangeToShareJs(selection.end)
-			@_cutState.text = @editor.getSelectedText()
-			@_cutState.docId = @$scope.docId
-			for comment in @rangesTracker.comments
-				comment_start = comment.op.p
-				comment_end = comment_start + comment.op.c.length
-				if selection_start <= comment_start and comment_end <= selection_end
-					@_cutState.comments.push {
-						offset: comment.op.p - selection_start
-						text: comment.op.c
-						comment: comment
-					}
-
-		onPaste: () =>
-			@editor.once "change", (change) =>
-				return if change.action != "insert"
-				pasted_text = change.lines.join("\n")
-				paste_offset = @_aceRangeToShareJs(change.start)
-				# We have to wait until the change has been processed by the range tracker,
-				# since if we move the ops into place beforehand, they will be moved again
-				# when the changes are processed by the range tracker. This ranges:dirty
-				# event is fired after the doc has applied the changes to the range tracker.
-				@$scope.sharejsDoc.on "ranges:dirty.paste", () =>
-					@$scope.sharejsDoc.off "ranges:dirty.paste" # Doc event emitter uses namespaced events
-					if pasted_text == @_cutState.text and @$scope.docId == @_cutState.docId
-						for {comment, offset, text} in @_cutState.comments
-							op = { c: text, p: paste_offset + offset, t: comment.id }
-							@$scope.sharejsDoc.submitOp op # Resubmitting an existing comment op (by thread id) will move it
-					@_resetCutState()
-					# Check that comments still match text. Will throw error if not.
-					@rangesTracker.validate(@editor.getValue())
-
-		checkMapping: () ->
-			# TODO: reintroduce this check
-			session = @editor.getSession()
-
-			# Make a copy of session.getMarkers() so we can modify it
-			markers = {}
-			for marker_id, marker of session.getMarkers()
-				markers[marker_id] = marker
-
-			expected_markers = []
-			for change in @rangesTracker.changes
-				if @changeIdToMarkerIdMap[change.id]?
-					op = change.op
-					{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id]
-					start = @_shareJsOffsetToAcePosition(op.p)
-					if op.i?
-						end = @_shareJsOffsetToAcePosition(op.p + op.i.length)
-					else if op.d?
-						end = start
-					expected_markers.push { marker_id: background_marker_id, start, end }
-					expected_markers.push { marker_id: callout_marker_id, start, end: start }
-
-			for comment in @rangesTracker.comments
-				if @changeIdToMarkerIdMap[comment.id]?
-					{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[comment.id]
-					start = @_shareJsOffsetToAcePosition(comment.op.p)
-					end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length)
-					expected_markers.push { marker_id: background_marker_id, start, end }
-					expected_markers.push { marker_id: callout_marker_id, start, end: start }
-
-			for {marker_id, start, end} in expected_markers
-				marker = markers[marker_id]
-				delete markers[marker_id]
-				if marker.range.start.row != start.row or
-						marker.range.start.column != start.column or
-						marker.range.end.row != end.row or
-						marker.range.end.column != end.column
-					console.error "Change doesn't match marker anymore", {change, marker, start, end}
-
-			for marker_id, marker of markers
-				if /track-changes/.test(marker.clazz)
-					console.error "Orphaned ace marker", marker
-
-		updateFocus: () ->
-			selection = @editor.getSelectionRange()
-			selection_start = @_aceRangeToShareJs(selection.start)
-			selection_end = @_aceRangeToShareJs(selection.end)
-			entries = @_getCurrentDocEntries()
-			is_selection = (selection_start != selection_end)
-			@$scope.$emit "editor:focus:changed", selection_start, selection_end, is_selection
-
-		broadcastChange: () ->
-			@$scope.$emit "editor:track-changes:changed", @$scope.docId
-
-		recalculateReviewEntriesScreenPositions: () ->
-			session = @editor.getSession()
-			renderer = @editor.renderer
-			{firstRow, lastRow} = renderer.layerConfig
-			entries = @_getCurrentDocEntries()
-			for entry_id, entry of entries or {}
-				doc_position = @_shareJsOffsetToAcePosition(entry.offset)
-				screen_position = session.documentToScreenPosition(doc_position.row, doc_position.column)
-				y = screen_position.row * renderer.lineHeight
-				entry.screenPos ?= {}
-				entry.screenPos.y = y
-				entry.docPos = doc_position
-			@recalculateVisibleEntries()
-			@$scope.$apply()
-
-		recalculateVisibleEntries: () ->
-			OFFSCREEN_ROWS = 20
-			CULL_AFTER = 100 # With less than this number of entries, don't bother culling to avoid little UI jumps when scrolling.
-			{firstRow, lastRow} = @editor.renderer.layerConfig
-			entries = @_getCurrentDocEntries() or {}
-			entriesLength = Object.keys(entries).length
-			changed = false
-			for entry_id, entry of entries
-				old = entry.visible
-				entry.visible = (entriesLength < CULL_AFTER) or (firstRow - OFFSCREEN_ROWS <= entry.docPos.row <= lastRow + OFFSCREEN_ROWS)
-				if (entry.visible != old)
-					changed = true
-			if changed
-				@$scope.$emit "editor:track-changes:visibility_changed"
-
-		_getCurrentDocEntries: () ->
-			doc_id = @$scope.docId
-			entries = @$scope.reviewPanel.entries[doc_id] ?= {}
-			return entries
-
-		_makeZeroWidthRange: (position) ->
-			ace_range = new Range(position.row, position.column, position.row, position.column)
-			# Our delete marker is zero characters wide, but Ace doesn't draw ranges
-			# that are empty. So we monkey patch the range to tell Ace it's not empty.
-			# We do want to claim to be empty if we're off screen after clipping rows though.
-			# This is the code we need to trick:
-			#   var range = marker.range.clipRows(config.firstRow, config.lastRow);
-			#   if (range.isEmpty()) continue;
-			ace_range.clipRows = (first_row, last_row) ->
-				@isEmpty = () ->
-					first_row > @end.row or last_row < @start.row
-				return @
-			return ace_range
-
-		_createCalloutMarker: (position, klass) ->
-			session = @editor.getSession()
-			callout_range = @_makeZeroWidthRange(position)
-			markerLayer = @editor.renderer.$markerBack
-			callout_marker_id = session.addMarker callout_range, klass, (html, range, left, top, config) ->
-				markerLayer.drawSingleLineMarker(html, range, "track-changes-marker-callout #{klass} ace_start", config, 0, "width: auto; right: 0;")
-
-		_onInsertAdded: (change) ->
-			start = @_shareJsOffsetToAcePosition(change.op.p)
-			end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length)
-			session = @editor.getSession()
-			doc = session.getDocument()
-			background_range = new Range(start.row, start.column, end.row, end.column)
-			background_marker_id = session.addMarker background_range, "track-changes-marker track-changes-added-marker", "text"
-			callout_marker_id = @_createCalloutMarker(start, "track-changes-added-marker-callout")
-			@changeIdToMarkerIdMap[change.id] = { background_marker_id, callout_marker_id }
-
-		_onDeleteAdded: (change) ->
-			position = @_shareJsOffsetToAcePosition(change.op.p)
-			session = @editor.getSession()
-			doc = session.getDocument()
-
-			markerLayer = @editor.renderer.$markerBack
-			klass = "track-changes-marker track-changes-deleted-marker"
-			background_range = @_makeZeroWidthRange(position)
-			background_marker_id = session.addMarker background_range, klass, (html, range, left, top, config) ->
-				markerLayer.drawSingleLineMarker(html, range, "#{klass} ace_start", config, 0, "")
-
-			callout_marker_id = @_createCalloutMarker(position, "track-changes-deleted-marker-callout")
-			@changeIdToMarkerIdMap[change.id] = { background_marker_id, callout_marker_id }
-
-		_onInsertRemoved: (change) ->
-			{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id]
-			delete @changeIdToMarkerIdMap[change.id]
-			session = @editor.getSession()
-			session.removeMarker background_marker_id
-			session.removeMarker callout_marker_id
-
-		_onDeleteRemoved: (change) ->
-			{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change.id]
-			delete @changeIdToMarkerIdMap[change.id]
-			session = @editor.getSession()
-			session.removeMarker background_marker_id
-			session.removeMarker callout_marker_id
-
-		_onCommentAdded: (comment) ->
-			if @rangesTracker.resolvedThreadIds[comment.op.t]
-				# Comment is resolved so shouldn't be displayed.
-				return
-			if !@changeIdToMarkerIdMap[comment.id]?
-				# Only create new markers if they don't already exist
-				start = @_shareJsOffsetToAcePosition(comment.op.p)
-				end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length)
-				session = @editor.getSession()
-				doc = session.getDocument()
-				background_range = new Range(start.row, start.column, end.row, end.column)
-				background_marker_id = session.addMarker background_range, "track-changes-marker track-changes-comment-marker", "text"
-				callout_marker_id = @_createCalloutMarker(start, "track-changes-comment-marker-callout")
-				@changeIdToMarkerIdMap[comment.id] = { background_marker_id, callout_marker_id }
-
-		_onCommentRemoved: (comment) ->
-			if @changeIdToMarkerIdMap[comment.id]?
-				# Resolved comments may not have marker ids
-				{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[comment.id]
-				delete @changeIdToMarkerIdMap[comment.id]
-				session = @editor.getSession()
-				session.removeMarker background_marker_id
-				session.removeMarker callout_marker_id
-
-		_aceRangeToShareJs: (range) ->
-			lines = @editor.getSession().getDocument().getLines 0, range.row
-			return AceShareJsCodec.aceRangeToShareJs(range, lines)
-
-		_aceChangeToShareJs: (delta) ->
-			lines = @editor.getSession().getDocument().getLines 0, delta.start.row
-			return AceShareJsCodec.aceChangeToShareJs(delta, lines)
-
-		_shareJsOffsetToAcePosition: (offset) ->
-			lines = @editor.getSession().getDocument().getAllLines()
-			return AceShareJsCodec.shareJsOffsetToAcePosition(offset, lines)
-
-		_onChangeMoved: (change) ->
-			start = @_shareJsOffsetToAcePosition(change.op.p)
-			if change.op.i?
-				end = @_shareJsOffsetToAcePosition(change.op.p + change.op.i.length)
-			else
-				end = start
-			@_updateMarker(change.id, start, end)
-
-		_onCommentMoved: (comment) ->
-			start = @_shareJsOffsetToAcePosition(comment.op.p)
-			end = @_shareJsOffsetToAcePosition(comment.op.p + comment.op.c.length)
-			@_updateMarker(comment.id, start, end)
-
-		_updateMarker: (change_id, start, end) ->
-			return if !@changeIdToMarkerIdMap[change_id]?
-			session = @editor.getSession()
-			markers = session.getMarkers()
-			{background_marker_id, callout_marker_id} = @changeIdToMarkerIdMap[change_id]
-			if background_marker_id? and markers[background_marker_id]?
-				background_marker = markers[background_marker_id]
-				background_marker.range.start = start
-				background_marker.range.end = end
-			if callout_marker_id? and markers[callout_marker_id]?
-				callout_marker = markers[callout_marker_id]
-				callout_marker.range.start = start
-				callout_marker.range.end = start

+ 0 - 367
services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee

@@ -1,367 +0,0 @@
-define [
-	"ace/ace"
-], () ->
-	Range = ace.require("ace/range").Range
-	EditSession = ace.require("ace/edit_session").EditSession
-	Doc = ace.require("ace/document").Document
-
-	class UndoManager
-		constructor: (@$scope, @editor) ->
-			@$scope.undo =
-				show_remote_warning: false
-				
-			@reset()
-
-			@editor.on "changeSession", (e) =>
-				@reset()
-				@session = e.session
-				e.session.setUndoManager(@)
-
-		showUndoConflictWarning: () ->
-			@$scope.$apply () =>
-				@$scope.undo.show_remote_warning = true
-
-			setTimeout () => 
-				@$scope.$apply () =>
-					@$scope.undo.show_remote_warning = false
-			, 4000
-
-		reset: () ->
-			@firstUpdate = true
-			@undoStack = []
-			@redoStack = []
-
-		execute: (options) ->
-			if @firstUpdate
-				# The first update we receive is Ace setting the document, which we should
-				# ignore
-				@firstUpdate = false
-				return
-			aceDeltaSets = options.args[0]
-			return if !aceDeltaSets?
-			@session = options.args[1]
-
-			# We need to split the delta sets into local or remote groups before pushing onto
-			# the undo stack, since these are treated differently.
-			splitDeltaSets = []
-			currentDeltaSet = null # Make global to this function
-			do newDeltaSet = () ->
-				currentDeltaSet = {group: "doc", deltas: []}
-				splitDeltaSets.push currentDeltaSet
-			currentRemoteState = null
-
-			for deltaSet in aceDeltaSets or []
-				if deltaSet.group == "doc" # ignore code folding etc.
-					for delta in deltaSet.deltas
-						if currentDeltaSet.remote? and currentDeltaSet.remote != !!delta.remote
-							newDeltaSet()
-						currentDeltaSet.deltas.push delta
-						currentDeltaSet.remote = !!delta.remote
-
-			# The lines are currently as they are after applying all these deltas, but to turn into simple deltas,
-			# we need the lines before each delta group.
-			docLines = @session.getDocument().getAllLines()
-			docLines = @_revertAceDeltaSetsOnDocLines(aceDeltaSets, docLines)
-			for deltaSet in splitDeltaSets
-				{simpleDeltaSet, docLines}  = @_aceDeltaSetToSimpleDeltaSet(deltaSet, docLines)
-				frame = {
-					deltaSets: [simpleDeltaSet]
-					remote: deltaSet.remote
-				}
-				@undoStack.push frame
-			@redoStack = []
-
-		undo: (dontSelect) ->
-			# We rely on the doclines being in sync with the undo stack, so make sure
-			# any pending undo deltas are processed.
-			@session.$syncInformUndoManager()
-
-			localUpdatesMade = @_shiftLocalChangeToTopOfUndoStack()
-			return if !localUpdatesMade
-
-			update = @undoStack.pop()
-			return if !update?
-
-			if update.remote
-				@showUndoConflictWarning()
-
-			lines = @session.getDocument().getAllLines()
-			linesBeforeDelta = @_revertSimpleDeltaSetsOnDocLines(update.deltaSets, lines)
-			deltaSets = @_simpleDeltaSetsToAceDeltaSets(update.deltaSets, linesBeforeDelta)
-			selectionRange = @session.undoChanges(deltaSets, dontSelect)
-			@redoStack.push(update)
-			return selectionRange
-		
-		redo: (dontSelect) ->
-			update = @redoStack.pop()
-			return if !update?
-			lines = @session.getDocument().getAllLines()
-			deltaSets = @_simpleDeltaSetsToAceDeltaSets(update.deltaSets, lines)
-			selectionRange = @session.redoChanges(deltaSets, dontSelect)
-			@undoStack.push(update)
-			return selectionRange
-
-		_shiftLocalChangeToTopOfUndoStack: () ->
-			head = []
-			localChangeExists = false
-			while @undoStack.length > 0
-				update = @undoStack.pop()
-				head.unshift update
-				if !update.remote
-					localChangeExists = true
-					break
-
-			if !localChangeExists
-				@undoStack = @undoStack.concat head
-				return false
-			else
-				# Undo stack looks like undoStack ++ reorderedhead ++ head
-				# Reordered head starts of empty and consumes entries from head
-				# while keeping the localChange at the top for as long as it can
-				localChange = head.shift()
-				reorderedHead = [localChange]
-				while head.length > 0
-					remoteChange = head.shift()
-					localChange = reorderedHead.pop()
-					result = @_swapSimpleDeltaSetsOrder(localChange.deltaSets, remoteChange.deltaSets)
-					if result?
-						remoteChange.deltaSets = result[0]
-						localChange.deltaSets = result[1]
-						reorderedHead.push remoteChange
-						reorderedHead.push localChange
-					else
-						reorderedHead.push localChange
-						reorderedHead.push remoteChange
-						break
-				@undoStack = @undoStack.concat(reorderedHead).concat(head)
-				return true
-				
-
-		_swapSimpleDeltaSetsOrder: (firstDeltaSets, secondDeltaSets) ->
-			newFirstDeltaSets = @_copyDeltaSets(firstDeltaSets)
-			newSecondDeltaSets = @_copyDeltaSets(secondDeltaSets)
-			for firstDeltaSet in newFirstDeltaSets.slice(0).reverse()
-				for firstDelta in firstDeltaSet.deltas.slice(0).reverse()
-					for secondDeltaSet in newSecondDeltaSets
-						for secondDelta in secondDeltaSet.deltas
-							success = @_swapSimpleDeltaOrderInPlace(firstDelta, secondDelta)
-							return null if !success
-			return [newSecondDeltaSets, newFirstDeltaSets]
-
-		_copyDeltaSets: (deltaSets) ->
-			newDeltaSets = []
-			for deltaSet in deltaSets
-				newDeltaSet =
-					deltas: []
-					group: deltaSet.group
-				newDeltaSets.push newDeltaSet
-				for delta in deltaSet.deltas
-					newDelta =
-						position: delta.position
-					newDelta.insert = delta.insert if delta.insert?
-					newDelta.remove = delta.remove if delta.remove?
-					newDeltaSet.deltas.push newDelta
-			return newDeltaSets
-
-		_swapSimpleDeltaOrderInPlace: (firstDelta, secondDelta) ->
-			result = @_swapSimpleDeltaOrder(firstDelta, secondDelta)
-			return false if !result?
-			firstDelta.position = result[1].position
-			secondDelta.position = result[0].position
-			return true
-
-		_swapSimpleDeltaOrder: (firstDelta, secondDelta) ->
-			if firstDelta.insert? and secondDelta.insert?
-				if secondDelta.position >= firstDelta.position + firstDelta.insert.length
-					secondDelta.position -= firstDelta.insert.length
-					return [secondDelta, firstDelta]
-				else if secondDelta.position > firstDelta.position
-					return null
-				else
-					firstDelta.position += secondDelta.insert.length
-					return [secondDelta, firstDelta]
-			else if firstDelta.remove? and secondDelta.remove?
-				if secondDelta.position >= firstDelta.position
-					secondDelta.position += firstDelta.remove.length
-					return [secondDelta, firstDelta]
-				else if secondDelta.position + secondDelta.remove.length > firstDelta.position
-					return null
-				else
-					firstDelta.position -= secondDelta.remove.length
-					return [secondDelta, firstDelta]
-			else if firstDelta.insert? and secondDelta.remove?
-				if secondDelta.position >= firstDelta.position + firstDelta.insert.length
-					secondDelta.position -= firstDelta.insert.length
-					return [secondDelta, firstDelta]
-				else if secondDelta.position + secondDelta.remove.length > firstDelta.position
-					return null
-				else
-					firstDelta.position -= secondDelta.remove.length
-					return [secondDelta, firstDelta]
-			else if firstDelta.remove? and secondDelta.insert?
-				if secondDelta.position >= firstDelta.position
-					secondDelta.position += firstDelta.remove.length
-					return [secondDelta, firstDelta]
-				else
-					firstDelta.position += secondDelta.insert.length
-					return [secondDelta, firstDelta]
-			else
-				throw "Unknown delta types"
-
-		_applyAceDeltasToDocLines: (deltas, docLines) ->
-			doc = new Doc(docLines.join("\n"))
-			doc.applyDeltas(deltas)
-			return doc.getAllLines()
-
-		_revertAceDeltaSetsOnDocLines: (deltaSets, docLines) ->
-			session = new EditSession(docLines.join("\n"))
-			session.undoChanges(deltaSets)
-			return session.getDocument().getAllLines()
-
-		_revertSimpleDeltaSetsOnDocLines: (deltaSets, docLines) ->
-			doc = docLines.join("\n")
-			for deltaSet in deltaSets.slice(0).reverse()
-				for delta in deltaSet.deltas.slice(0).reverse()
-					if delta.remove?
-						doc = doc.slice(0, delta.position) + delta.remove + doc.slice(delta.position)
-					else if delta.insert?
-						doc = doc.slice(0, delta.position) + doc.slice(delta.position + delta.insert.length)
-					else
-						throw "Unknown delta type"
-			return doc.split("\n")
-
-		_aceDeltaSetToSimpleDeltaSet: (deltaSet, docLines) ->
-			simpleDeltas = []
-			for delta in deltaSet.deltas
-				simpleDeltas.push @_aceDeltaToSimpleDelta(delta, docLines)
-				docLines = @_applyAceDeltasToDocLines([delta], docLines)
-			simpleDeltaSet = {
-				deltas: simpleDeltas
-				group: deltaSet.group
-			}
-			return {simpleDeltaSet, docLines} 
-
-		_simpleDeltaSetsToAceDeltaSets: (simpleDeltaSets, docLines) ->
-			for deltaSet in simpleDeltaSets
-				aceDeltas = []
-				for delta in deltaSet.deltas
-					newAceDeltas = @_simpleDeltaToAceDeltas(delta, docLines)
-					docLines = @_applyAceDeltasToDocLines(newAceDeltas, docLines)
-					aceDeltas = aceDeltas.concat newAceDeltas
-				{
-					deltas: aceDeltas
-					group: deltaSet.group
-				}
-
-		_aceDeltaToSimpleDelta: (aceDelta, docLines) ->
-			start = aceDelta.start
-			if !start?
-				JSONstringifyWithCycles = (o) ->
-					seen = []
-					return JSON.stringify o, (k,v) ->
-						if (typeof v == 'object')
-							if ( seen.indexOf(v) >= 0 )
-								return '__cycle__'
-							seen.push(v);
-						return v
-				error = new Error("aceDelta had no start event: #{JSONstringifyWithCycles(aceDelta)}")
-				throw error
-			linesBefore = docLines.slice(0, start.row)
-			position =
-				linesBefore.join("").length + # full lines
-				linesBefore.length + # new line characters
-				start.column # partial line
-			switch aceDelta.action
-				when "insert"
-					simpleDelta = {
-						position: position
-						insert: aceDelta.lines.join("\n")
-					}
-				when "remove"
-					simpleDelta = {
-						position: position
-						remove: aceDelta.lines.join("\n")
-					}
-				else
-					throw new Error("Unknown Ace action: #{aceDelta.action}")
-			return simpleDelta
-
-		_simplePositionToAcePosition: (position, docLines) ->
-			column = 0
-			row = 0
-			for line in docLines
-				if position > line.length
-					row++
-					position -= (line + "\n").length
-				else
-					column = position
-					break
-			return {row: row, column: column}
-
-		_simpleDeltaToAceDeltas: (simpleDelta, docLines) ->
-			{row, column} = @_simplePositionToAcePosition(simpleDelta.position, docLines)
-
-			lines = (simpleDelta.insert or simpleDelta.remove or "").split("\n")
-			
-			start = {column, row}
-			if lines.length > 1
-				end = {
-					row: row + lines.length - 1,
-					column: lines[lines.length - 1].length
-				}
-			else
-				end = {
-					row,
-					column: column + lines[0].length
-				}
-			
-			if simpleDelta.insert?
-				aceDelta = {
-					action: "insert"
-					lines, start, end
-				}
-			else if simpleDelta.remove?
-				aceDelta = {
-					action: "remove"
-					lines, start, end
-				}
-			else
-				throw "Unknown simple delta: #{simpleDelta}"
-
-			return [aceDelta]
-
-		_concatSimpleDeltas: (deltas) ->
-			return [] if deltas.length == 0
-
-			concattedDeltas = []
-			previousDelta = deltas.shift()
-			for delta in deltas
-				if delta.insert? and previousDelta.insert?
-					if previousDelta.position + previousDelta.insert.length == delta.position
-						previousDelta =
-							insert: previousDelta.insert + delta.insert
-							position: previousDelta.position
-					else
-						concattedDeltas.push previousDelta
-						previousDelta = delta
-
-				else if delta.remove? and previousDelta.remove?
-					if previousDelta.position == delta.position
-						previousDelta =
-							remove: previousDelta.remove + delta.remove
-							position: delta.position
-					else
-						concattedDeltas.push previousDelta
-						previousDelta = delta
-				else
-					concattedDeltas.push previousDelta
-					previousDelta = delta
-			concattedDeltas.push previousDelta
-					
-
-			return concattedDeltas
-				
-
-		hasUndo: () -> @undoStack.length > 0
-		hasRedo: () -> @redoStack.length > 0
-		

+ 0 - 3
services/web/public/coffee/ide/editor/sharejs/header.coffee

@@ -1,3 +0,0 @@
-WEB = true
-window.sharejs = exports = {}
-types = exports.types = {}

+ 0 - 157
services/web/public/coffee/ide/editor/sharejs/vendor/client/ace.coffee

@@ -1,157 +0,0 @@
-# This is some utility code to connect an ace editor to a sharejs document.
-
-Range = ace.require("ace/range").Range
-
-# Convert an ace delta into an op understood by share.js
-applyAceToShareJS = (editorDoc, delta, doc, fromUndo) ->
-  # Get the start position of the range, in no. of characters
-  getStartOffsetPosition = (start) ->
-    # This is quite inefficient - getLines makes a copy of the entire
-    # lines array in the document. It would be nice if we could just
-    # access them directly.
-    lines = editorDoc.getLines 0, start.row
-      
-    offset = 0
-
-    for line, i in lines
-      offset += if i < start.row
-        line.length
-      else
-        start.column
-
-    # Add the row number to include newlines.
-    offset + start.row
-
-  pos = getStartOffsetPosition(delta.start)
-
-  switch delta.action
-    when 'insert'
-      text = delta.lines.join('\n')
-      doc.insert pos, text, fromUndo
-      
-    when 'remove'
-      text = delta.lines.join('\n')
-      doc.del pos, text.length, fromUndo
-
-    else throw new Error "unknown action: #{delta.action}"
-  
-  return
-
-# Attach an ace editor to the document. The editor's contents are replaced
-# with the document's contents unless keepEditorContents is true. (In which case the document's
-# contents are nuked and replaced with the editor's).
-window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents, maxDocLength) ->
-  throw new Error 'Only text documents can be attached to ace' unless @provides['text']
-
-  doc = this
-  editorDoc = editor.getSession().getDocument()
-  editorDoc.setNewLineMode 'unix'
-
-  check = ->
-    window.setTimeout ->
-        editorText = editorDoc.getValue()
-        otText = doc.getText()
-
-        if editorText != otText
-          console.error "Text does not match!"
-          console.error "editor: #{editorText}"
-          console.error "ot:     #{otText}"
-          # Should probably also replace the editor text with the doc snapshot.
-      , 0
-
-  if keepEditorContents
-    doc.del 0, doc.getText().length
-    doc.insert 0, editorDoc.getValue()
-  else
-    editorDoc.setValue doc.getText()
-
-  check()
-
-  # When we apply ops from sharejs, ace emits edit events. We need to ignore those
-  # to prevent an infinite typing loop.
-  suppress = false
-  
-  # Listen for edits in ace
-  editorListener = (change) ->
-    return if suppress
-    
-    if maxDocLength? and editorDoc.getValue().length > maxDocLength
-        doc.emit "error", new Error("document length is greater than maxDocLength")
-        return
-
-    fromUndo = !!(editor.getSession().$fromUndo or editor.getSession().$fromReject)
-    
-    applyAceToShareJS editorDoc, change, doc, fromUndo
-
-    check()
-
-  editorDoc.on 'change', editorListener
-
-  # Horribly inefficient.
-  offsetToPos = (offset) ->
-    # Again, very inefficient.
-    lines = editorDoc.getAllLines()
-
-    row = 0
-    for line, row in lines
-      break if offset <= line.length
-
-      # +1 for the newline.
-      offset -= lines[row].length + 1
-
-    row:row, column:offset
-
-  # We want to insert a remote:true into the delta if the op comes from the
-  # underlying sharejs doc (which means it is from a remote op), so we have to do
-  # the work of editorDoc.insert and editorDoc.remove manually. These methods are
-  # copied from ace.js doc#insert and #remove, and then inject the remote:true
-  # flag into the delta.
-  onInsert = (pos, text) ->
-    if (editorDoc.getLength() <= 1)
-        editorDoc.$detectNewLine(text)
-
-    lines = editorDoc.$split(text)
-    position = offsetToPos(pos)
-    start = editorDoc.clippedPos(position.row, position.column)
-    end = {
-        row: start.row + lines.length - 1,
-        column: (if lines.length == 1 then start.column else 0) + lines[lines.length - 1].length
-    }
-
-    suppress = true
-    editorDoc.applyDelta({
-        start: start,
-        end: end,
-        action: "insert",
-        lines: lines,
-        remote: true
-    });
-    suppress = false
-    check()
-
-  onDelete = (pos, text) ->
-    range = Range.fromPoints offsetToPos(pos), offsetToPos(pos + text.length)
-    start = editorDoc.clippedPos(range.start.row, range.start.column)
-    end = editorDoc.clippedPos(range.end.row, range.end.column)
-    suppress = true
-    editorDoc.applyDelta({
-        start: start,
-        end: end,
-        action: "remove",
-        lines: editorDoc.getLinesForRange({start: start, end: end})
-        remote: true
-    });
-    suppress = false
-    check()
-
-  doc.on 'insert', onInsert
-  doc.on 'delete', onDelete
-
-  doc.detach_ace = ->
-    doc.removeListener 'insert', onInsert
-    doc.removeListener 'delete', onDelete
-    editorDoc.removeListener 'change', editorListener
-    delete doc.detach_ace
-
-  return
-

+ 0 - 89
services/web/public/coffee/ide/editor/sharejs/vendor/client/cm.coffee

@@ -1,89 +0,0 @@
-# This is some utility code to connect a CodeMirror editor
-# to a sharejs document.
-# It is heavily inspired from the Ace editor hook.
-
-# Convert a CodeMirror delta into an op understood by share.js
-applyCMToShareJS = (editorDoc, delta, doc) ->
-  # CodeMirror deltas give a text replacement.
-  # I tuned this operation a little bit, for speed.
-  startPos = 0  # Get character position from # of chars in each line.
-  i = 0         # i goes through all lines.
-
-  while i < delta.from.line
-    startPos += editorDoc.lineInfo(i).text.length + 1   # Add 1 for '\n'
-    i++
-  startPos += delta.from.ch
-
-  doc.del startPos, delta.removed.join('\n').length if delta.removed
-  doc.insert startPos, delta.text.join('\n') if delta.text
-
-# Attach a CodeMirror editor to the document. The editor's contents are replaced
-# with the document's contents unless keepEditorContents is true. (In which case
-# the document's contents are nuked and replaced with the editor's).
-window.sharejs.extendDoc 'attach_cm', (editor, keepEditorContents) ->
-  unless @provides.text
-    throw new Error 'Only text documents can be attached to CodeMirror2'
-
-  sharedoc = @
-  editorDoc = editor.getDoc()
-
-  check = ->
-    window.setTimeout ->
-        editorText = editor.getValue()
-        otText = sharedoc.getText()
-
-        if editorText != otText
-          console.error "Text does not match!"
-          console.error "editor: #{editorText}"
-          console.error "ot:     #{otText}"
-          # Removed editor.setValue here as it would cause recursive loops if
-          # consistency check failed - because setting the value would trigger
-          # the change event
-      , 0
-
-  if keepEditorContents
-    @del 0, sharedoc.getText().length
-    @insert 0, editor.getValue()
-  else
-    editor.setValue sharedoc.getText()
-
-  check()
-
-  # When we apply ops from sharejs, CodeMirror emits edit events.
-  # We need to ignore those to prevent an infinite typing loop.
-  suppress = false
-
-  # Listen for edits in CodeMirror.
-  editorListener = (ed, change) ->
-    return if suppress
-    applyCMToShareJS editorDoc, change, sharedoc
-    check()
-
-  editorDoc.on 'change', editorListener
-
-  onInsert = (pos, text) ->
-    suppress = true
-    # All the primitives we need are already in CM's API.
-    editor.replaceRange text, editor.posFromIndex(pos)
-    suppress = false
-    check()
-
-  onDelete = (pos, text) ->
-    suppress = true
-    from = editor.posFromIndex pos
-    to = editor.posFromIndex (pos + text.length)
-    editor.replaceRange '', from, to
-    suppress = false
-    check()
-
-  @on 'insert', onInsert
-  @on 'delete', onDelete
-
-  @detach_cm = ->
-    @removeListener 'insert', onInsert
-    @removeListener 'delete', onDelete
-    editorDoc.off 'change', editorListener
-    delete @detach_cm
-
-  return
-

+ 0 - 167
services/web/public/coffee/ide/editor/sharejs/vendor/client/connection.coffee

@@ -1,167 +0,0 @@
-# A Connection wraps a persistant BC connection to a sharejs server.
-#
-# This class implements the client side of the protocol defined here:
-# https://github.com/josephg/ShareJS/wiki/Wire-Protocol
-#
-# The equivalent server code is in src/server/browserchannel.coffee.
-#
-# This file is a bit of a mess. I'm dreadfully sorry about that. It passes all the tests,
-# so I have hope that its *correct* even if its not clean.
-#
-# Most of Connection exists to support the open() method, which creates a new document
-# reference.
-
-if WEB?
-  types = exports.types
-  throw new Error 'Must load browserchannel before this library' unless window.BCSocket
-  {BCSocket} = window
-else
-  types = require '../types'
-  {BCSocket} = require 'browserchannel'
-  Doc = require('./doc').Doc
-
-class Connection
-  constructor: (host) ->
-    # Map of docname -> doc
-    @docs = {}
-
-    # States:
-    # - 'connecting': The connection is being established
-    # - 'handshaking': The connection has been established, but we don't have the auth ID yet
-    # - 'ok': We have connected and recieved our client ID. Ready for data.
-    # - 'disconnected': The connection is closed, but it will not reconnect automatically.
-    # - 'stopped': The connection is closed, and will not reconnect.
-    @state = 'connecting'
-    @socket = new BCSocket host, reconnect:true
-
-    @socket.onmessage = (msg) =>
-      if msg.auth is null
-        # Auth failed.
-        @lastError = msg.error # 'forbidden'
-        @disconnect()
-        return @emit 'connect failed', msg.error
-      else if msg.auth
-        # Our very own client id.
-        @id = msg.auth
-        @setState 'ok'
-        return
-
-      docName = msg.doc
-
-      if docName isnt undefined
-        @lastReceivedDoc = docName
-      else
-        msg.doc = docName = @lastReceivedDoc
-
-      if @docs[docName]
-        @docs[docName]._onMessage msg
-      else
-        console?.error 'Unhandled message', msg
-
-    @connected = false
-    @socket.onclose = (reason) =>
-      #console.warn 'onclose', reason
-      @setState 'disconnected', reason
-      if reason in ['Closed', 'Stopped by server']
-        @setState 'stopped', @lastError or reason
-
-    @socket.onerror = (e) =>
-      #console.warn 'onerror', e
-      @emit 'error', e
-
-    @socket.onopen = =>
-      #console.warn 'onopen'
-      @lastError = @lastReceivedDoc = @lastSentDoc = null
-      @setState 'handshaking'
-
-    @socket.onconnecting = =>
-      #console.warn 'connecting'
-      @setState 'connecting'
-
-  setState: (state, data) ->
-    return if @state is state
-    @state = state
-
-    delete @id if state is 'disconnected'
-    @emit state, data
-
-    # Documents could just subscribe to the state change events, but there's less state to
-    # clean up when you close a document if I just notify the doucments directly.
-    for docName, doc of @docs
-      doc._connectionStateChanged state, data
-
-  send: (data) ->
-    docName = data.doc
-
-    if docName is @lastSentDoc
-      delete data.doc
-    else
-      @lastSentDoc = docName
-
-    #console.warn 'c->s', data
-    @socket.send data
-
-  disconnect: ->
-    # This will call @socket.onclose(), which in turn will emit the 'disconnected' event.
-    #console.warn 'calling close on the socket'
-    @socket.close()
-
-  # *** Doc management
- 
-  makeDoc: (name, data, callback) ->
-    throw new Error("Doc #{name} already open") if @docs[name]
-    doc = new Doc(@, name, data)
-    @docs[name] = doc
-
-    doc.open (error) =>
-      delete @docs[name] if error
-      callback error, (doc unless error)
-
-  # Open a document that already exists
-  # callback(error, doc)
-  openExisting: (docName, callback) ->
-    return callback 'connection closed' if @state is 'stopped'
-    return callback null, @docs[docName] if @docs[docName]
-    doc = @makeDoc docName, {}, callback
-
-  # Open a document. It will be created if it doesn't already exist.
-  # Callback is passed a document or an error
-  # type is either a type name (eg 'text' or 'simple') or the actual type object.
-  # Types must be supported by the server.
-  # callback(error, doc)
-  open: (docName, type, callback) ->
-    return callback 'connection closed' if @state is 'stopped'
-
-    if typeof type is 'function'
-      callback = type
-      type = 'text'
-
-    callback ||= ->
-
-    type = types[type] if typeof type is 'string'
-
-    throw new Error "OT code for document type missing" unless type
-
-    throw new Error 'Server-generated random doc names are not currently supported' unless docName?
-
-    if @docs[docName]
-      doc = @docs[docName]
-      if doc.type == type
-        callback null, doc
-      else
-        callback 'Type mismatch', doc
-      return
-
-    @makeDoc docName, {create:true, type:type.name}, callback
-
-# Not currently working.
-#  create: (type, callback) ->
-#    open null, type, callback
-
-# Make connections event emitters.
-unless WEB?
-  MicroEvent = require './microevent'
-
-MicroEvent.mixin Connection
-
-exports.Connection = Connection

+ 0 - 349
services/web/public/coffee/ide/editor/sharejs/vendor/client/doc.coffee

@@ -1,349 +0,0 @@
-unless WEB?
-  types = require '../types'
-
-if WEB?
-  exports.extendDoc = (name, fn) ->
-    Doc::[name] = fn
-
-# A Doc is a client's view on a sharejs document.
-#
-# Documents are created by calling Connection.open().
-#
-# Documents are event emitters - use doc.on(eventname, fn) to subscribe.
-#
-# Documents get mixed in with their type's API methods. So, you can .insert('foo', 0) into
-# a text document and stuff like that.
-#
-# Events:
-#  - remoteop (op)
-#  - changed (op)
-#  - acknowledge (op)
-#  - error
-#  - open, closing, closed. 'closing' is not guaranteed to fire before closed.
-class Doc
-  # connection is a Connection object.
-  # name is the documents' docName.
-  # data can optionally contain known document data, and initial open() call arguments:
-  # {v[erson], snapshot={...}, type, create=true/false/undefined}
-  # callback will be called once the document is first opened.
-  constructor: (@connection, @name, openData) ->
-    # Any of these can be null / undefined at this stage.
-    openData ||= {}
-    @version = openData.v
-    @snapshot = openData.snaphot
-    @_setType openData.type if openData.type
-
-    @state = 'closed'
-    @autoOpen = false
-
-    # Has the document already been created?
-    @_create = openData.create
-
-    # The op that is currently roundtripping to the server, or null.
-    #
-    # When the connection reconnects, the inflight op is resubmitted.
-    @inflightOp = null
-    @inflightCallbacks = []
-    # The auth ids which the client has previously used to attempt to send inflightOp. This is
-    # usually empty.
-    @inflightSubmittedIds = []
-
-    # All ops that are waiting for the server to acknowledge @inflightOp
-    @pendingOp = null
-    @pendingCallbacks = []
-
-    # Some recent ops, incase submitOp is called with an old op version number.
-    @serverOps = {}
-
-  # Transform a server op by a client op, and vice versa.
-  _xf: (client, server) ->
-    if @type.transformX
-      @type.transformX(client, server)
-    else
-      client_ = @type.transform client, server, 'left'
-      server_ = @type.transform server, client, 'right'
-      return [client_, server_]
-  
-  _otApply: (docOp, isRemote, msg) ->
-    oldSnapshot = @snapshot
-    @snapshot = @type.apply(@snapshot, docOp)
-
-    # Its important that these event handlers are called with oldSnapshot.
-    # The reason is that the OT type APIs might need to access the snapshots to
-    # determine information about the received op.
-    @emit 'change', docOp, oldSnapshot, msg
-    @emit 'remoteop', docOp, oldSnapshot, msg if isRemote
-  
-  _connectionStateChanged: (state, data) ->
-    switch state
-      when 'disconnected'
-        @state = 'closed'
-        # This is used by the server to make sure that when an op is resubmitted it
-        # doesn't end up getting applied twice.
-        @inflightSubmittedIds.push @connection.id if @inflightOp
-
-        @emit 'closed'
-
-      when 'ok' # Might be able to do this when we're connecting... that would save a roundtrip.
-        @open() if @autoOpen
-
-      when 'stopped'
-        @_openCallback? data
-
-    @emit state, data
-
-  _setType: (type) ->
-    if typeof type is 'string'
-      type = types[type]
-
-    throw new Error 'Support for types without compose() is not implemented' unless type and type.compose
-
-    @type = type
-    if type.api
-      this[k] = v for k, v of type.api
-      @_register?()
-    else
-      @provides = {}
-
-  _onMessage: (msg) ->
-    #console.warn 's->c', msg
-    if msg.open == true
-      # The document has been successfully opened.
-      @state = 'open'
-      @_create = false # Don't try and create the document again next time open() is called.
-      unless @created?
-        @created = !!msg.create
-
-      @_setType msg.type if msg.type
-      if msg.create
-        @created = true
-        @snapshot = @type.create()
-      else
-        @created = false unless @created is true
-        @snapshot = msg.snapshot if msg.snapshot isnt undefined
-
-      @version = msg.v if msg.v?
-
-      # Resend any previously queued operation.
-      if @inflightOp
-        response =
-          doc: @name
-          op: @inflightOp
-          v: @version
-        response.dupIfSource = @inflightSubmittedIds if @inflightSubmittedIds.length
-        @connection.send response
-      else
-        @flush()
-
-      @emit 'open'
-      
-      @_openCallback? null
- 
-    else if msg.open == false
-      # The document has either been closed, or an open request has failed.
-      if msg.error
-        # An error occurred opening the document.
-        console?.error "Could not open document: #{msg.error}"
-        @emit 'error', msg.error
-        @_openCallback? msg.error
-
-      @state = 'closed'
-      @emit 'closed'
-
-      @_closeCallback?()
-      @_closeCallback = null
-
-    else if msg.op is null and error is 'Op already submitted'
-      # We've tried to resend an op to the server, which has already been received successfully. Do nothing.
-      # The op will be confirmed normally when we get the op itself was echoed back from the server
-      # (handled below).
-
-    else if (msg.op is undefined and msg.v isnt undefined) or (msg.op and msg.meta.source in @inflightSubmittedIds)
-      # Our inflight op has been acknowledged.
-      oldInflightOp = @inflightOp
-      @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.
-        # We'll send the error message to the user and roll back the change.
-        #
-        # If the server isn't going to allow edits anyway, we should probably
-        # figure out some way to flag that (readonly:true in the open request?)
-
-        if @type.invert
-          undo = @type.invert oldInflightOp
-
-          # Now we have to transform the undo operation by any server ops & pending ops
-          if @pendingOp
-            [@pendingOp, undo] = @_xf @pendingOp, undo
-
-          # ... and apply it locally, reverting the changes.
-          # 
-          # This call will also call @emit 'remoteop'. I'm still not 100% sure about this
-          # functionality, because its really a local op. Basically, the problem is that
-          # if the client's op is rejected by the server, the editor window should update
-          # to reflect the undo.
-          @_otApply undo, true, msg
-        else
-          @emit 'error', "Op apply failed (#{error}) and the op could not be reverted"
-
-        callback error for callback in @inflightCallbacks
-      else
-        # The op applied successfully.
-        
-        # We may get multiple acks of the same message if we retried it, 
-        # so its ok if we receive an ack for a version that we've already gone past.
-        # If so, just ignore it
-        if msg.v < @version
-            return
-
-        throw new Error('Invalid version from server') unless msg.v == @version
-
-        @serverOps[@version] = oldInflightOp
-        @version++
-        @emit 'acknowledge', oldInflightOp
-        callback null, oldInflightOp for callback in @inflightCallbacks
-
-      # Send the next op.
-      @delayedFlush()
-
-    else if msg.op
-      # We got a new op from the server.
-      # msg is {doc:, op:, v:}
-
-      # There is a bug in socket.io (produced on firefox 3.6) which causes messages
-      # to be duplicated sometimes.
-      # We'll just silently drop subsequent messages.
-      return if msg.v < @version
-
-      return @emit 'error', "Expected docName '#{@name}' but got #{msg.doc}" unless msg.doc == @name
-      return @emit 'error', "Expected version #{@version} but got #{msg.v}" unless msg.v == @version
-
-  #    p "if: #{i @inflightOp} pending: #{i @pendingOp} doc '#{@snapshot}' op: #{i msg.op}"
-
-      op = msg.op
-      @serverOps[@version] = op
-
-      docOp = op
-      if @inflightOp != null
-        [@inflightOp, docOp] = @_xf @inflightOp, docOp
-      if @pendingOp != null
-        [@pendingOp, docOp] = @_xf @pendingOp, docOp
-        
-      @version++
-      # Finally, apply the op to @snapshot and trigger any event listeners
-      @_otApply docOp, true, msg
-
-    else if msg.meta
-      {path, value} = msg.meta
-
-      switch path?[0]
-        when 'shout'
-          return @emit 'shout', value
-        else
-          console?.warn 'Unhandled meta op:', msg
-
-    else
-      console?.warn 'Unhandled document message:', msg
-
-
-  # Send ops to the server, if appropriate.
-  #
-  # Only one op can be in-flight at a time, so if an op is already on its way then
-  # this method does nothing.
-  flush: =>
-    @flushTimeout = null
-    #console.log "CALLED FLUSH"
-
-    return unless @connection.state == 'ok' and @inflightOp == null and @pendingOp != null
-
-    # Rotate null -> pending -> inflight
-    @inflightOp = @pendingOp
-    @inflightCallbacks = @pendingCallbacks
-
-    @pendingOp = null
-    @pendingCallbacks = []
-
-    @emit "flipped_pending_to_inflight"
-
-    #console.log "SENDING OP TO SERVER", @inflightOp, @version
-    @connection.send {doc:@name, op:@inflightOp, v:@version}
-
-  # Submit an op to the server. The op maybe held for a little while before being sent, as only one
-  # op can be inflight at any time.
-  submitOp: (op, callback) ->
-    op = @type.normalize(op) if @type.normalize?
-
-    oldSnapshot = @snapshot
-    # If this throws an exception, no changes should have been made to the doc
-    @snapshot = @type.apply @snapshot, op
-
-    if @pendingOp != null
-      @pendingOp = @type.compose(@pendingOp, op)
-    else
-      @pendingOp = op
-
-    @pendingCallbacks.push callback if callback
-
-    @emit 'change', op, oldSnapshot
-
-    @delayedFlush()
-
-  delayedFlush: () ->
-    if !@flushTimeout?
-        @flushTimeout = setTimeout @flush, @_flushDelay || 0
-  
-  setFlushDelay: (delay) =>
-      @_flushDelay = delay
-  
-  shout: (msg) =>
-    # Meta ops don't have to queue, they can go direct. Good/bad idea?
-    @connection.send {doc:@name, meta: { path: ['shout'], value: msg } }
-  
-  # Open a document. The document starts closed.
-  open: (callback) ->
-    @autoOpen = true
-    return unless @state is 'closed'
-
-    message =
-      doc: @name
-      open: true
-
-    message.snapshot = null if @snapshot is undefined
-    message.type = @type.name if @type
-    message.v = @version if @version?
-    message.create = true if @_create
-
-    @connection.send message
-
-    @state = 'opening'
-
-    @_openCallback = (error) =>
-      @_openCallback = null
-      callback? error
-
-  # Close a document.
-  close: (callback) ->
-    @autoOpen = false
-    return callback?() if @state is 'closed'
-
-    @connection.send {doc:@name, open:false}
-
-    # Should this happen immediately or when we get open:false back from the server?
-    @state = 'closed'
-
-    @emit 'closing'
-    @_closeCallback = callback
- 
-# Make documents event emitters
-unless WEB?
-  MicroEvent = require './microevent'
-
-MicroEvent.mixin Doc
-
-exports.Doc = Doc

+ 0 - 73
services/web/public/coffee/ide/editor/sharejs/vendor/client/index.coffee

@@ -1,73 +0,0 @@
-# This file implements the sharejs client, as defined here:
-# https://github.com/josephg/ShareJS/wiki/Client-API
-#
-# It works from both a node.js context and a web context (though in the latter case,
-# it needs to be compiled to work.)
-#
-# It should become a little nicer once I start using more of the new RPC features added
-# in socket.io 0.7.
-#
-# Note that anything declared in the global scope here is shared with other files
-# built by closure. Be careful what you put in this namespace.
-
-unless WEB?
-  Connection = require('./connection').Connection
-
-# Open a document with the given name. The connection is created implicitly and reused.
-#
-# This function uses a local (private) set of connections to support .open().
-#
-# Open returns the connection its using to access the document.
-exports.open = do ->
-  # This is a private connection pool for implicitly created connections.
-  connections = {}
-
-  getConnection = (origin) ->
-    if WEB?
-      location = window.location
-      origin ?= "#{location.protocol}//#{location.host}/channel"
-    
-    unless connections[origin]
-      c = new Connection origin
-
-      del = -> delete connections[origin]
-      c.on 'disconnecting', del
-      c.on 'connect failed', del
-      connections[origin] = c
-    
-    connections[origin]
-
-  # If you're using the bare API, connections are cleaned up as soon as there's no
-  # documents using them.
-  maybeClose = (c) ->
-    numDocs = 0
-    for name, doc of c.docs
-      numDocs++ if doc.state isnt 'closed' || doc.autoOpen
-
-    if numDocs == 0
-      c.disconnect()
- 
-  (docName, type, origin, callback) ->
-    if typeof origin == 'function'
-      callback = origin
-      origin = null
-
-    c = getConnection origin
-    c.numDocs++
-    c.open docName, type, (error, doc) ->
-      if error
-        callback error
-        maybeClose c
-      else
-        doc.on 'closed', -> maybeClose c
-       
-        callback null, doc
-    
-    c.on 'connect failed'
-    return c
-
-
-unless WEB?
-  exports.Doc = require('./doc').Doc
-  exports.Connection = require('./connection').Connection
-

+ 0 - 46
services/web/public/coffee/ide/editor/sharejs/vendor/client/microevent.coffee

@@ -1,46 +0,0 @@
-# This is a simple port of microevent.js to Coffeescript. I've changed the
-# function names to be consistent with node.js EventEmitter.
-#
-# microevent.js is copyright Jerome Etienne, and licensed under the MIT license:
-# https://github.com/jeromeetienne/microevent.js
-
-nextTick = if WEB? then (fn) -> setTimeout fn, 0 else process['nextTick']
-
-class MicroEvent
-  on: (event, fct) ->
-    @_events ||= {}
-    @_events[event] ||= []
-    @_events[event].push(fct)
-    this
-
-  removeListener: (event, fct) ->
-    @_events ||= {}
-    listeners = (@_events[event] ||= [])
-    
-    # Sadly, there's no IE8- support for indexOf.
-    i = 0
-    while i < listeners.length
-      listeners[i] = undefined if listeners[i] == fct
-      i++
-
-    nextTick => @_events[event] = (x for x in @_events[event] when x)
-
-    this
-
-  emit: (event, args...) ->
-    return this unless @_events?[event]
-    fn.apply this, args for fn in @_events[event] when fn
-    this
-
-# mixin will delegate all MicroEvent.js function in the destination object
-MicroEvent.mixin = (obj) ->
-  proto = obj.prototype || obj
-
-  # Damn closure compiler :/
-  proto.on = MicroEvent.prototype.on
-  proto.removeListener = MicroEvent.prototype.removeListener
-  proto.emit = MicroEvent.prototype.emit
-  obj
-
-module.exports = MicroEvent unless WEB?
-

+ 0 - 69
services/web/public/coffee/ide/editor/sharejs/vendor/client/textarea.coffee

@@ -1,69 +0,0 @@
-# Create an op which converts oldval -> newval.
-#
-# This function should be called every time the text element is changed. Because changes are
-# always localised, the diffing is quite easy.
-#
-# This algorithm is O(N), but I suspect you could speed it up somehow using regular expressions.
-applyChange = (doc, oldval, newval) ->
-  return if oldval == newval
-  commonStart = 0
-  commonStart++ while oldval.charAt(commonStart) == newval.charAt(commonStart)
-
-  commonEnd = 0
-  commonEnd++ while oldval.charAt(oldval.length - 1 - commonEnd) == newval.charAt(newval.length - 1 - commonEnd) and
-    commonEnd + commonStart < oldval.length and commonEnd + commonStart < newval.length
-
-  doc.del commonStart, oldval.length - commonStart - commonEnd unless oldval.length == commonStart + commonEnd
-  doc.insert commonStart, newval[commonStart ... newval.length - commonEnd] unless newval.length == commonStart + commonEnd
-
-window.sharejs.extendDoc 'attach_textarea', (elem) ->
-  doc = this
-  elem.value = @getText()
-  prevvalue = elem.value
-
-  replaceText = (newText, transformCursor) ->
-    newSelection = [
-      transformCursor elem.selectionStart
-      transformCursor elem.selectionEnd
-    ]
-
-    scrollTop = elem.scrollTop
-    elem.value = newText
-    elem.scrollTop = scrollTop if elem.scrollTop != scrollTop
-    [elem.selectionStart, elem.selectionEnd] = newSelection
-
-  @on 'insert', (pos, text) ->
-    transformCursor = (cursor) ->
-      if pos < cursor
-        cursor + text.length
-      else
-        cursor
-    #for IE8 and Opera that replace \n with \r\n.
-    prevvalue = elem.value.replace /\r\n/g, '\n'
-    replaceText prevvalue[...pos] + text + prevvalue[pos..], transformCursor
-  
-  @on 'delete', (pos, text) ->
-    transformCursor = (cursor) ->
-      if pos < cursor
-        cursor - Math.min(text.length, cursor - pos)
-      else
-        cursor
-    #for IE8 and Opera that replace \n with \r\n.
-    prevvalue = elem.value.replace /\r\n/g, '\n'
-    replaceText prevvalue[...pos] + prevvalue[pos + text.length..], transformCursor
-
-  genOp = (event) ->
-    onNextTick = (fn) -> setTimeout fn, 0
-    onNextTick ->
-      if elem.value != prevvalue
-        # IE constantly replaces unix newlines with \r\n. ShareJS docs
-        # should only have unix newlines.
-        prevvalue = elem.value
-        applyChange doc, doc.getText(), elem.value.replace /\r\n/g, '\n'
-
-  for event in ['textInput', 'keydown', 'keyup', 'select', 'cut', 'paste']
-    if elem.addEventListener
-      elem.addEventListener event, genOp, false
-    else
-      elem.attachEvent 'on'+event, genOp
-

+ 0 - 13
services/web/public/coffee/ide/editor/sharejs/vendor/client/web-prelude.coffee

@@ -1,13 +0,0 @@
-# This file is included at the top of the compiled client-side javascript
-
-# This way all the modules can add stuff to exports, and for the web client they'll all get exported.
-window.sharejs = exports =
-  'version': '0.5.0'
-
-# This is compiled out when compiled with uglifyjs, but its important for the share.uncompressed.js.
-#
-# Maybe I should rename WEB to __SHAREJS_WEB or something, but its only relevant for testing
-# anyway.
-if typeof WEB == 'undefined'
-  # This will put WEB in the global scope in a browser.
-  window.WEB = true

+ 0 - 5
services/web/public/coffee/ide/editor/sharejs/vendor/index.coffee

@@ -1,5 +0,0 @@
-exports.server = require './server'
-exports.client = require './client'
-exports.types = require './types'
-
-exports.version = '0.5.0'

+ 0 - 333
services/web/public/coffee/ide/editor/sharejs/vendor/server/browserchannel.coffee

@@ -1,333 +0,0 @@
-# This implements the network API for ShareJS.
-#
-# The wire protocol is speccced out here:
-# https://github.com/josephg/ShareJS/wiki/Wire-Protocol
-#
-# When a client connects the server first authenticates it and sends:
-#
-# S: {auth:<agent session id>}
-#  or
-# S: {auth:null, error:'forbidden'}
-#
-# After that, the client can open documents:
-#
-# C: {doc:'foo', open:true, snapshot:null, create:true, type:'text'}
-# S: {doc:'foo', open:true, snapshot:{snapshot:'hi there', v:5, meta:{}}, create:false}
-#
-# ...
-#
-# The client can send open requests as soon as the socket has opened - it doesn't need to
-# wait for auth.
-#
-# The wire protocol is documented here:
-# https://github.com/josephg/ShareJS/wiki/Wire-Protocol
-
-browserChannel = require('browserchannel').server
-util = require 'util'
-hat = require 'hat'
-
-syncQueue = require './syncqueue'
-
-# Attach the streaming protocol to the supplied http.Server.
-#
-# Options = {}
-module.exports = (createAgent, options) ->
-  options or= {}
-
-  browserChannel options, (session) ->
-    #console.log "New BC session from #{session.address} with id #{session.id}"
-    data =
-      headers: session.headers
-      remoteAddress: session.address
-
-    # This is the user agent through which a connecting client acts. It is set when the
-    # session is authenticated. The agent is responsible for making sure client requests are
-    # properly authorized, and metadata is kept up to date.
-    agent = null
-
-    # To save on network traffic, the agent & server can leave out the docName with each message to mean
-    # 'same as the last message'
-    lastSentDoc = null
-    lastReceivedDoc = null
-
-    # Map from docName -> {queue, listener if open}
-    docState = {}
-
-    # We'll only handle one message from each client at a time.
-    handleMessage = (query) ->
-      #console.log "Message from #{session.id}", query
-
-      error = null
-      error = 'Invalid docName' unless query.doc is null or typeof query.doc is 'string' or (query.doc is undefined and lastReceivedDoc)
-      error = "'create' must be true or missing" unless query.create in [true, undefined]
-      error = "'open' must be true, false or missing" unless query.open in [true, false, undefined]
-      error = "'snapshot' must be null or missing" unless query.snapshot in [null, undefined]
-      error = "'type' invalid" unless query.type is undefined or typeof query.type is 'string'
-      error = "'v' invalid" unless query.v is undefined or (typeof query.v is 'number' and query.v >= 0)
-      
-      if error
-        console.warn "Invalid query #{JSON.stringify query} from #{agent.sessionId}: #{error}"
-        session.abort()
-        return callback()
-
-      # The agent can specify null as the docName to get a random doc name.
-      if query.doc is null
-        query.doc = lastReceivedDoc = hat()
-      else if query.doc != undefined
-        lastReceivedDoc = query.doc
-      else
-        unless lastReceivedDoc
-          console.warn "msg.doc missing in query #{JSON.stringify query} from #{agent.sessionId}"
-          # The disconnect handler will be called when we do this, which will clean up the open docs.
-          return session.abort()
-
-        query.doc = lastReceivedDoc
-
-      docState[query.doc] or= queue: syncQueue (query, callback) ->
-        # When the session is closed, we'll nuke docState. When that happens, no more messages
-        # should be handled.
-        return callback() unless docState
-
-        # Close messages are {open:false}
-        if query.open == false
-          handleClose query, callback
-   
-        # Open messages are {open:true}. There's a lot of shared logic with getting snapshots
-        # and creating documents. These operations can be done together; and I'll handle them
-        # together.
-        else if query.open or query.snapshot is null or query.create
-          # You can open, request a snapshot and create all in the same
-          # request. They're all handled together.
-          handleOpenCreateSnapshot query, callback
-
-        # The socket is submitting an op.
-        else if query.op? or query.meta?.path?
-          handleOp query, callback
-
-        else
-          console.warn "Invalid query #{JSON.stringify query} from #{agent.sessionId}"
-          session.abort()
-          callback()
-
-      # ... And add the message to the queue.
-      docState[query.doc].queue query
-
-
-    # # Some utility methods for message handlers
-
-    # Send a message to the socket.
-    # msg _must_ have the doc:DOCNAME property set. We'll remove it if its the same as lastReceivedDoc.
-    send = (response) ->
-      if response.doc is lastSentDoc
-        delete response.doc
-      else
-        lastSentDoc = response.doc
-
-      # Its invalid to send a message to a closed session. We'll silently drop messages if the
-      # session has closed.
-      if session.state isnt 'closed'
-        #console.log "Sending", response
-        session.send response
-
-    # Open the given document name, at the requested version.
-    # callback(error, version)
-    open = (docName, version, callback) ->
-      return callback 'Session closed' unless docState
-      return callback 'Document already open' if docState[docName].listener
-      #p "Registering listener on #{docName} by #{socket.id} at #{version}"
-
-      docState[docName].listener = listener = (opData) ->
-        throw new Error 'Consistency violation - doc listener invalid' unless docState[docName].listener == listener
-
-        #p "listener doc:#{docName} opdata:#{i opData} v:#{version}"
-
-        # Skip the op if this socket sent it.
-        return if opData.meta.source is agent.sessionId
-
-        opMsg =
-          doc: docName
-          op: opData.op
-          v: opData.v
-          meta: opData.meta
-
-        send opMsg
-      
-      # Tell the socket the doc is open at the requested version
-      agent.listen docName, version, listener, (error, v) ->
-        delete docState[docName].listener if error
-        callback error, v
-
-    # Close the named document.
-    # callback([error])
-    close = (docName, callback) ->
-      #p "Closing #{docName}"
-      return callback 'Session closed' unless docState
-      listener = docState[docName].listener
-      return callback 'Doc already closed' unless listener?
-
-      agent.removeListener docName
-      delete docState[docName].listener
-      callback()
-
-    # Handles messages with any combination of the open:true, create:true and snapshot:null parameters
-    handleOpenCreateSnapshot = (query, finished) ->
-      docName = query.doc
-      msg = doc:docName
-
-      callback = (error) ->
-        if error
-          close(docName) if msg.open == true
-          msg.open = false if query.open == true
-          msg.snapshot = null if query.snapshot != undefined
-          delete msg.create
-
-          msg.error = error
-
-        send msg
-        finished()
-
-      return callback 'No docName specified' unless query.doc?
-
-      if query.create == true
-        if typeof query.type != 'string'
-          return callback 'create:true requires type specified'
-
-      if query.meta != undefined
-        unless typeof query.meta == 'object' and Array.isArray(query.meta) == false
-          return callback 'meta must be an object'
-
-      docData = undefined
-
-      # This is implemented with a series of cascading methods for each different type of
-      # thing this method can handle. This would be so much nicer with an async library. Welcome to
-      # callback hell.
-
-      step1Create = ->
-        return step2Snapshot() if query.create != true
-
-        # The document obviously already exists if we have a snapshot.
-        if docData
-          msg.create = false
-          step2Snapshot()
-        else
-          agent.create docName, query.type, query.meta || {}, (error) ->
-            if error is 'Document already exists'
-              # We've called getSnapshot (-> null), then create (-> already exists). Its possible
-              # another agent has called create() between our getSnapshot and create() calls.
-              agent.getSnapshot docName, (error, data) ->
-                return callback error if error
-
-                docData = data
-                msg.create = false
-                step2Snapshot()
-            else if error
-              callback error
-            else
-              msg.create = true
-              step2Snapshot()
-
-      # The socket requested a document snapshot
-      step2Snapshot = ->
-#        if query.create or query.open or query.snapshot == null
-#          msg.meta = docData.meta
-
-        # Skip inserting a snapshot if the document was just created.
-        if query.snapshot != null or msg.create == true
-          step3Open()
-          return
-
-        if docData
-          msg.v = docData.v
-          msg.type = docData.type.name unless query.type == docData.type.name
-          msg.snapshot = docData.snapshot
-        else
-          return callback 'Document does not exist'
-
-        step3Open()
-
-      # Attempt to open a document with a given name. Version is optional.
-      # callback(opened at version) or callback(null, errormessage)
-      step3Open = ->
-        return callback() if query.open != true
-
-        # Verify the type matches
-        return callback 'Type mismatch' if query.type and docData and query.type != docData.type.name
-
-        open docName, query.v, (error, version) ->
-          return callback error if error
-
-          # + Should fail if the type is wrong.
-
-          #p "Opened #{docName} at #{version} by #{socket.id}"
-          msg.open = true
-          msg.v = version
-          callback()
-
-      # Technically, we don't need a snapshot if the user called create but not open or createSnapshot,
-      # but no clients do that yet anyway.
-      if query.snapshot == null or query.open == true #and query.type
-        agent.getSnapshot query.doc, (error, data) ->
-          return callback error if error and error != 'Document does not exist'
-
-          docData = data
-          step1Create()
-      else
-        step1Create()
-
-    # The socket closes a document
-    handleClose = (query, callback) ->
-      close query.doc, (error) ->
-        if error
-          # An error closing still results in the doc being closed.
-          send {doc:query.doc, open:false, error:error}
-        else
-          send {doc:query.doc, open:false}
-
-        callback()
-
-    # We received an op from the socket
-    handleOp = (query, callback) ->
-      # ...
-      #throw new Error 'No version specified' unless query.v?
-
-      opData = {v:query.v, op:query.op, meta:query.meta, dupIfSource:query.dupIfSource}
-
-      # If it's a metaOp don't send a response
-      agent.submitOp query.doc, opData, if (not opData.op? and opData.meta?.path?) then callback else (error, appliedVersion) ->
-        msg = if error
-          #p "Sending error to socket: #{error}"
-          {doc:query.doc, v:null, error:error}
-        else
-          {doc:query.doc, v:appliedVersion}
-
-        send msg
-        callback()
-
-    # We don't process any messages from the agent until they've authorized. Instead,
-    # they are stored in this buffer.
-    buffer = []
-    session.on 'message', bufferMsg = (msg) -> buffer.push msg
-
-    createAgent data, (error, agent_) ->
-      if error
-        # The client is not authorized, so they shouldn't try and reconnect.
-        session.send {auth:null, error}
-        session.stop()
-      else
-        agent = agent_
-        session.send auth:agent.sessionId
-
-        # Ok. Now we can handle all the messages in the buffer. They'll go straight to
-        # handleMessage from now on.
-        session.removeListener 'message', bufferMsg
-        handleMessage msg for msg in buffer
-        buffer = null
-        session.on 'message', handleMessage
-
-    session.on 'close', ->
-      return unless agent
-      #console.log "Client #{agent.sessionId} disconnected"
-      for docName, {listener} of docState
-        agent.removeListener docName if listener
-      docState = null
-

+ 0 - 149
services/web/public/coffee/ide/editor/sharejs/vendor/server/db/couchdb.coffee

@@ -1,149 +0,0 @@
-# OT storage for CouchDB
-# Author: Max Ogden (@maxogden)
-#
-# The couchdb database contains two kinds of documents:
-#
-# - Document snapshots have a key which is doc:the document name
-# - Document ops have a random key, but docName: defined.
-
-request = require('request').defaults json: true
-
-# Helper method to parse errors out of couchdb. There's way more ways
-# things can go wrong, but I think this catches all the ones I care about.
-#
-# callback(error) or callback()
-parseError = (err, resp, body, callback) ->
-  body = body[0] if Array.isArray body and body.length >= 1
-
-  if err
-    # This indicates an HTTP error
-    callback err
-  else if resp.statusCode is 404
-    callback 'Document does not exist'
-  else if resp.statusCode is 403
-    callback 'forbidden'
-  else if typeof body is 'object'
-    if body.error is 'conflict'
-      callback 'Document already exists'
-    else if body.error
-      callback "#{body.error} reason: #{body.reason}"
-    else
-      callback()
-  else
-    callback()
-
-module.exports = (options) ->
-  options ?= {}
-  db = options.uri or "http://localhost:5984/sharejs"
-
-  uriForDoc = (docName) -> "#{db}/doc:#{encodeURIComponent docName}"
-  uriForOps = (docName, start, end, include_docs) ->
-    startkey = encodeURIComponent(JSON.stringify [docName, start])
-    # {} is sorted after all numbers - so this will get all ops in the case that end is null.
-    endkey = encodeURIComponent(JSON.stringify [docName, end ? {}])
-
-    # Another way to write this method would be to use node's builtin uri-encoder.
-    extra = if include_docs then '&include_docs=true' else ''
-    "#{db}/_design/sharejs/_view/operations?startkey=#{startkey}&endkey=#{endkey}&inclusive_end=false#{extra}"
-
-  # Helper method to get the revision of a document snapshot.
-  getRev = (docName, dbMeta, callback) ->
-    if dbMeta?.rev
-      callback null, dbMeta.rev
-    else
-      # JSON defaults to true, and that makes request think I'm trying to sneak a request
-      # body in. Ugh.
-      request.head {uri:uriForDoc(docName), json:false}, (err, resp, body) ->
-        parseError err, resp, body, (error) ->
-          if error
-            callback error
-          else
-            # The etag is the rev in quotes.
-            callback null, JSON.parse(resp.headers.etag)
-  
-  writeSnapshotInternal = (docName, data, rev, callback) ->
-    body = data
-    body.fieldType = 'Document'
-    body._rev = rev if rev?
-
-    request.put uri:(uriForDoc docName), body:body, (err, resp, body) ->
-      parseError err, resp, body, (error) ->
-        if error
-          #console.log 'create error'
-          # This will send write conflicts as 'document already exists'. Thats kinda wierd, but
-          # it shouldn't happen anyway
-          callback? error
-        else
-          # We pass the document revision back to the db cache so it can give it back to couchdb on subsequent requests.
-          callback? null, {rev: body.rev}
-
-  # getOps returns all ops between start and end. end can be null.
-  getOps: (docName, start, end, callback) ->
-    return callback null, [] if start == end
-    
-    # Its a bit gross having this end parameter here....
-    endkey = if end? then [docName, end - 1]
-    
-    request uriForOps(docName, start, end), (err, resp, body) ->
-      # Rows look like this:
-      # {"id":"<uuid>","key":["doc name",0],"value":{"op":[{"p":0,"i":"hi"}],"meta":{}}}
-      data = ({op: row.value.op, meta: row.value.meta} for row in body.rows)
-      callback null, data
-  
-  # callback(error, db metadata)
-  create: (docName, data, callback) ->
-    writeSnapshotInternal docName, data, null, callback
- 
-  delete: del = (docName, dbMeta, callback) ->
-    getRev docName, dbMeta, (error, rev) ->
-      return callback? error if error
-
-      docs = [{_id:"doc:#{docName}", _rev:rev, _deleted:true}]
-      # Its annoying, but we need to get the revision from the document. I don't think there's a simple way to do this.
-      # This request will get all the ops twice.
-      request uriForOps(docName, 0, null, true), (err, resp, body) ->
-        # Rows look like this:
-        # {"id":"<uuid>","key":["doc name",0],"value":{"op":[{"p":0,"i":"hi"}],"meta":{}},
-        #  "doc":{"_id":"<uuid>","_rev":"1-21a40c56ebd5d424ffe56950e77bc847","op":[{"p":0,"i":"hi"}],"v":0,"meta":{},"docName":"doc6"}}
-        for row in body.rows
-          row.doc._deleted = true
-          docs.push row.doc
-
-        request.post url: "#{db}/_bulk_docs", body: {docs}, (err, resp, body) ->
-          if body[0].error is 'conflict'
-            # Somebody has edited the document since we did a GET on the revision information. Recurse.
-            # By passing null to dbMeta I'm forcing the revision information to be reacquired.
-            del docName, null, callback
-          else
-            parseError err, resp, body, (error) -> callback? error
- 
-  writeOp: (docName, opData, callback) ->
-    body =
-      docName: docName
-      op: opData.op
-      v: opData.v
-      meta: opData.meta
-
-    request.post url:db, body:body, (err, resp, body) ->
-      parseError err, resp, body, callback
-
-  writeSnapshot: (docName, docData, dbMeta, callback) ->
-    getRev docName, dbMeta, (error, rev) ->
-      return callback? error if error
-
-      writeSnapshotInternal docName, docData, rev, callback
-
-  getSnapshot: (docName, callback) ->
-    request uriForDoc(docName), (err, resp, body) ->
-      parseError err, resp, body, (error) ->
-        if error
-          callback error
-        else
-          callback null,
-              snapshot: body.snapshot
-              type: body.type
-              meta: body.meta
-              v: body.v
-            , {rev: body._rev} # dbMeta
-
-  close: ->

+ 0 - 28
services/web/public/coffee/ide/editor/sharejs/vendor/server/db/index.coffee

@@ -1,28 +0,0 @@
-# This is a simple switch for the different database implementations.
-#
-# The interface is the same as the regular database implementations, except
-# the options object can have another type:<TYPE> parameter which specifies
-# which type of database to use.
-#
-# Example usage:
-#  require('server/db').create {type:'redis'}
-
-defaultType = 'redis'
-
-module.exports = (options) ->
-  options ?= {}
-  type = options.type ? defaultType
-
-  console.warn "Database type: 'memory' detected. This has been deprecated and will
- be removed in a future version. Use 'none' instead, or just remove the db:{} block
- from your options." if type is 'memory'
-
-  if type in ['none', 'memory']
-    null
-  else
-    Db = switch type
-      when 'redis' then require './redis'
-      when 'couchdb' then require './couchdb'
-      when 'pg' then require './pg'
-      else throw new Error "Invalid or unsupported database type: '#{type}'"
-    new Db options

+ 0 - 198
services/web/public/coffee/ide/editor/sharejs/vendor/server/db/pg.coffee

@@ -1,198 +0,0 @@
-# This is an implementation of the OT data backend for PostgreSQL. It requires
-# that you have two tables defined in your schema: one for the snapshots
-# and one for the operations. You must also install the 'pg' package.
-#
-#
-# Example usage:
-#
-#     var connect = require('connect');
-#     var share   = require('share').server;
-#
-#     var server = connect(connect.logger());
-#
-#     var options = {
-#       db: {
-#         type: 'pg',
-#         uri:  'tcp://josh:@localhost/sharejs',
-#         create_tables_automatically: true
-#       }
-#     };
-#
-#     share.attach(server, options);
-#     server.listen(9000);
-#
-# You can run bin/setup_pg to create the SQL tables initially.
-
-pg = require('pg').native
-
-defaultOptions =
-  schema: 'sharejs'
-  create_tables_automatically: true
-  operations_table: 'ops'
-  snapshot_table: 'snapshots'
-
-module.exports = PgDb = (options) ->
-  return new Db if !(this instanceof PgDb)
-
-  options ?= {}
-  options[k] ?= v for k, v of defaultOptions
-
-  client = new pg.Client options.uri
-  client.connect()
-
-  snapshot_table = options.schema and "#{options.schema}.#{options.snapshot_table}" or options.snapshot_table
-  operations_table = options.schema and "#{options.schema}.#{options.operations_table}" or options.operations_table
-
-  @close = ->
-    client.end()
-
-  @initialize = (callback) ->
-    console.warn 'Creating postgresql database tables'
-
-    sql = """
-      CREATE SCHEMA #{options.schema};
-
-      CREATE TABLE #{snapshot_table} (
-        doc text NOT NULL,
-        v int4 NOT NULL,
-        type text NOT NULL,
-        snapshot text NOT NULL,
-        meta text NOT NULL,
-        created_at timestamp(6) NOT NULL,
-        CONSTRAINT snapshots_pkey PRIMARY KEY (doc, v)
-      );
-
-      CREATE TABLE #{operations_table} (
-        doc text NOT NULL,
-        v int4 NOT NULL,
-        op text NOT NULL,
-        meta text NOT NULL,
-        CONSTRAINT operations_pkey PRIMARY KEY (doc, v)
-      );
-    """
-    client.query sql, (error, result) ->
-      callback? error?.message
-
-  # This will perminantly delete all data in the database.
-  @dropTables = (callback) ->
-    sql = "DROP SCHEMA #{options.schema} CASCADE;"
-    client.query sql, (error, result) ->
-      callback? error.message
-
-  @create = (docName, docData, callback) ->
-    sql = """
-      INSERT INTO #{snapshot_table} ("doc", "v", "snapshot", "meta", "type", "created_at")
-        VALUES ($1, $2, $3, $4, $5, now())
-    """
-    values = [docName, docData.v, JSON.stringify(docData.snapshot), JSON.stringify(docData.meta), docData.type]
-    client.query sql, values, (error, result) ->
-      if !error?
-        callback?()
-      else if /duplicate key value violates unique constraint/.test(error.toString())
-        callback? "Document already exists"
-      else
-        callback? error?.message
-
-  @delete = (docName, dbMeta, callback) ->
-    sql = """
-      DELETE FROM #{operations_table}
-      WHERE "doc" = $1
-      RETURNING *
-    """
-    values = [docName]
-    client.query sql, values, (error, result) ->
-      if !error?
-        sql = """
-          DELETE FROM #{snapshot_table}
-          WHERE "doc" = $1
-          RETURNING *
-        """
-        client.query sql, values, (error, result) ->
-          if !error? and result.rows.length > 0
-            callback?()
-          else if !error?
-            callback? "Document does not exist"
-          else
-            callback? error?.message
-      else
-        callback? error?.message
-
-  @getSnapshot = (docName, callback) ->
-    sql = """
-      SELECT *
-      FROM #{snapshot_table}
-      WHERE "doc" = $1
-      ORDER BY "v" DESC
-      LIMIT 1
-    """
-    values = [docName]
-    client.query sql, values, (error, result) ->
-      if !error? and result.rows.length > 0
-        row = result.rows[0]
-        data =
-          v:        row.v
-          snapshot: JSON.parse(row.snapshot)
-          meta:     JSON.parse(row.meta)
-          type:     row.type
-        callback? null, data
-      else if !error?
-        callback? "Document does not exist"
-      else
-        callback? error?.message
-
-  @writeSnapshot = (docName, docData, dbMeta, callback) ->
-    sql = """
-      UPDATE #{snapshot_table}
-      SET "v" = $2, "snapshot" = $3, "meta" = $4
-      WHERE "doc" = $1
-    """
-    values = [docName, docData.v, JSON.stringify(docData.snapshot), JSON.stringify(docData.meta)]
-    client.query sql, values, (error, result) ->
-      if !error?
-        callback?()
-      else
-        callback? error?.message
-
-  @getOps = (docName, start, end, callback) ->
-    end = if end? then end - 1 else 2147483647
-    sql = """
-      SELECT *
-      FROM #{operations_table}
-      WHERE "v" BETWEEN $1 AND $2
-      AND "doc" = $3
-      ORDER BY "v" ASC
-    """
-    values = [start, end, docName]
-    client.query sql, values, (error, result) ->
-      if !error?
-        data = result.rows.map (row) ->
-          return {
-            op:   JSON.parse row.op
-            # v:    row.version
-            meta: JSON.parse row.meta
-          }
-        callback? null, data
-      else
-        callback? error?.message
-
-  @writeOp = (docName, opData, callback) ->
-    sql = """
-      INSERT INTO #{operations_table} ("doc", "op", "v", "meta")
-        VALUES ($1, $2, $3, $4)
-    """
-    values = [docName, JSON.stringify(opData.op), opData.v, JSON.stringify(opData.meta)]
-    client.query sql, values, (error, result) ->
-      if !error?
-        callback?()
-      else
-        callback? error?.message
-
-  # Immediately try and create the database tables if need be. Its possible that a query
-  # which happens immediately will happen before the database has been initialized.
-  #
-  # But, its not really a big problem.
-  if options.create_tables_automatically
-    client.query "SELECT * from #{snapshot_table} LIMIT 0", (error, result) =>
-      @initialize() if error?.message.match "does not exist"
-
-  this

+ 0 - 140
services/web/public/coffee/ide/editor/sharejs/vendor/server/db/redis.coffee

@@ -1,140 +0,0 @@
-# This is an implementation of the OT data backend for redis.
-#   http://redis.io/
-#
-# This implementation isn't written to support multiple frontends
-# talking to a single redis backend using redis's transactions.
-
-redis = require 'redis'
-
-defaultOptions = {
-  # Prefix for all database keys.
-  prefix: 'ShareJS:'
-
-  # Inherit the default options from redis. (Hostname: 127.0.0.1, port: 6379)
-  hostname: null
-  port: null
-  redisOptions: null
-  auth: null
-
-  # If this is set to true, the client will select db 15 and wipe all data in
-  # this database.
-  testing: false
-}
-
-# Valid options as above.
-module.exports = RedisDb = (options) ->
-  return new Db if !(this instanceof RedisDb)
-
-  options ?= {}
-  options[k] ?= v for k, v of defaultOptions
-
-  keyForOps = (docName) -> "#{options.prefix}ops:#{docName}"
-  keyForDoc = (docName) -> "#{options.prefix}doc:#{docName}"
-
-  client = redis.createClient options.port, options.hostname, options.redisOptions
-
-  if options.auth and typeof options.auth == "string"
-    client.auth(if ":" in options.auth then options.auth.split(":").pop() else options.auth)
-
-  client.select 15 if options.testing
-
-  # Creates a new document.
-  # data = {snapshot, type:typename, [meta]}
-  # calls callback(true) if the document was created or callback(false) if a document with that name
-  # already exists.
-  @create = (docName, data, callback) ->
-    value = JSON.stringify(data)
-    client.setnx keyForDoc(docName), value, (err, result) ->
-      return callback? err if err
-
-      if result
-        callback?()
-      else
-        callback? 'Document already exists'
-
-  # Get all ops with version = start to version = end. Noninclusive.
-  # end is trimmed to the size of the document.
-  # If any documents are passed to the callback, the first one has v = start
-  # end can be null. If so, returns all documents from start onwards.
-  # Each document returned is in the form {op:o, meta:m, v:version}.
-  @getOps = (docName, start, end, callback) ->
-    if start == end
-      callback null, []
-      return
-
-    # In redis, lrange values are inclusive.
-    if end?
-      end--
-    else
-      end = -1
-
-    client.lrange keyForOps(docName), start, end, (err, values) ->
-      throw err if err?
-      ops = (JSON.parse value for value in values)
-      callback null, ops
-
-  # Write an op to a document.
-  #
-  # opData = {op:the op to append, v:version, meta:optional metadata object containing author, etc.}
-  # callback = callback when op committed
-  # 
-  # opData.v MUST be the subsequent version for the document.
-  #
-  # This function has UNDEFINED BEHAVIOUR if you call append before calling create().
-  # (its either that, or I have _another_ check when you append an op that the document already exists
-  # ... and that would slow it down a bit.)
-  @writeOp = (docName, opData, callback) ->
-    # ****** NOT SAFE FOR MULTIPLE PROCESSES. Rewrite me using transactions or something.
-
-    # The version isn't stored.
-    json = JSON.stringify {op:opData.op, meta:opData.meta}
-    client.rpush keyForOps(docName), json, (err, response) ->
-      return callback err if err
-
-      if response == opData.v + 1
-        callback()
-      else
-        # The document has been corrupted by the change. For now, throw an exception.
-        # Later, rebuild the snapshot.
-        callback "Version mismatch in db.append. '#{docName}' is corrupted."
-    
-  # Write new snapshot data to the database.
-  #
-  # docData = resultant document snapshot data. {snapshot:s, type:t, meta}
-  #
-  # The callback just takes an optional error.
-  #
-  # This function has UNDEFINED BEHAVIOUR if you call append before calling create().
-  @writeSnapshot = (docName, docData, dbMeta, callback) ->
-    client.set keyForDoc(docName), JSON.stringify(docData), (err, response) ->
-      callback? err
-
-  # Data = {v, snapshot, type}. Snapshot == null and v = 0 if the document doesn't exist.
-  @getSnapshot = (docName, callback) ->
-    client.get keyForDoc(docName), (err, response) ->
-      throw err if err?
-
-      if response != null
-        docData = JSON.parse(response)
-        callback null, docData
-      else
-        callback 'Document does not exist'
-
-  # Perminantly deletes a document. There is no undo.
-  # Callback takes a single argument which is true iff something was deleted.
-  @delete = (docName, dbMeta, callback) ->
-    client.del keyForOps(docName)
-    client.del keyForDoc(docName), (err, response) ->
-      throw err if err?
-      if callback
-        if response == 1
-          # Something was deleted.
-          callback null
-        else
-          callback 'Document does not exist'
-  
-  # Close the connection to the database
-  @close = ->
-    client.quit()
-
-  this

+ 0 - 57
services/web/public/coffee/ide/editor/sharejs/vendor/server/index.coffee

@@ -1,57 +0,0 @@
-# The server module...
-
-connect = require 'connect'
-
-Model = require './model'
-createDb = require './db'
-
-rest = require './rest'
-socketio = require './socketio'
-browserChannel = require './browserchannel'
-
-# Create an HTTP server and attach whatever frontends are specified in the options.
-#
-# The model will be created based on options if it is not specified.
-module.exports = create = (options, model = createModel(options)) ->
-  attach(connect(), options, model)
-
-# Create an OT document model attached to a database.
-create.createModel = createModel = (options) ->
-  dbOptions = options?.db
-
-  db = createDb dbOptions
-  new Model db, options
-
-# Attach the OT server frontends to the provided Node HTTP server. Use this if you
-# already have a http.Server or https.Server and want to make some URL paths do OT.
-#
-# The options object specifies options for everything. If settings are missing,
-# defaults will be provided.
-#
-# Set options.rest == null or options.socketio == null to turn off that frontend.
-create.attach = attach = (server, options, model = createModel(options)) ->
-  options ?= {}
-  options.staticpath ?= '/share'
-
-  server.model = model
-  server.on 'close', -> model.closeDb()
-
-  server.use options.staticpath, connect.static("#{__dirname}/../../webclient") if options.staticpath != null
-
-  createAgent = require('./useragent') model, options
-
-  # The client frontend doesn't get access to the model at all, to make sure security stuff is
-  # done properly.
-  server.use rest(createAgent, options.rest) if options.rest != null
-
-  # Socketio frontend is now disabled by default.
-  socketio.attach(server, createAgent, options.socketio or {}) if options.socketio?
-
-  if options.browserChannel != null
-    options.browserChannel ?= {}
-    #options.browserChannel.base ?= '/sjs'
-    options.browserChannel.server = server
-    server.use browserChannel(createAgent, options.browserChannel)
-
-  server
-

+ 0 - 602
services/web/public/coffee/ide/editor/sharejs/vendor/server/model.coffee

@@ -1,602 +0,0 @@
-# The model of all the ops. Responsible for applying & transforming remote deltas
-# and managing the storage layer.
-#
-# Actual storage is handled by the database wrappers in db/*, wrapped by DocCache
-
-{EventEmitter} = require 'events'
-
-queue = require './syncqueue'
-types = require '../types'
-
-isArray = (o) -> Object.prototype.toString.call(o) == '[object Array]'
-
-# This constructor creates a new Model object. There will be one model object
-# per server context.
-#
-# The model object is responsible for a lot of things:
-#
-# - It manages the interactions with the database
-# - It maintains (in memory) a set of all active documents
-# - It calls out to the OT functions when necessary
-#
-# The model is an event emitter. It emits the following events:
-#
-# create(docName, data): A document has been created with the specified name & data
-module.exports = Model = (db, options) ->
-  # db can be null if the user doesn't want persistance.
-
-  return new Model(db, options) if !(this instanceof Model)
-
-  model = this
-
-  options ?= {}
-
-  # This is a cache of 'live' documents.
-  #
-  # The cache is a map from docName -> {
-  #   ops:[{op, meta}]
-  #   snapshot
-  #   type
-  #   v
-  #   meta
-  #   eventEmitter
-  #   reapTimer
-  #   committedVersion: v
-  #   snapshotWriteLock: bool to make sure writeSnapshot isn't re-entrant
-  #   dbMeta: database specific data
-  #   opQueue: syncQueue for processing ops
-  # }
-  #
-  # The ops list contains the document's last options.numCachedOps ops. (Or all
-  # of them if we're using a memory store).
-  #
-  # Documents are stored in this set so long as the document has been accessed in
-  # the last few seconds (options.reapTime) OR at least one client has the document
-  # open. I don't know if I should keep open (but not being edited) documents live -
-  # maybe if a client has a document open but the document isn't being edited, I should
-  # flush it from the cache.
-  #
-  # In any case, the API to model is designed such that if we want to change that later
-  # it should be pretty easy to do so without any external-to-the-model code changes.
-  docs = {}
-
-  # This is a map from docName -> [callback]. It is used when a document hasn't been
-  # cached and multiple getSnapshot() / getVersion() requests come in. All requests
-  # are added to the callback list and called when db.getSnapshot() returns.
-  #
-  # callback(error, snapshot data)
-  awaitingGetSnapshot = {}
-
-  # The time that documents which no clients have open will stay in the cache.
-  # Should be > 0.
-  options.reapTime ?= 3000
-
-  # The number of operations the cache holds before reusing the space
-  options.numCachedOps ?= 10
-
-  # This option forces documents to be reaped, even when there's no database backend.
-  # This is useful when you don't care about persistance and don't want to gradually
-  # fill memory.
-  #
-  # You might want to set reapTime to a day or something.
-  options.forceReaping ?= false
-
-  # Until I come up with a better strategy, we'll save a copy of the document snapshot
-  # to the database every ~20 submitted ops.
-  options.opsBeforeCommit ?= 20
-
-  # It takes some processing time to transform client ops. The server will punt ops back to the
-  # client to transform if they're too old.
-  options.maximumAge ?= 40
-
-  # **** Cache API methods
-
-  # Its important that all ops are applied in order. This helper method creates the op submission queue
-  # for a single document. This contains the logic for transforming & applying ops.
-  makeOpQueue = (docName, doc) -> queue (opData, callback) ->
-    return callback 'Version missing' unless opData.v >= 0
-    return callback 'Op at future version' if opData.v > doc.v
-
-    # Punt the transforming work back to the client if the op is too old.
-    return callback 'Op too old' if opData.v + options.maximumAge < doc.v
-
-    opData.meta ||= {}
-    opData.meta.ts = Date.now()
-
-    # We'll need to transform the op to the current version of the document. This
-    # calls the callback immediately if opVersion == doc.v.
-    getOps docName, opData.v, doc.v, (error, ops) ->
-      return callback error if error
-
-      unless doc.v - opData.v == ops.length
-        # This should never happen. It indicates that we didn't get all the ops we
-        # asked for. Its important that the submitted op is correctly transformed.
-        console.error "Could not get old ops in model for document #{docName}"
-        console.error "Expected ops #{opData.v} to #{doc.v} and got #{ops.length} ops"
-        return callback 'Internal error'
-
-      if ops.length > 0
-        try
-          # If there's enough ops, it might be worth spinning this out into a webworker thread.
-          for oldOp in ops
-            # Dup detection works by sending the id(s) the op has been submitted with previously.
-            # If the id matches, we reject it. The client can also detect the op has been submitted
-            # already if it sees its own previous id in the ops it sees when it does catchup.
-            if oldOp.meta.source and opData.dupIfSource and oldOp.meta.source in opData.dupIfSource
-              return callback 'Op already submitted'
-
-            opData.op = doc.type.transform opData.op, oldOp.op, 'left'
-            opData.v++
-        catch error
-          console.error error.stack
-          return callback error.message
-
-      try
-        snapshot = doc.type.apply doc.snapshot, opData.op
-      catch error
-        console.error error.stack
-        return callback error.message
-
-      # The op data should be at the current version, and the new document data should be at
-      # the next version.
-      #
-      # This should never happen in practice, but its a nice little check to make sure everything
-      # is hunky-dory.
-      unless opData.v == doc.v
-        # This should never happen.
-        console.error "Version mismatch detected in model. File a ticket - this is a bug."
-        console.error "Expecting #{opData.v} == #{doc.v}"
-        return callback 'Internal error'
-
-      #newDocData = {snapshot, type:type.name, v:opVersion + 1, meta:docData.meta}
-      writeOp = db?.writeOp or (docName, newOpData, callback) -> callback()
-
-      writeOp docName, opData, (error) ->
-        if error
-          # The user should probably know about this.
-          console.warn "Error writing ops to database: #{error}"
-          return callback error
-
-        options.stats?.writeOp?()
-
-        # This is needed when we emit the 'change' event, below.
-        oldSnapshot = doc.snapshot
-
-        # All the heavy lifting is now done. Finally, we'll update the cache with the new data
-        # and (maybe!) save a new document snapshot to the database.
-
-        doc.v = opData.v + 1
-        doc.snapshot = snapshot
-
-        doc.ops.push opData
-        doc.ops.shift() if db and doc.ops.length > options.numCachedOps
-
-        model.emit 'applyOp', docName, opData, snapshot, oldSnapshot
-        doc.eventEmitter.emit 'op', opData, snapshot, oldSnapshot
-
-        # The callback is called with the version of the document at which the op was applied.
-        # This is the op.v after transformation, and its doc.v - 1.
-        callback null, opData.v
-
-        # I need a decent strategy here for deciding whether or not to save the snapshot.
-        #
-        # The 'right' strategy looks something like "Store the snapshot whenever the snapshot
-        # is smaller than the accumulated op data". For now, I'll just store it every 20
-        # ops or something. (Configurable with doc.committedVersion)
-        if !doc.snapshotWriteLock and doc.committedVersion + options.opsBeforeCommit <= doc.v
-          tryWriteSnapshot docName, (error) ->
-            console.warn "Error writing snapshot #{error}. This is nonfatal" if error
-
-  # Add the data for the given docName to the cache. The named document shouldn't already
-  # exist in the doc set.
-  #
-  # Returns the new doc.
-  add = (docName, error, data, committedVersion, ops, dbMeta) ->
-    callbacks = awaitingGetSnapshot[docName]
-    delete awaitingGetSnapshot[docName]
-
-    if error
-      callback error for callback in callbacks if callbacks
-    else
-      doc = docs[docName] =
-        snapshot: data.snapshot
-        v: data.v
-        type: data.type
-        meta: data.meta
-
-        # Cache of ops
-        ops: ops or []
-
-        eventEmitter: new EventEmitter
-
-        # Timer before the document will be invalidated from the cache (if the document has no
-        # listeners)
-        reapTimer: null
-
-        # Version of the snapshot thats in the database
-        committedVersion: committedVersion ? data.v
-        snapshotWriteLock: false
-        dbMeta: dbMeta
-
-      doc.opQueue = makeOpQueue docName, doc
-
-      refreshReapingTimeout docName
-      model.emit 'add', docName, data
-      callback null, doc for callback in callbacks if callbacks
-
-    doc
-
-  # This is a little helper wrapper around db.getOps. It does two things:
-  #
-  # - If there's no database set, it returns an error to the callback
-  # - It adds version numbers to each op returned from the database
-  # (These can be inferred from context so the DB doesn't store them, but its useful to have them).
-  getOpsInternal = (docName, start, end, callback) ->
-    return callback? 'Document does not exist' unless db
-
-    db.getOps docName, start, end, (error, ops) ->
-      return callback? error if error
-
-      v = start
-      op.v = v++ for op in ops
-
-      callback? null, ops
-
-  # Load the named document into the cache. This function is re-entrant.
-  #
-  # The callback is called with (error, doc)
-  load = (docName, callback) ->
-    if docs[docName]
-      # The document is already loaded. Return immediately.
-      options.stats?.cacheHit? 'getSnapshot'
-      return callback null, docs[docName]
-
-    # We're a memory store. If we don't have it, nobody does.
-    return callback 'Document does not exist' unless db
-
-    callbacks = awaitingGetSnapshot[docName]
-
-    # The document is being loaded already. Add ourselves as a callback.
-    return callbacks.push callback if callbacks
-
-    options.stats?.cacheMiss? 'getSnapshot'
-
-    # The document isn't loaded and isn't being loaded. Load it.
-    awaitingGetSnapshot[docName] = [callback]
-    db.getSnapshot docName, (error, data, dbMeta) ->
-      return add docName, error if error
-
-      type = types[data.type]
-      unless type
-        console.warn "Type '#{data.type}' missing"
-        return callback "Type not found"
-      data.type = type
-
-      committedVersion = data.v
-
-      # The server can close without saving the most recent document snapshot.
-      # In this case, there are extra ops which need to be applied before
-      # returning the snapshot.
-      getOpsInternal docName, data.v, null, (error, ops) ->
-        return callback error if error
-
-        if ops.length > 0
-          console.log "Catchup #{docName} #{data.v} -> #{data.v + ops.length}"
-
-          try
-            for op in ops
-              data.snapshot = type.apply data.snapshot, op.op
-              data.v++
-          catch e
-            # This should never happen - it indicates that whats in the
-            # database is invalid.
-            console.error "Op data invalid for #{docName}: #{e.stack}"
-            return callback 'Op data invalid'
-
-        model.emit 'load', docName, data
-        add docName, error, data, committedVersion, ops, dbMeta
-
-  # This makes sure the cache contains a document. If the doc cache doesn't contain
-  # a document, it is loaded from the database and stored.
-  #
-  # Documents are stored so long as either:
-  # - They have been accessed within the past #{PERIOD}
-  # - At least one client has the document open
-  refreshReapingTimeout = (docName) ->
-    doc = docs[docName]
-    return unless doc
-
-    # I want to let the clients list be updated before this is called.
-    process.nextTick ->
-      # This is an awkward way to find out the number of clients on a document. If this
-      # causes performance issues, add a numClients field to the document.
-      #
-      # The first check is because its possible that between refreshReapingTimeout being called and this
-      # event being fired, someone called delete() on the document and hence the doc is something else now.
-      if doc == docs[docName] and
-          doc.eventEmitter.listeners('op').length == 0 and
-          (db or options.forceReaping) and
-          doc.opQueue.busy is false
-
-        clearTimeout doc.reapTimer
-        doc.reapTimer = reapTimer = setTimeout ->
-            tryWriteSnapshot docName, ->
-              # If the reaping timeout has been refreshed while we're writing the snapshot, or if we're
-              # in the middle of applying an operation, don't reap.
-              delete docs[docName] if docs[docName].reapTimer is reapTimer and doc.opQueue.busy is false
-          , options.reapTime
-
-  tryWriteSnapshot = (docName, callback) ->
-    return callback?() unless db
-
-    doc = docs[docName]
-
-    # The doc is closed
-    return callback?() unless doc
-
-    # The document is already saved.
-    return callback?() if doc.committedVersion is doc.v
-
-    return callback? 'Another snapshot write is in progress' if doc.snapshotWriteLock
-
-    doc.snapshotWriteLock = true
-
-    options.stats?.writeSnapshot?()
-
-    writeSnapshot = db?.writeSnapshot or (docName, docData, dbMeta, callback) -> callback()
-
-    data =
-      v: doc.v
-      meta: doc.meta
-      snapshot: doc.snapshot
-      # The database doesn't know about object types.
-      type: doc.type.name
-
-    # Commit snapshot.
-    writeSnapshot docName, data, doc.dbMeta, (error, dbMeta) ->
-      doc.snapshotWriteLock = false
-
-      # We have to use data.v here because the version in the doc could
-      # have been updated between the call to writeSnapshot() and now.
-      doc.committedVersion = data.v
-      doc.dbMeta = dbMeta
-
-      callback? error
-
-  # *** Model interface methods
-
-  # Create a new document.
-  #
-  # data should be {snapshot, type, [meta]}. The version of a new document is 0.
-  @create = (docName, type, meta, callback) ->
-    [meta, callback] = [{}, meta] if typeof meta is 'function'
-
-    return callback? 'Invalid document name' if /\//.test(docName)
-    return callback? 'Document already exists' if docs[docName]
-
-    type = types[type] if typeof type == 'string'
-    return callback? 'Type not found' unless type
-
-    data =
-      snapshot:type.create()
-      type:type.name
-      meta:meta or {}
-      v:0
-
-    done = (error, dbMeta) ->
-      # dbMeta can be used to cache extra state needed by the database to access the document, like an ID or something.
-      return callback? error if error
-
-      # From here on we'll store the object version of the type name.
-      data.type = type
-      add docName, null, data, 0, [], dbMeta
-      model.emit 'create', docName, data
-      callback?()
-
-    if db
-      db.create docName, data, done
-    else
-      done()
-
-  # Perminantly deletes the specified document.
-  # If listeners are attached, they are removed.
-  #
-  # The callback is called with (error) if there was an error. If error is null / undefined, the
-  # document was deleted.
-  #
-  # WARNING: This isn't well supported throughout the code. (Eg, streaming clients aren't told about the
-  # deletion. Subsequent op submissions will fail).
-  @delete = (docName, callback) ->
-    doc = docs[docName]
-
-    if doc
-      clearTimeout doc.reapTimer
-      delete docs[docName]
-
-    done = (error) ->
-      model.emit 'delete', docName unless error
-      callback? error
-
-    if db
-      db.delete docName, doc?.dbMeta, done
-    else
-      done (if !doc then 'Document does not exist')
-
-  # This gets all operations from [start...end]. (That is, its not inclusive.)
-  #
-  # end can be null. This means 'get me all ops from start'.
-  #
-  # Each op returned is in the form {op:o, meta:m, v:version}.
-  #
-  # Callback is called with (error, [ops])
-  #
-  # If the document does not exist, getOps doesn't necessarily return an error. This is because
-  # its awkward to figure out whether or not the document exists for things
-  # like the redis database backend. I guess its a bit gross having this inconsistant
-  # with the other DB calls, but its certainly convenient.
-  #
-  # Use getVersion() to determine if a document actually exists, if thats what you're
-  # after.
-  @getOps = getOps = (docName, start, end, callback) ->
-    # getOps will only use the op cache if its there. It won't fill the op cache in.
-    throw new Error 'start must be 0+' unless start >= 0
-
-    [end, callback] = [null, end] if typeof end is 'function'
-
-    ops = docs[docName]?.ops
-
-    if ops
-      version = docs[docName].v
-
-      # Ops contains an array of ops. The last op in the list is the last op applied
-      end ?= version
-      start = Math.min start, end
-
-      return callback null, [] if start == end
-
-      # Base is the version number of the oldest op we have cached
-      base = version - ops.length
-
-      # If the database is null, we'll trim to the ops we do have and hope thats enough.
-      if start >= base or db is null
-        refreshReapingTimeout docName
-        options.stats?.cacheHit 'getOps'
-
-        return callback null, ops[(start - base)...(end - base)]
-
-    options.stats?.cacheMiss 'getOps'
-
-    getOpsInternal docName, start, end, callback
-
-  # Gets the snapshot data for the specified document.
-  # getSnapshot(docName, callback)
-  # Callback is called with (error, {v: <version>, type: <type>, snapshot: <snapshot>, meta: <meta>})
-  @getSnapshot = (docName, callback) ->
-    load docName, (error, doc) ->
-      callback error, if doc then {v:doc.v, type:doc.type, snapshot:doc.snapshot, meta:doc.meta}
-
-  # Gets the latest version # of the document.
-  # getVersion(docName, callback)
-  # callback is called with (error, version).
-  @getVersion = (docName, callback) ->
-    load docName, (error, doc) -> callback error, doc?.v
-
-  # Apply an op to the specified document.
-  # The callback is passed (error, applied version #)
-  # opData = {op:op, v:v, meta:metadata}
-  #
-  # Ops are queued before being applied so that the following code applies op C before op B:
-  # model.applyOp 'doc', OPA, -> model.applyOp 'doc', OPB
-  # model.applyOp 'doc', OPC
-  @applyOp = (docName, opData, callback) ->
-    # All the logic for this is in makeOpQueue, above.
-    load docName, (error, doc) ->
-      return callback error if error
-
-      process.nextTick -> doc.opQueue opData, (error, newVersion) ->
-        refreshReapingTimeout docName
-        callback? error, newVersion
-
-  # TODO: store (some) metadata in DB
-  # TODO: op and meta should be combineable in the op that gets sent
-  @applyMetaOp = (docName, metaOpData, callback) ->
-    {path, value} = metaOpData.meta
-
-    return callback? "path should be an array" unless isArray path
-
-    load docName, (error, doc) ->
-      if error?
-        callback? error
-      else
-        applied = false
-        switch path[0]
-          when 'shout'
-            doc.eventEmitter.emit 'op', metaOpData
-            applied = true
-
-        model.emit 'applyMetaOp', docName, path, value if applied
-        callback? null, doc.v
-
-  # Listen to all ops from the specified version. If version is in the past, all
-  # ops since that version are sent immediately to the listener.
-  #
-  # The callback is called once the listener is attached, but before any ops have been passed
-  # to the listener.
-  #
-  # This will _not_ edit the document metadata.
-  #
-  # If there are any listeners, we don't purge the document from the cache. But be aware, this behaviour
-  # might change in a future version.
-  #
-  # version is the document version at which the document is opened. It can be left out if you want to open
-  # the document at the most recent version.
-  #
-  # listener is called with (opData) each time an op is applied.
-  #
-  # callback(error, openedVersion)
-  @listen = (docName, version, listener, callback) ->
-    [version, listener, callback] = [null, version, listener] if typeof version is 'function'
-
-    load docName, (error, doc) ->
-      return callback? error if error
-
-      clearTimeout doc.reapTimer
-
-      if version?
-        getOps docName, version, null, (error, data) ->
-          return callback? error if error
-
-          doc.eventEmitter.on 'op', listener
-          callback? null, version
-          for op in data
-            listener op
-
-            # The listener may well remove itself during the catchup phase. If this happens, break early.
-            # This is done in a quite inefficient way. (O(n) where n = #listeners on doc)
-            break unless listener in doc.eventEmitter.listeners 'op'
-
-      else # Version is null / undefined. Just add the listener.
-        doc.eventEmitter.on 'op', listener
-        callback? null, doc.v
-
-  # Remove a listener for a particular document.
-  #
-  # removeListener(docName, listener)
-  #
-  # This is synchronous.
-  @removeListener = (docName, listener) ->
-    # The document should already be loaded.
-    doc = docs[docName]
-    throw new Error 'removeListener called but document not loaded' unless doc
-
-    doc.eventEmitter.removeListener 'op', listener
-    refreshReapingTimeout docName
-
-  # Flush saves all snapshot data to the database. I'm not sure whether or not this is actually needed -
-  # sharejs will happily replay uncommitted ops when documents are re-opened anyway.
-  @flush = (callback) ->
-    return callback?() unless db
-
-    pendingWrites = 0
-
-    for docName, doc of docs
-      if doc.committedVersion < doc.v
-        pendingWrites++
-        # I'm hoping writeSnapshot will always happen in another thread.
-        tryWriteSnapshot docName, ->
-          process.nextTick ->
-            pendingWrites--
-            callback?() if pendingWrites is 0
-
-    # If nothing was queued, terminate immediately.
-    callback?() if pendingWrites is 0
-
-  # Close the database connection. This is needed so nodejs can shut down cleanly.
-  @closeDb = ->
-    db?.close?()
-    db = null
-
-  return
-
-# Model inherits from EventEmitter.
-Model:: = new EventEmitter

+ 0 - 148
services/web/public/coffee/ide/editor/sharejs/vendor/server/rest.coffee

@@ -1,148 +0,0 @@
-# A REST-ful frontend to the OT server.
-#
-# See the docs for details and examples about how the protocol works.
-
-http = require 'http'
-url = require 'url'
-
-connect = require 'connect'
-
-send403 = (res, message = 'Forbidden\n') ->
-  res.writeHead 403, {'Content-Type': 'text/plain'}
-  res.end message
-
-send404 = (res, message = '404: Your document could not be found.\n') ->
-  res.writeHead 404, {'Content-Type': 'text/plain'}
-  res.end message
-
-sendError = (res, message, head = false) ->
-  if message == 'forbidden'
-    if head
-      send403 res, ""
-    else
-      send403 res
-  else if message == 'Document does not exist'
-    if head
-      send404 res, ""
-    else
-      send404 res
-  else
-    console.warn "REST server does not know how to send error: '#{message}'"
-    if head
-      res.writeHead 500, {'Content-Type': 'text/plain'}
-      res.end "Error: #{message}\n"
-    else
-      res.writeHead 500, {}
-      res.end ""
-
-send400 = (res, message) ->
-  res.writeHead 400, {'Content-Type': 'text/plain'}
-  res.end message
-
-send200 = (res, message = "OK\n") ->
-  res.writeHead 200, {'Content-Type': 'text/plain'}
-  res.end message
-
-sendJSON = (res, obj) ->
-  res.writeHead 200, {'Content-Type': 'application/json'}
-  res.end JSON.stringify(obj) + '\n'
-
-# Callback is only called if the object was indeed JSON
-expectJSONObject = (req, res, callback) ->
-  pump req, (data) ->
-    try
-      obj = JSON.parse data
-    catch error
-      send400 res, 'Supplied JSON invalid'
-      return
-
-    callback(obj)
-
-pump = (req, callback) ->
-  data = ''
-  req.on 'data', (chunk) -> data += chunk
-  req.on 'end', () -> callback(data)
-
-# connect.router will be removed in connect 2.0 - this code will have to be rewritten or
-# more libraries pulled in.
-# https://github.com/senchalabs/connect/issues/262
-router = (app, createClient, options) ->
-  auth = (req, res, next) ->
-    data =
-      headers: req.headers
-      remoteAddress: req.connection.remoteAddress
-
-    createClient data, (error, client) ->
-      if client
-        req._client = client
-        next()
-      else
-        sendError res, error
-
-  # GET returns the document snapshot. The version and type are sent as headers.
-  # I'm not sure what to do with document metadata - it is inaccessable for now.
-  app.get '/doc/:name', auth, (req, res) ->
-    req._client.getSnapshot req.params.name, (error, doc) ->
-      if doc  
-        res.setHeader 'X-OT-Type', doc.type.name
-        res.setHeader 'X-OT-Version', doc.v
-        if req.method == "HEAD"
-          send200 res, ""
-        else
-          if typeof doc.snapshot == 'string'
-            send200 res, doc.snapshot
-          else
-            sendJSON res, doc.snapshot
-      else
-        if req.method == "HEAD"
-          sendError res, error, true
-        else
-          sendError res, error
-          
-  # Put is used to create a document. The contents are a JSON object with {type:TYPENAME, meta:{...}}
-  app.put '/doc/:name', auth, (req, res) ->
-    expectJSONObject req, res, (obj) ->
-      type = obj?.type
-      meta = obj?.meta
-
-      unless typeof type == 'string' and (meta == undefined or typeof meta == 'object')
-        send400 res, 'Type invalid'
-      else
-        req._client.create req.params.name, type, meta, (error) ->
-          if error
-            sendError res, error
-          else
-            send200 res
-
-  # POST submits an op to the document.
-  app.post '/doc/:name', auth, (req, res) ->
-    query = url.parse(req.url, true).query
-
-    version = if query?.v?
-      parseInt query?.v
-    else
-      parseInt req.headers['x-ot-version']
-    
-    unless version? and version >= 0
-      send400 res, 'Version required - attach query parameter ?v=X on your URL or set the X-OT-Version header'
-    else
-      expectJSONObject req, res, (obj) ->
-        opData = {v:version, op:obj, meta:{source:req.socket.remoteAddress}}
-        req._client.submitOp req.params.name, opData, (error, newVersion) ->
-          if error?
-            sendError res, error
-          else
-            sendJSON res, {v:newVersion}
-
-  app.delete '/doc/:name', auth, (req, res) ->
-    req._client.delete req.params.name, (error) ->
-      if error
-        sendError res, error
-      else
-        send200 res
-
-# Attach the frontend to the supplied http.Server.
-# 
-# As of sharejs 0.4.0, options is ignored. To control the deleting of documents, specify an auth() function.
-module.exports = (createClient, options) ->
-  connect.router (app) -> router(app, createClient, options)

+ 0 - 336
services/web/public/coffee/ide/editor/sharejs/vendor/server/socketio.coffee

@@ -1,336 +0,0 @@
-# This implements the socketio-based network API for ShareJS.
-#
-# This is the frontend used by the javascript socket implementation.
-#
-# See documentation for this protocol is in doc/protocol.md
-# Tests are in test/socketio.coffee
-#
-# This code will be removed in a future version of sharejs because socket.io is
-# too buggy.
-
-socketio = require 'socket.io'
-util = require 'util'
-hat = require 'hat'
-
-p = ->#util.debug
-i = ->#util.inspect
-
-# Attach the streaming protocol to the supplied http.Server.
-#
-# Options = {}
-exports.attach = (server, createClient, options) ->
-  io = socketio.listen server
-
-  io.configure ->
-    io.set 'log level', 1
-    for option of options
-      io.set option, options[option]
-
-  authClient = (handshakeData, callback) ->
-    data =
-      headers: handshakeData.headers
-      remoteAddress: handshakeData.address.address
-      secure: handshakeData.secure
-
-    createClient data, (error, client) ->
-      if error
-        # Its important that we don't pass the error message to the client here - leaving it as null
-        # will ensure the client recieves the normal 'not_authorized' message and thus
-        # emits 'connect_failed' instead of 'error'
-        callback null, false
-      else
-        handshakeData.client = client
-        callback null, true
-
-  io.of('/sjs').authorization(authClient).on 'connection', (socket) ->
-    client = socket.handshake.client
-
-    # There seems to be a bug in socket.io where socket.request isn't set sometimes.
-    p "New socket connected from #{socket.request.socket.remoteAddress} with id #{socket.id}" if socket.request?
-
-    lastSentDoc = null
-    lastReceivedDoc = null
-
-    # Map from docName -> {listener:fn, queue:[msg], busy:bool}
-    docState = {}
-    closed = false
-
-    # Send a message to the socket.
-    # msg _must_ have the doc:DOCNAME property set. We'll remove it if its the same as lastReceivedDoc.
-    send = (msg) ->
-      if msg.doc == lastSentDoc
-        delete msg.doc
-      else
-        lastSentDoc = msg.doc
-
-      p "Sending #{i msg}"
-      socket.json.send msg
-
-    # Open the given document name, at the requested version.
-    # callback(error, version)
-    open = (docName, version, callback) ->
-      callback 'Doc already opened' if docState[docName].listener?
-      p "Registering listener on #{docName} by #{socket.id} at #{version}"
-
-      # This passes op events to the client
-      docState[docName].listener = listener = (opData) ->
-        throw new Error 'Consistency violation - doc listener invalid' unless docState[docName].listener == listener
-
-        p "listener doc:#{docName} opdata:#{i opData} v:#{version}"
-
-        # Skip the op if this socket sent it.
-        return if opData.meta?.source == client.id
-
-        opMsg =
-          doc: docName
-          op: opData.op
-          v: opData.v
-          meta: opData.meta
-
-        send opMsg
-      
-      # Tell the socket the doc is open at the requested version
-      client.listen docName, version, listener, (error, v) ->
-        delete docState[docName].listener if error
-        callback error, v
-
-    # Close the named document.
-    # callback([error])
-    close = (docName, callback) ->
-      p "Closing #{docName}"
-      listener = docState[docName].listener
-      return callback 'Doc already closed' unless listener?
-
-      client.removeListener docName
-      docState[docName].listener = null
-      callback()
-
-    # Handles messages with any combination of the open:true, create:true and snapshot:null parameters
-    handleOpenCreateSnapshot = (query, finished) ->
-      docName = query.doc
-      msg = doc:docName
-
-      callback = (error) ->
-        if error
-          close(docName) if msg.open == true
-          msg.open = false if query.open == true
-          msg.snapshot = null if query.snapshot != undefined
-          delete msg.create
-
-          msg.error = error
-
-        send msg
-        finished()
-
-      return callback 'No docName specified' unless query.doc?
-
-      if query.create == true
-        if typeof query.type != 'string'
-          return callback 'create:true requires type specified'
-
-      if query.meta != undefined
-        unless typeof query.meta == 'object' and Array.isArray(query.meta) == false
-          return callback 'meta must be an object'
-
-      docData = undefined
-      # Technically, we don't need a snapshot if the user called create but not open or createSnapshot,
-      # but no clients do that yet anyway.
-      #
-      # It might be nice to add a 'createOrGet()' method to model / db manager. But most
-      # of the time clients are opening an existing document rather than creating a new one anyway.
-      ###
-      model.clientGetSnapshot client, query.doc, (error, data) ->
-        maybeCreate = (callback) ->
-          if query.create and error is 'Document does not exist'
-            model.clientCreate client, docName, query.type, query.meta or {}, callback
-          else
-            callback error, data
-
-        maybeCreate (error, data) ->
-          if query.create
-            msg.create = !!error
-          if error is 'Document already exists'
-            msg.create = false
-          else if error and (!msg.create or error isnt 'Document already exists')
-            # This is the real final callback, to say an error has occurred.
-            return callback error
-          else if query.create or query.snapshot is null
-
-
-          if query.snapshot isnt null
-      ###
-
-      # This is implemented with a series of cascading methods for each different type of
-      # thing this method can handle. This would be so much nicer with an async library. Welcome to
-      # callback hell.
-
-      step1Create = ->
-        return step2Snapshot() if query.create != true
-
-        # The document obviously already exists if we have a snapshot.
-        if docData
-          msg.create = false
-          step2Snapshot()
-        else
-          client.create docName, query.type, query.meta || {}, (error) ->
-            if error is 'Document already exists'
-              # We've called getSnapshot (-> null), then createClient (-> already exists). Its possible
-              # another client has called createClient first.
-              client.getSnapshot docName, (error, data) ->
-                return callback error if error
-
-                docData = data
-                msg.create = false
-                step2Snapshot()
-            else if error
-              callback error
-            else
-              msg.create = !error
-              step2Snapshot()
-
-      # The socket requested a document snapshot
-      step2Snapshot = ->
-        # Skip inserting a snapshot if the document was just created.
-        if query.snapshot != null or msg.create == true
-          step3Open()
-          return
-
-        if docData
-          msg.v = docData.v
-          msg.type = docData.type.name unless query.type == docData.type.name
-          msg.snapshot = docData.snapshot
-        else
-          return callback 'Document does not exist'
-
-        step3Open()
-
-      # Attempt to open a document with a given name. Version is optional.
-      # callback(opened at version) or callback(null, errormessage)
-      step3Open = ->
-        return callback() if query.open != true
-
-        # Verify the type matches
-        return callback 'Type mismatch' if query.type and docData and query.type != docData.type.name
-
-        open docName, query.v, (error, version) ->
-          return callback error if error
-
-          # + Should fail if the type is wrong.
-
-          p "Opened #{docName} at #{version} by #{socket.id}"
-          msg.open = true
-          msg.v = version
-          callback()
-
-      if query.snapshot == null or (query.open == true and query.type)
-        client.getSnapshot query.doc, (error, data) ->
-          return callback error if error and error != 'Document does not exist'
-
-          docData = data
-          step1Create()
-      else
-        step1Create()
-
-    # The socket closes a document
-    handleClose = (query, callback) ->
-      close query.doc, (error) ->
-        if error
-          # An error closing still results in the doc being closed.
-          send {doc:query.doc, open:false, error:error}
-        else
-          send {doc:query.doc, open:false}
-
-        callback()
-
-    # We received an op from the socket
-    handleOp = (query, callback) ->
-      throw new Error 'No docName specified' unless query.doc?
-      throw new Error 'No version specified' unless query.v? or (query.meta?.path? and query.meta?.value?)
-
-      op_data = {v:query.v, op:query.op}
-      op_data.meta = query.meta || {}
-      op_data.meta.source = socket.id
-
-      client.submitOp query.doc, op_data, (error, appliedVersion) ->
-        msg = if error
-          p "Sending error to socket: #{error}"
-          {doc:query.doc, v:null, error:error}
-        else
-          {doc:query.doc, v:appliedVersion}
-
-        p "sending #{i msg}"
-        send msg
-        callback()
-
-    flush = (state) ->
-      return if state.busy || state.queue.length == 0
-      state.busy = true
-
-      query = state.queue.shift()
-
-      callback = ->
-        state.busy = false
-        flush state
-
-      p "processing query #{i query}"
-      try
-        if query.open == false
-          handleClose query, callback
-
-        else if query.open != undefined or query.snapshot != undefined or query.create
-          # You can open, request a snapshot and create all in the same
-          # request. They're all handled together.
-          handleOpenCreateSnapshot query, callback
-
-        else if query.op? or query.meta? # The socket is applying an op.
-          handleOp query, callback
-
-        else
-          util.debug "Unknown message received: #{util.inspect query}"
-
-      catch error
-        util.debug error.stack
-        # ... And disconnect the socket?
-        callback()
-    
-    # And now the actual message handler.
-    messageListener = (query) ->
-      p "Server recieved message #{i query}"
-      # There seems to be a bug in socket.io where messages are detected
-      # after the client disconnects.
-      if closed
-        console.warn "WARNING: received query from socket after the socket disconnected."
-        console.warn socket
-        return
-
-      try
-        query = JSON.parse query if typeof(query) == 'string'
-
-        if query.doc == null
-          lastReceivedDoc = null
-          query.doc = hat()
-        else if query.doc != undefined
-          lastReceivedDoc = query.doc
-        else
-          throw new Error 'msg.doc missing. Probably the client reconnected without telling us - this is a socket.io bug.' unless lastReceivedDoc
-          query.doc = lastReceivedDoc
-      catch error
-        util.debug error.stack
-        return
-
-      docState[query.doc] ||= {listener:null, queue:[], busy:no}
-      docState[query.doc].queue.push query
-      flush docState[query.doc]
-
-    socket.on 'message', messageListener
-    socket.on 'disconnect', ->
-      p "socket #{socket.id} disconnected"
-      closed = true
-      for docName, state of docState
-        state.busy = true
-        state.queue = []
-        client.removeListener docName if state.listener?
-      socket.removeListener 'message', messageListener
-      docState = {}
-  
-  server

+ 0 - 42
services/web/public/coffee/ide/editor/sharejs/vendor/server/syncqueue.coffee

@@ -1,42 +0,0 @@
-# A synchronous processing queue. The queue calls process on the arguments,
-# ensuring that process() is only executing once at a time.
-#
-# process(data, callback) _MUST_ eventually call its callback.
-#
-# Example:
-#
-# queue = require 'syncqueue'
-#
-# fn = queue (data, callback) ->
-#     asyncthing data, ->
-#         callback(321)
-#
-# fn(1)
-# fn(2)
-# fn(3, (result) -> console.log(result))
-#
-#   ^--- async thing will only be running once at any time.
-
-module.exports = (process) ->
-  throw new Error('process is not a function') unless typeof process == 'function'
-  queue = []
-  
-  enqueue = (data, callback) ->
-    queue.push [data, callback]
-    flush()
-
-  enqueue.busy = false
-
-  flush = ->
-    return if enqueue.busy or queue.length == 0
-
-    enqueue.busy = true
-    [data, callback] = queue.shift()
-    process data, (result...) -> # TODO: Make this not use varargs - varargs are really slow.
-      enqueue.busy = false
-      # This is called after busy = false so a user can check if enqueue.busy is set in the callback.
-      callback.apply null, result if callback
-      flush()
-
-  enqueue
-

+ 0 - 146
services/web/public/coffee/ide/editor/sharejs/vendor/server/useragent.coffee

@@ -1,146 +0,0 @@
-# A useragent is assigned to each client when the client connects. The useragent is responsible for making
-# sure all requests are authorized and maintaining document metadata.
-#
-# This is used by all the client frontends to interact with the server.
-
-hat = require 'hat'
-types = require '../types'
-
-# This module exports a function which you can call with the model and options. Calling the function
-# returns _another_ function which you can call when clients connect.
-module.exports = (model, options) ->
-  # By default, accept all connections + data submissions.
-  # Don't let anyone delete documents though.
-  auth = options.auth or (agent, action) ->
-    if action.type in ['connect', 'read', 'create', 'update'] then action.accept() else action.reject()
-
-  class UserAgent
-    constructor: (data) ->
-      @sessionId = hat()
-      @connectTime = new Date
-      @headers = data.headers
-      @remoteAddress = data.remoteAddress
-
-      # This is a map from docName -> listener function
-      @listeners = {}
-
-      # Should be manually set by the auth function.
-      @name = null
-
-      # We have access to these with socket.io, but I'm not sure we can support
-      # these properties on the REST API or sockjs, etc.
-      #xdomain: data.xdomain
-      #secure: data.secure
-
-    # This is a helper method which wraps auth() above. It creates the action and calls
-    # auth. If authentication succeeds, acceptCallback() is called if it exists. otherwise
-    # userCallback(true) is called.
-    #
-    # If authentication fails, userCallback('forbidden', null) is called.
-    #
-    # If supplied, actionData is turned into the action object passed to auth.
-    doAuth: (actionData, name, userCallback, acceptCallback) ->
-      action = actionData || {}
-      action.name = name
-      action.type = switch name
-        when 'connect' then 'connect'
-        when 'create' then 'create'
-        when 'get snapshot', 'get ops', 'open' then 'read'
-        when 'submit op' then 'update'
-        when 'submit meta' then 'update'
-        when 'delete' then 'delete'
-        else throw new Error "Invalid action name #{name}"
-
-      responded = false
-      action.reject = ->
-        throw new Error 'Multiple accept/reject calls made' if responded
-        responded = true
-        userCallback 'forbidden', null
-      action.accept = ->
-        throw new Error 'Multiple accept/reject calls made' if responded
-        responded = true
-        acceptCallback()
-
-      auth this, action
-
-    disconnect: ->
-      model.removeListener docName, listener for docName, listener of @listeners
-
-    getOps: (docName, start, end, callback) ->
-      @doAuth {docName, start, end}, 'get ops', callback, ->
-        model.getOps docName, start, end, callback
-
-    getSnapshot: (docName, callback) ->
-      @doAuth {docName}, 'get snapshot', callback, ->
-        model.getSnapshot docName, callback
-    
-    create: (docName, type, meta, callback) ->
-      # We don't check that types[type.name] == type. That might be important at some point.
-      type = types[type] if typeof type == 'string'
-
-      # I'm not sure what client-specified metadata should be allowed in the document metadata
-      # object. For now, I'm going to ignore all create metadata until I know how it should work.
-      meta = {}
-
-      meta.creator = @name if @name
-      meta.ctime = meta.mtime = Date.now()
-
-      # The action object has a 'type' property already. Hence the doc type is renamed to 'docType'
-      @doAuth {docName, docType:type, meta}, 'create', callback, =>
-        model.create docName, type, meta, callback
-
-    submitOp: (docName, opData, callback) ->
-      opData.meta ||= {}
-      opData.meta.source = @sessionId
-      dupIfSource = opData.dupIfSource or []
-
-      # If ops and meta get coalesced, they should be separated here.
-      if opData.op
-        @doAuth {docName, op:opData.op, v:opData.v, meta:opData.meta, dupIfSource}, 'submit op', callback, =>
-          model.applyOp docName, opData, callback
-      else
-        @doAuth {docName, meta:opData.meta}, 'submit meta', callback, =>
-          model.applyMetaOp docName, opData, callback
-
-    # Delete the named operation.
-    # Callback is passed (deleted?, error message)
-    delete: (docName, callback) ->
-      @doAuth {docName}, 'delete', callback, =>
-        model.delete docName, callback
-    
-    # Open the named document for reading. Just like model.listen, version is optional.
-    listen: (docName, version, listener, callback) ->
-      authOps = if version?
-        # If the specified version is older than the current version, we have to also check that the
-        # agent is allowed to get ops from the specified version.
-        #
-        # We _could_ check the version number of the document and then only check getOps if
-        # the specified version is old, but an auth check is almost certainly faster than a db roundtrip.
-        (c) => @doAuth {docName, start:version, end:null}, 'get ops', callback, c
-      else
-        (c) -> c()
-
-      authOps =>
-        @doAuth {docName, v:version if version?}, 'open', callback, =>
-          return callback? 'Document is already open' if @listeners[docName]
-          @listeners[docName] = listener
-
-          model.listen docName, version, listener, (error, v) =>
-            if error
-              delete @listeners[docName]
-
-            callback? error, v
-
-    removeListener: (docName) ->
-      throw new Error 'Document is not open' unless @listeners[docName]
-      model.removeListener docName, @listeners[docName]
-      delete @listeners[docName]
-
-  # Finally, return a function which takes client data and returns an authenticated useragent object
-  # through a callback.
-  (data, callback) ->
-    agent = new UserAgent data
-    agent.doAuth null, 'connect', callback, ->
-      # Maybe store a set of agents? Is that useful?
-      callback null, agent
-

+ 0 - 48
services/web/public/coffee/ide/editor/sharejs/vendor/types/README.md

@@ -1,48 +0,0 @@
-This directory contains all the operational transform code. Each file defines a type.
-
-Most of the types in here are for testing or demonstration. The only types which are sent to the webclient
-are `text` and `json`.
-
-
-# An OT type
-
-All OT types have the following fields:
-
-`name`: _(string)_ Name of the type. Should match the filename.
-`create() -> snapshot`: Function which creates and returns a new document snapshot
-
-`apply(snapshot, op) -> snapshot`: A function which creates a new document snapshot with the op applied
-`transform(op1, op2, side) -> op1'`: OT transform function.
-
-Given op1, op2, `apply(s, op2, transform(op1, op2, 'left')) == apply(s, op1, transform(op2, op1, 'right'))`.
-
-Transform and apply must never modify their arguments.
-
-
-Optional properties:
-
-`tp2`: _(bool)_ True if the transform function supports TP2. This allows p2p architectures to work.
-`compose(op1, op2) -> op`: Create and return a new op which has the same effect as op1 + op2.
-`serialize(snapshot) -> JSON object`: Serialize a document to something we can JSON.stringify()
-`deserialize(object) -> snapshot`: Deserialize a JSON object into the document's internal snapshot format
-`prune(op1', op2, side) -> op1`: Inserse transform function. Only required for TP2 types.
-`normalize(op) -> op`: Fix up an op to make it valid. Eg, remove skips of size zero.
-`api`: _(object)_ Set of helper methods which will be mixed in to the client document object for manipulating documents. See below.
-
-
-# Examples
-
-`count` and `simple` are two trivial OT type definitions if you want to take a look. JSON defines
-the ot-for-JSON type (see the wiki for documentation) and all the text types define different text
-implementations. (I still have no idea which one I like the most, and they're fun to write!)
-
-
-# API
-
-Types can also define API functions. These methods are mixed into the client's Doc object when a document is created.
-You can use them to help construct ops programatically (so users don't need to understand how ops are structured).
-
-For example, the three text types defined here (text, text-composable and text-tp2) all provide the text API, supplying
-`.insert()`, `.del()`, `.getLength` and `.getText` methods.
-
-See text-api.coffee for an example.

+ 0 - 22
services/web/public/coffee/ide/editor/sharejs/vendor/types/count.coffee

@@ -1,22 +0,0 @@
-# This is a simple type used for testing other OT code. Each op is [expectedSnapshot, increment]
-
-exports.name = 'count'
-exports.create = -> 1
-
-exports.apply = (snapshot, op) ->
-  [v, inc] = op
-  throw new Error "Op #{v} != snapshot #{snapshot}" unless snapshot == v
-  snapshot + inc
-
-# transform op1 by op2. Return transformed version of op1.
-exports.transform = (op1, op2) ->
-  throw new Error "Op1 #{op1[0]} != op2 #{op2[0]}" unless op1[0] == op2[0]
-  [op1[0] + op2[1], op1[1]]
-
-exports.compose = (op1, op2) ->
-  throw new Error "Op1 #{op1} + 1 != op2 #{op2}" unless op1[0] + op1[1] == op2[0]
-  [op1[0], op1[1] + op2[1]]
-
-exports.generateRandomOp = (doc) ->
-  [[doc, 1], doc + 1]
-

+ 0 - 65
services/web/public/coffee/ide/editor/sharejs/vendor/types/helpers.coffee

@@ -1,65 +0,0 @@
-# These methods let you build a transform function from a transformComponent function
-# for OT types like text and JSON in which operations are lists of components
-# and transforming them requires N^2 work.
-
-# Add transform and transformX functions for an OT type which has transformComponent defined.
-# transformComponent(destination array, component, other component, side)
-exports['_bt'] = bootstrapTransform = (type, transformComponent, checkValidOp, append) ->
-  transformComponentX = (left, right, destLeft, destRight) ->
-    transformComponent destLeft, left, right, 'left'
-    transformComponent destRight, right, left, 'right'
-
-  # Transforms rightOp by leftOp. Returns ['rightOp', clientOp']
-  type.transformX = type['transformX'] = transformX = (leftOp, rightOp) ->
-    checkValidOp leftOp
-    checkValidOp rightOp
-
-    newRightOp = []
-
-    for rightComponent in rightOp
-      # Generate newLeftOp by composing leftOp by rightComponent
-      newLeftOp = []
-
-      k = 0
-      while k < leftOp.length
-        nextC = []
-        transformComponentX leftOp[k], rightComponent, newLeftOp, nextC
-        k++
-
-        if nextC.length == 1
-          rightComponent = nextC[0]
-        else if nextC.length == 0
-          append newLeftOp, l for l in leftOp[k..]
-          rightComponent = null
-          break
-        else
-          # Recurse.
-          [l_, r_] = transformX leftOp[k..], nextC
-          append newLeftOp, l for l in l_
-          append newRightOp, r for r in r_
-          rightComponent = null
-          break
-    
-      append newRightOp, rightComponent if rightComponent?
-      leftOp = newLeftOp
-    
-    [leftOp, newRightOp]
-
-  # Transforms op with specified type ('left' or 'right') by otherOp.
-  type.transform = type['transform'] = (op, otherOp, type) ->
-    throw new Error "type must be 'left' or 'right'" unless type == 'left' or type == 'right'
-
-    return op if otherOp.length == 0
-
-    # TODO: Benchmark with and without this line. I _think_ it'll make a big difference...?
-    return transformComponent [], op[0], otherOp[0], type if op.length == 1 and otherOp.length == 1
-
-    if type == 'left'
-      [left, _] = transformX op, otherOp
-      left
-    else
-      [_, right] = transformX otherOp, op
-      right
-
-if typeof WEB is 'undefined'
-  exports.bootstrapTransform = bootstrapTransform

+ 0 - 15
services/web/public/coffee/ide/editor/sharejs/vendor/types/index.coffee

@@ -1,15 +0,0 @@
-
-register = (file) ->
-  type = require file
-  exports[type.name] = type
-  try require "#{file}-api"
-
-# Import all the built-in types.
-register './simple'
-register './count'
-
-register './text'
-register './text-composable'
-register './text-tp2'
-
-register './json'

+ 0 - 180
services/web/public/coffee/ide/editor/sharejs/vendor/types/json-api.coffee

@@ -1,180 +0,0 @@
-# API for JSON OT
-
-json = require './json' if typeof WEB is 'undefined'
-
-if WEB?
-  extendDoc = exports.extendDoc
-  exports.extendDoc = (name, fn) ->
-    SubDoc::[name] = fn
-    extendDoc name, fn
-
-depath = (path) ->
-  if path.length == 1 and path[0].constructor == Array
-    path[0]
-  else path
-
-class SubDoc
-  constructor: (@doc, @path) ->
-  at: (path...) -> @doc.at @path.concat depath path
-  get: -> @doc.getAt @path
-  # for objects and lists
-  set: (value, cb) -> @doc.setAt @path, value, cb
-  # for strings and lists.
-  insert: (pos, value, cb) -> @doc.insertAt @path, pos, value, cb
-  # for strings
-  del: (pos, length, cb) -> @doc.deleteTextAt @path, length, pos, cb
-  # for objects and lists
-  remove: (cb) -> @doc.removeAt @path, cb
-  push: (value, cb) -> @insert @get().length, value, cb
-  move: (from, to, cb) -> @doc.moveAt @path, from, to, cb
-  add: (amount, cb) -> @doc.addAt @path, amount, cb
-  on: (event, cb) -> @doc.addListener @path, event, cb
-  removeListener: (l) -> @doc.removeListener l
-
-  # text API compatibility
-  getLength: -> @get().length
-  getText: -> @get()
-
-traverse = (snapshot, path) ->
-  container = data:snapshot
-  key = 'data'
-  elem = container
-  for p in path
-    elem = elem[key]
-    key = p
-    throw new Error 'bad path' if typeof elem == 'undefined'
-  {elem, key}
-
-pathEquals = (p1, p2) ->
-  return false if p1.length != p2.length
-  for e,i in p1
-    return false if e != p2[i]
-  true
-
-json.api =
-  provides: {json:true}
-
-  at: (path...) -> new SubDoc this, depath path
-
-  get: -> @snapshot
-  set: (value, cb) -> @setAt [], value, cb
-
-  getAt: (path) ->
-    {elem, key} = traverse @snapshot, path
-    return elem[key]
-
-  setAt: (path, value, cb) ->
-    {elem, key} = traverse @snapshot, path
-    op = {p:path}
-    if elem.constructor == Array
-      op.li = value
-      op.ld = elem[key] if typeof elem[key] != 'undefined'
-    else if typeof elem == 'object'
-      op.oi = value
-      op.od = elem[key] if typeof elem[key] != 'undefined'
-    else throw new Error 'bad path'
-    @submitOp [op], cb
-
-  removeAt: (path, cb) ->
-    {elem, key} = traverse @snapshot, path
-    throw new Error 'no element at that path' unless typeof elem[key] != 'undefined'
-    op = {p:path}
-    if elem.constructor == Array
-      op.ld = elem[key]
-    else if typeof elem == 'object'
-      op.od = elem[key]
-    else throw new Error 'bad path'
-    @submitOp [op], cb
-
-  insertAt: (path, pos, value, cb) ->
-    {elem, key} = traverse @snapshot, path
-    op = {p:path.concat pos}
-    if elem[key].constructor == Array
-      op.li = value
-    else if typeof elem[key] == 'string'
-      op.si = value
-    @submitOp [op], cb
-
-  moveAt: (path, from, to, cb) ->
-    op = [{p:path.concat(from), lm:to}]
-    @submitOp op, cb
-
-  addAt: (path, amount, cb) ->
-    op = [{p:path, na:amount}]
-    @submitOp op, cb
-
-  deleteTextAt: (path, length, pos, cb) ->
-    {elem, key} = traverse @snapshot, path
-    op = [{p:path.concat(pos), sd:elem[key][pos...(pos + length)]}]
-    @submitOp op, cb
-
-  addListener: (path, event, cb) ->
-    l = {path, event, cb}
-    @_listeners.push l
-    l
-  removeListener: (l) ->
-    i = @_listeners.indexOf l
-    return false if i < 0
-    @_listeners.splice i, 1
-    return true
-  _register: ->
-    @_listeners = []
-    @on 'change', (op) ->
-      for c in op
-        if c.na != undefined or c.si != undefined or c.sd != undefined
-          # no change to structure
-          continue
-        to_remove = []
-        for l, i in @_listeners
-          # Transform a dummy op by the incoming op to work out what
-          # should happen to the listener.
-          dummy = {p:l.path, na:0}
-          xformed = @type.transformComponent [], dummy, c, 'left'
-          if xformed.length == 0
-            # The op was transformed to noop, so we should delete the listener.
-            to_remove.push i
-          else if xformed.length == 1
-            # The op remained, so grab its new path into the listener.
-            l.path = xformed[0].p
-          else
-            throw new Error "Bad assumption in json-api: xforming an 'si' op will always result in 0 or 1 components."
-        to_remove.sort (a, b) -> b - a
-        for i in to_remove
-          @_listeners.splice i, 1
-    @on 'remoteop', (op) ->
-      for c in op
-        match_path = if c.na == undefined then c.p[...c.p.length-1] else c.p
-        for {path, event, cb} in @_listeners
-          if pathEquals path, match_path
-            switch event
-              when 'insert'
-                if c.li != undefined and c.ld == undefined
-                  cb(c.p[c.p.length-1], c.li)
-                else if c.oi != undefined and c.od == undefined
-                  cb(c.p[c.p.length-1], c.oi)
-                else if c.si != undefined
-                  cb(c.p[c.p.length-1], c.si)
-              when 'delete'
-                if c.li == undefined and c.ld != undefined
-                  cb(c.p[c.p.length-1], c.ld)
-                else if c.oi == undefined and c.od != undefined
-                  cb(c.p[c.p.length-1], c.od)
-                else if c.sd != undefined
-                  cb(c.p[c.p.length-1], c.sd)
-              when 'replace'
-                if c.li != undefined and c.ld != undefined
-                  cb(c.p[c.p.length-1], c.ld, c.li)
-                else if c.oi != undefined and c.od != undefined
-                  cb(c.p[c.p.length-1], c.od, c.oi)
-              when 'move'
-                if c.lm != undefined
-                  cb(c.p[c.p.length-1], c.lm)
-              when 'add'
-                if c.na != undefined
-                  cb(c.na)
-          else if (common = @type.commonPath match_path, path)?
-            if event == 'child op'
-              if match_path.length == path.length == common
-                throw new Error "paths match length and have commonality, but aren't equal?"
-              child_path = c.p[common+1..]
-              cb(child_path, c)

+ 0 - 441
services/web/public/coffee/ide/editor/sharejs/vendor/types/json.coffee

@@ -1,441 +0,0 @@
-# This is the implementation of the JSON OT type.
-#
-# Spec is here: https://github.com/josephg/ShareJS/wiki/JSON-Operations
-
-if WEB?
-  text = exports.types.text
-else
-  text = require './text'
-
-json = {}
-
-json.name = 'json'
-
-json.create = -> null
-
-json.invertComponent = (c) ->
-  c_ = {p: c.p}
-  c_.sd = c.si if c.si != undefined
-  c_.si = c.sd if c.sd != undefined
-  c_.od = c.oi if c.oi != undefined
-  c_.oi = c.od if c.od != undefined
-  c_.ld = c.li if c.li != undefined
-  c_.li = c.ld if c.ld != undefined
-  c_.na = -c.na if c.na != undefined
-  if c.lm != undefined
-    c_.lm = c.p[c.p.length-1]
-    c_.p = c.p[0...c.p.length - 1].concat([c.lm])
-  c_
-
-json.invert = (op) -> json.invertComponent c for c in op.slice().reverse()
-
-json.checkValidOp = (op) ->
-
-isArray = (o) -> Object.prototype.toString.call(o) == '[object Array]'
-json.checkList = (elem) ->
-  throw new Error 'Referenced element not a list' unless isArray(elem)
-
-json.checkObj = (elem) ->
-  throw new Error "Referenced element not an object (it was #{JSON.stringify elem})" unless elem.constructor is Object
-
-json.apply = (snapshot, op) ->
-  json.checkValidOp op
-  op = clone op
-
-  container = {data: clone snapshot}
-
-  try
-    for c, i in op
-      parent = null
-      parentkey = null
-      elem = container
-      key = 'data'
-
-      for p in c.p
-        parent = elem
-        parentkey = key
-        elem = elem[key]
-        key = p
-
-        throw new Error 'Path invalid' unless parent?
-
-      if c.na != undefined
-        # Number add
-        throw new Error 'Referenced element not a number' unless typeof elem[key] is 'number'
-        elem[key] += c.na
-
-      else if c.si != undefined
-        # String insert
-        throw new Error "Referenced element not a string (it was #{JSON.stringify elem})" unless typeof elem is 'string'
-        parent[parentkey] = elem[...key] + c.si + elem[key..]
-      else if c.sd != undefined
-        # String delete
-        throw new Error 'Referenced element not a string' unless typeof elem is 'string'
-        throw new Error 'Deleted string does not match' unless elem[key...key + c.sd.length] == c.sd
-        parent[parentkey] = elem[...key] + elem[key + c.sd.length..]
-
-      else if c.li != undefined && c.ld != undefined
-        # List replace
-        json.checkList elem
-
-        # Should check the list element matches c.ld
-        elem[key] = c.li
-      else if c.li != undefined
-        # List insert
-        json.checkList elem
-
-        elem.splice key, 0, c.li
-      else if c.ld != undefined
-        # List delete
-        json.checkList elem
-
-        # Should check the list element matches c.ld here too.
-        elem.splice key, 1
-      else if c.lm != undefined
-        # List move
-        json.checkList elem
-        if c.lm != key
-          e = elem[key]
-          # Remove it...
-          elem.splice key, 1
-          # And insert it back.
-          elem.splice c.lm, 0, e
-
-      else if c.oi != undefined
-        # Object insert / replace
-        json.checkObj elem
-        
-        # Should check that elem[key] == c.od
-        elem[key] = c.oi
-      else if c.od != undefined
-        # Object delete
-        json.checkObj elem
-
-        # Should check that elem[key] == c.od
-        delete elem[key]
-      else
-        throw new Error 'invalid / missing instruction in op'
-  catch error
-    # TODO: Roll back all already applied changes. Write tests before implementing this code.
-    throw error
-
-  container.data
-
-# Checks if two paths, p1 and p2 match.
-json.pathMatches = (p1, p2, ignoreLast) ->
-  return false unless p1.length == p2.length
-
-  for p, i in p1
-    return false if p != p2[i] and (!ignoreLast or i != p1.length - 1)
-      
-  true
-
-json.append = (dest, c) ->
-  c = clone c
-  if dest.length != 0 and json.pathMatches c.p, (last = dest[dest.length - 1]).p
-    if last.na != undefined and c.na != undefined
-      dest[dest.length - 1] = { p: last.p, na: last.na + c.na }
-    else if last.li != undefined and c.li == undefined and c.ld == last.li
-      # insert immediately followed by delete becomes a noop.
-      if last.ld != undefined
-        # leave the delete part of the replace
-        delete last.li
-      else
-        dest.pop()
-    else if last.od != undefined and last.oi == undefined and
-        c.oi != undefined and c.od == undefined
-      last.oi = c.oi
-    else if c.lm != undefined and c.p[c.p.length-1] == c.lm
-      null # don't do anything
-    else
-      dest.push c
-  else
-    dest.push c
-
-json.compose = (op1, op2) ->
-  json.checkValidOp op1
-  json.checkValidOp op2
-
-  newOp = clone op1
-  json.append newOp, c for c in op2
-
-  newOp
-
-json.normalize = (op) ->
-  newOp = []
-  
-  op = [op] unless isArray op
-
-  for c in op
-    c.p ?= []
-    json.append newOp, c
-  
-  newOp
-
-# hax, copied from test/types/json. Apparently this is still the fastest way to deep clone an object, assuming
-# we have browser support for JSON.
-# http://jsperf.com/cloning-an-object/12
-clone = (o) -> JSON.parse(JSON.stringify o)
-
-json.commonPath = (p1, p2) ->
-  p1 = p1.slice()
-  p2 = p2.slice()
-  p1.unshift('data')
-  p2.unshift('data')
-  p1 = p1[...p1.length-1]
-  p2 = p2[...p2.length-1]
-  return -1 if p2.length == 0
-  i = 0
-  while p1[i] == p2[i] && i < p1.length
-    i++
-    if i == p2.length
-      return i-1
-  return
-
-# transform c so it applies to a document with otherC applied.
-json.transformComponent = (dest, c, otherC, type) ->
-  c = clone c
-  c.p.push(0) if c.na != undefined
-  otherC.p.push(0) if otherC.na != undefined
-
-  common = json.commonPath c.p, otherC.p
-  common2 = json.commonPath otherC.p, c.p
-
-  cplength = c.p.length
-  otherCplength = otherC.p.length
-
-  c.p.pop() if c.na != undefined # hax
-  otherC.p.pop() if otherC.na != undefined
-
-  if otherC.na
-    if common2? && otherCplength >= cplength && otherC.p[common2] == c.p[common2]
-      if c.ld != undefined
-        oc = clone otherC
-        oc.p = oc.p[cplength..]
-        c.ld = json.apply clone(c.ld), [oc]
-      else if c.od != undefined
-        oc = clone otherC
-        oc.p = oc.p[cplength..]
-        c.od = json.apply clone(c.od), [oc]
-    json.append dest, c
-    return dest
-
-  if common2? && otherCplength > cplength && c.p[common2] == otherC.p[common2]
-    # transform based on c
-    if c.ld != undefined
-      oc = clone otherC
-      oc.p = oc.p[cplength..]
-      c.ld = json.apply clone(c.ld), [oc]
-    else if c.od != undefined
-      oc = clone otherC
-      oc.p = oc.p[cplength..]
-      c.od = json.apply clone(c.od), [oc]
-
-
-  if common?
-    commonOperand = cplength == otherCplength
-    # transform based on otherC
-    if otherC.na != undefined
-      # this case is handled above due to icky path hax
-    else if otherC.si != undefined || otherC.sd != undefined
-      # String op vs string op - pass through to text type
-      if c.si != undefined || c.sd != undefined
-        throw new Error("must be a string?") unless commonOperand
-
-        # Convert an op component to a text op component
-        convert = (component) ->
-          newC = p:component.p[component.p.length - 1]
-          if component.si
-            newC.i = component.si
-          else
-            newC.d = component.sd
-          newC
-
-        tc1 = convert c
-        tc2 = convert otherC
-          
-        res = []
-        text._tc res, tc1, tc2, type
-        for tc in res
-          jc = { p: c.p[...common] }
-          jc.p.push(tc.p)
-          jc.si = tc.i if tc.i?
-          jc.sd = tc.d if tc.d?
-          json.append dest, jc
-        return dest
-    else if otherC.li != undefined && otherC.ld != undefined
-      if otherC.p[common] == c.p[common]
-        # noop
-        if !commonOperand
-          # we're below the deleted element, so -> noop
-          return dest
-        else if c.ld != undefined
-          # we're trying to delete the same element, -> noop
-          if c.li != undefined and type == 'left'
-            # we're both replacing one element with another. only one can
-            # survive!
-            c.ld = clone otherC.li
-          else
-            return dest
-    else if otherC.li != undefined
-      if c.li != undefined and c.ld == undefined and commonOperand and c.p[common] == otherC.p[common]
-        # in li vs. li, left wins.
-        if type == 'right'
-          c.p[common]++
-      else if otherC.p[common] <= c.p[common]
-        c.p[common]++
-
-      if c.lm != undefined
-        if commonOperand
-          # otherC edits the same list we edit
-          if otherC.p[common] <= c.lm
-            c.lm++
-          # changing c.from is handled above.
-    else if otherC.ld != undefined
-      if c.lm != undefined
-        if commonOperand
-          if otherC.p[common] == c.p[common]
-            # they deleted the thing we're trying to move
-            return dest
-          # otherC edits the same list we edit
-          p = otherC.p[common]
-          from = c.p[common]
-          to = c.lm
-          if p < to || (p == to && from < to)
-            c.lm--
-
-      if otherC.p[common] < c.p[common]
-        c.p[common]--
-      else if otherC.p[common] == c.p[common]
-        if otherCplength < cplength
-          # we're below the deleted element, so -> noop
-          return dest
-        else if c.ld != undefined
-          if c.li != undefined
-            # we're replacing, they're deleting. we become an insert.
-            delete c.ld
-          else
-            # we're trying to delete the same element, -> noop
-            return dest
-    else if otherC.lm != undefined
-      if c.lm != undefined and cplength == otherCplength
-        # lm vs lm, here we go!
-        from = c.p[common]
-        to = c.lm
-        otherFrom = otherC.p[common]
-        otherTo = otherC.lm
-        if otherFrom != otherTo
-          # if otherFrom == otherTo, we don't need to change our op.
-
-          # where did my thing go?
-          if from == otherFrom
-            # they moved it! tie break.
-            if type == 'left'
-              c.p[common] = otherTo
-              if from == to # ugh
-                c.lm = otherTo
-            else
-              return dest
-          else
-            # they moved around it
-            if from > otherFrom
-              c.p[common]--
-            if from > otherTo
-              c.p[common]++
-            else if from == otherTo
-              if otherFrom > otherTo
-                c.p[common]++
-                if from == to # ugh, again
-                  c.lm++
-
-            # step 2: where am i going to put it?
-            if to > otherFrom
-              c.lm--
-            else if to == otherFrom
-              if to > from
-                c.lm--
-            if to > otherTo
-              c.lm++
-            else if to == otherTo
-              # if we're both moving in the same direction, tie break
-              if (otherTo > otherFrom and to > from) or
-                 (otherTo < otherFrom and to < from)
-                if type == 'right'
-                  c.lm++
-              else
-                if to > from
-                  c.lm++
-                else if to == otherFrom
-                  c.lm--
-      else if c.li != undefined and c.ld == undefined and commonOperand
-        # li
-        from = otherC.p[common]
-        to = otherC.lm
-        p = c.p[common]
-        if p > from
-          c.p[common]--
-        if p > to
-          c.p[common]++
-      else
-        # ld, ld+li, si, sd, na, oi, od, oi+od, any li on an element beneath
-        # the lm
-        #
-        # i.e. things care about where their item is after the move.
-        from = otherC.p[common]
-        to = otherC.lm
-        p = c.p[common]
-        if p == from
-          c.p[common] = to
-        else
-          if p > from
-            c.p[common]--
-          if p > to
-            c.p[common]++
-          else if p == to
-            if from > to
-              c.p[common]++
-    else if otherC.oi != undefined && otherC.od != undefined
-      if c.p[common] == otherC.p[common]
-        if c.oi != undefined and commonOperand
-          # we inserted where someone else replaced
-          if type == 'right'
-            # left wins
-            return dest
-          else
-            # we win, make our op replace what they inserted
-            c.od = otherC.oi
-        else
-          # -> noop if the other component is deleting the same object (or any
-          # parent)
-          return dest
-    else if otherC.oi != undefined
-      if c.oi != undefined and c.p[common] == otherC.p[common]
-        # left wins if we try to insert at the same place
-        if type == 'left'
-          json.append dest, {p:c.p, od:otherC.oi}
-        else
-          return dest
-    else if otherC.od != undefined
-      if c.p[common] == otherC.p[common]
-        return dest if !commonOperand
-        if c.oi != undefined
-          delete c.od
-        else
-          return dest
-  
-  json.append dest, c
-  return dest
-
-if WEB?
-  exports.types ||= {}
-
-  # This is kind of awful - come up with a better way to hook this helper code up.
-  exports._bt(json, json.transformComponent, json.checkValidOp, json.append)
-
-  # [] is used to prevent closure from renaming types.text
-  exports.types.json = json
-else
-  module.exports = json
-
-  require('./helpers').bootstrapTransform(json, json.transformComponent, json.checkValidOp, json.append)
-

+ 0 - 38
services/web/public/coffee/ide/editor/sharejs/vendor/types/simple.coffee

@@ -1,38 +0,0 @@
-# This is a really simple OT type. Its not compiled with the web client, but it could be.
-#
-# Its mostly included for demonstration purposes and its used in a lot of unit tests.
-#
-# This defines a really simple text OT type which only allows inserts. (No deletes).
-#
-# Ops look like:
-#   {position:#, text:"asdf"}
-#
-# Document snapshots look like:
-#   {str:string}
-
-module.exports =
-  # The name of the OT type. The type is stored in types[type.name]. The name can be
-  # used in place of the actual type in all the API methods.
-  name: 'simple'
-
-  # Create a new document snapshot
-  create: -> {str:""}
-
-  # Apply the given op to the document snapshot. Returns the new snapshot.
-  #
-  # The original snapshot should not be modified.
-  apply: (snapshot, op) ->
-    throw new Error 'Invalid position' unless 0 <= op.position <= snapshot.str.length
-
-    str = snapshot.str
-    str = str.slice(0, op.position) + op.text + str.slice(op.position)
-    {str}
-
-  # transform op1 by op2. Return transformed version of op1.
-  # sym describes the symmetry of the op. Its 'left' or 'right' depending on whether the
-  # op being transformed comes from the client or the server.
-  transform: (op1, op2, sym) ->
-    pos = op1.position
-    pos += op2.text.length if op2.position < pos or (op2.position == pos and sym is 'left')
-
-    return {position:pos, text:op1.text}

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác