Merge pull request #266 from sharelatex/as-cm

CodeMirror
This commit is contained in:
Alasdair Smith
2018-02-09 11:23:11 +00:00
committed by GitHub
20 changed files with 498 additions and 34 deletions
+4
View File
@@ -1,5 +1,6 @@
fs = require "fs"
PackageVersions = require "./app/coffee/infrastructure/PackageVersions"
Settings = require "settings-sharelatex"
require('es6-promise').polyfill()
module.exports = (grunt) ->
@@ -108,6 +109,7 @@ module.exports = (grunt) ->
"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"
]
client:
@@ -152,6 +154,7 @@ module.exports = (grunt) ->
sourceMapBasepath: "public/stylesheets"
globalVars:
'is-overleaf': false
'show-rich-text': Settings.showRichText
files:
"public/stylesheets/style.css": "public/stylesheets/style.less"
ol:
@@ -161,6 +164,7 @@ module.exports = (grunt) ->
sourceMapBasepath: "public/stylesheets"
globalVars:
'is-overleaf': true
'show-rich-text': Settings.showRichText
files:
"public/stylesheets/ol-style.css": "public/stylesheets/ol-style.less"
+2 -1
View File
@@ -21,7 +21,8 @@ SHAREJS_COFFEE_FILES := \
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/ace.coffee \
public/coffee/ide/editor/sharejs/vendor/client/cm.coffee
LESS_FILES := $(shell find public/stylesheets -name '*.less')
CSS_FILES := public/stylesheets/style.css public/stylesheets/ol-style.css
@@ -14,5 +14,7 @@ module.exports = Features =
return Settings.enableGithubSync
when 'v1-return-message'
return Settings.accountMerge? and Settings.overleaf?
when 'rich-text'
return Settings.showRichText
else
throw new Error("unknown feature: #{feature}")
@@ -158,6 +158,13 @@ block requirejs
window.pdfCMapsPath = "#{pdfCMapsPath}"
window.uiConfig = JSON.parse('!{JSON.stringify(uiConfig).replace(/\//g, "\\/")}');
// TODO: inject based on environment
if hasFeature('rich-text')
script(
src="http://localhost:3809/public/js/es/richText.js"
type="text/javascript"
)
script(
data-main=buildJsPath("ide.js", {hashedPath:false}),
baseurl=fullJsPath,
@@ -30,9 +30,13 @@ div.full-size(
i.fa.fa-arrow-left
|   #{translate("open_a_file_on_the_left")}
if hasFeature('rich-text')
.toolbar.toolbar-editor(ng-controller="EditorToolbarController")
button(ng-click="toggleRichText()") Rich Text
#editor(
ace-editor="editor",
ng-show="!!editor.sharejs_doc && !editor.opening"
ng-if="!!editor.sharejs_doc && !editor.opening && !editor.richText"
theme="settings.theme",
keybindings="settings.mode",
font-size="settings.fontSize",
@@ -63,6 +67,13 @@ div.full-size(
renderer-data="reviewPanel.rendererData"
)
if hasFeature('rich-text')
#editor-rich-text(
cm-editor
ng-if="!editor.opening && editor.richText"
sharejs-doc="editor.sharejs_doc"
)
include ./review-panel
.ui-layout-east
+5
View File
@@ -1354,6 +1354,11 @@
"from": "code-point-at@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz"
},
"codemirror": {
"version": "5.33.0",
"from": "codemirror@latest",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.33.0.tgz"
},
"coffee-script": {
"version": "1.12.4",
"from": "coffee-script@1.12.4",
+1
View File
@@ -30,6 +30,7 @@
"bcrypt": "1.0.1",
"body-parser": "^1.13.1",
"bufferedstream": "1.6.0",
"codemirror": "^5.33.0",
"connect-redis": "^3.1.0",
"cookie": "^0.2.3",
"cookie-parser": "1.3.5",
@@ -26,7 +26,8 @@ define [
@connected = @ide.socket.socket.connected
@joined = false
@wantToBeJoined = false
@_checkConsistency = _.bind(@_checkConsistency, @)
@_checkAceConsistency = _.bind(@_checkConsistency, @, @ace)
@_checkCMConsistency = _.bind(@_checkConsistency, @, @cm)
@inconsistentCount = 0
@_bindToEditorEvents()
@_bindToSocketEvents()
@@ -34,32 +35,43 @@ define [
attachToAce: (@ace) ->
@doc?.attachToAce(@ace)
editorDoc = @ace.getSession().getDocument()
editorDoc.on "change", @_checkConsistency
editorDoc.on "change", @_checkAceConsistency
@ide.$scope.$emit 'document:opened', @doc
detachFromAce: () ->
@doc?.detachFromAce()
editorDoc = @ace?.getSession().getDocument()
editorDoc?.off "change", @_checkConsistency
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: () ->
# 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 = @ace?.getValue()
sharejsValue = @doc?.getSnapshot()
if editorValue != sharejsValue
@inconsistentCount++
else
@inconsistentCount = 0
_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
if @inconsistentCount >= 3
@_onError new Error("Editor text does not match server text")
, 0
getSnapshot: () ->
@doc?.getSnapshot()
@@ -1,7 +1,9 @@
define [
"ide/editor/Document"
"ide/editor/directives/aceEditor"
"ide/editor/directives/cmEditor"
"ide/editor/controllers/SavingNotificationController"
"ide/editor/controllers/EditorToolbarController"
], (Document) ->
class EditorManager
constructor: (@ide, @$scope) ->
@@ -12,6 +14,7 @@ define [
opening: true
trackChanges: false
wantTrackChanges: false
richText: false
}
@$scope.$on "entity:selected", (event, entity) =>
@@ -186,3 +189,6 @@ define [
@$scope.editor.trackChanges = want
else
@_syncTimeout = setTimeout tryToggle, 100
toggleRichText: () ->
@$scope.editor.richText = !@$scope.editor.richText
@@ -120,6 +120,9 @@ define [
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) ->
@@ -0,0 +1,7 @@
define [
"base"
"ide/editor/Document"
], (App, Document) ->
App.controller "EditorToolbarController", ($scope, ide) ->
$scope.toggleRichText = () ->
ide.editorManager.toggleRichText()
@@ -97,7 +97,7 @@ define [
scope.name = attrs.aceEditor
if scope.spellCheck # only enable spellcheck when explicitly required
spellCheckCache = $cacheFactory("spellCheck-#{scope.name}", {capacity: 1000})
spellCheckCache = $cacheFactory.get("spellCheck-#{scope.name}") || $cacheFactory("spellCheck-#{scope.name}", {capacity: 1000})
spellCheckManager = new SpellCheckManager(scope, editor, element, spellCheckCache, $http, $q)
undoManager = new UndoManager(scope, editor, element)
highlightsManager = new HighlightsManager(scope, editor, element)
@@ -106,7 +106,6 @@ define [
metadataManager = new MetadataManager(scope, editor, element, metadata)
autoCompleteManager = new AutoCompleteManager(scope, editor, element, metadataManager, graphics, preamble, files)
# Prevert Ctrl|Cmd-S from triggering save dialog
editor.commands.addCommand
name: "save",
@@ -428,6 +427,9 @@ define [
if rendererData?
rendererData.lineHeight = editor.renderer.lineHeight
scope.$on '$destroy', () ->
detachFromAce(scope.sharejsDoc)
template: """
<div class="ace-editor-wrapper">
<div
@@ -0,0 +1,32 @@
define [
"base"
], (App) ->
App.directive "cmEditor", () ->
return {
scope: {
sharejsDoc: "="
}
link: (scope, element, attrs) ->
cm = Frontend.richText.init(element.find('.cm-editor-wrapper')[0])
scope.$watch "sharejsDoc", (sharejsDoc, oldSharejsDoc) ->
if oldSharejsDoc?
detachFromCM(oldSharejsDoc)
if sharejsDoc?
attachToCM(sharejsDoc)
attachToCM = (sharejsDoc) ->
cm.setValue(sharejsDoc.getSnapshot())
sharejsDoc.attachToCM(cm)
detachFromCM = (sharejsDoc) ->
sharejsDoc.detachFromCM()
scope.$on 'destroy', () ->
detachFromCM(scope.sharejsDoc)
template: """
<div class="cm-editor-wrapper"></div>
"""
}
@@ -3,7 +3,7 @@
Range = ace.require("ace/range").Range
# Convert an ace delta into an op understood by share.js
applyToShareJS = (editorDoc, delta, doc, fromUndo) ->
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
@@ -81,7 +81,7 @@ window.sharejs.extendDoc 'attach_ace', (editor, keepEditorContents, maxDocLength
fromUndo = !!(editor.getSession().$fromUndo or editor.getSession().$fromReject)
applyToShareJS editorDoc, change, doc, fromUndo
applyAceToShareJS editorDoc, change, doc, fromUndo
check()
@@ -3,7 +3,7 @@
# It is heavily inspired from the Ace editor hook.
# Convert a CodeMirror delta into an op understood by share.js
applyToShareJS = (editorDoc, delta, doc) ->
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.
@@ -26,7 +26,7 @@ applyToShareJS = (editorDoc, delta, doc) ->
doc.del startPos, delLen
doc.insert startPos, delta.text.join '\n' if delta.text
applyToShareJS editorDoc, delta.next, doc if delta.next
applyCMToShareJS editorDoc, delta.next, doc if delta.next
# 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
@@ -39,14 +39,14 @@ window.sharejs.extendDoc 'attach_cm', (editor, keepEditorContents) ->
check = ->
window.setTimeout ->
editorText = editor.getValue()
otText = sharedoc.getValue()
otText = sharedoc.getText()
if editorText != otText
console.error "Text does not match!"
console.error "editor: #{editorText}"
console.error "ot: #{otText}"
# Replace the editor text with the doc snapshot.
editor.setValue sharedoc.getValue()
editor.setValue sharedoc.getText()
, 0
if keepEditorContents
@@ -64,11 +64,11 @@ window.sharejs.extendDoc 'attach_cm', (editor, keepEditorContents) ->
# Listen for edits in CodeMirror.
editorListener = (ed, change) ->
return if suppress
applyToShareJS editor, change, sharedoc
applyCMToShareJS editor, change, sharedoc
check()
editor.setOption 'onChange', editorListener
editor.on 'change', editorListener
@on 'insert', (pos, text) ->
suppress = true
@@ -87,7 +87,7 @@ window.sharejs.extendDoc 'attach_cm', (editor, keepEditorContents) ->
@detach_cm = ->
# TODO: can we remove the insert and delete event callbacks?
editor.setOption 'onChange', null
editor.off 'onChange', editorListener
delete @detach_cm
return
+5 -1
View File
@@ -1 +1,5 @@
console.log('hello from rich text')
import CodeMirror from 'codemirror'
export function init(rootEl) {
return CodeMirror(rootEl)
}
@@ -80,6 +80,12 @@
@import "app/error-pages.less";
@import "app/v1-badge.less";
// Vendor CSS
@import "../js/libs/pdfListView/TextLayer.css";
@import "../js/libs/pdfListView/AnnotationsLayer.css";
@import "../js/libs/pdfListView/HighlightsLayer.css";
// CodeMirror
& when (@show-rich-text) {
@import "vendor/codemirror.css";
}
@@ -75,10 +75,17 @@
top: 40px;
}
#editor {
#editor, #editor-rich-text {
.full-size;
}
#editor when (@show-rich-text = true) {
top: 40px; // TODO: replace with toolbar height var
}
#editor-rich-text when (@show-rich-text = true) {
top: 40px; // TODO: replace with toolbar height var
}
.loading-screen {
display: flex;
flex-direction: column;
@@ -190,6 +197,14 @@
}
}
.cm-editor-wrapper {
height: 100%;
.CodeMirror {
height: 100%;
}
}
// The internal components of the aceEditor directive
.ace-editor-wrapper {
.full-size;
+346
View File
@@ -0,0 +1,346 @@
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: 300px;
color: black;
direction: ltr;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
white-space: nowrap;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-fat-cursor-mark {
background-color: rgba(20, 255, 20, 0.5);
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
position: absolute;
left: 0; right: 0; top: -50px; bottom: -20px;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0; bottom: 0;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px; margin-right: -30px;
padding-bottom: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
padding: 0.1px; /* Force widget margins to stay inside of the container */
}
.CodeMirror-widget {}
.CodeMirror-rtl pre { direction: rtl; }
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background-color: #ffa;
background-color: rgba(255, 255, 0, .4);
}
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }
+1 -1
View File
@@ -4,7 +4,7 @@ module.exports = {
// Defines the "entry point(s)" for the application - i.e. the file which
// bootstraps the application
entry: {
'rich-text': './public/es/rich-text.js'
richText: './public/es/rich-text.js'
},
// Define where and how the bundle will be output to disk