diff --git a/services/web/public/coffee/ide/editor/EditorManager.coffee b/services/web/public/coffee/ide/editor/EditorManager.coffee
index 72bbe8509a..e3cabf8e98 100644
--- a/services/web/public/coffee/ide/editor/EditorManager.coffee
+++ b/services/web/public/coffee/ide/editor/EditorManager.coffee
@@ -1,5 +1,6 @@
define [
"ide/editor/Document"
+ "ide/editor/components/spellMenu"
"ide/editor/directives/aceEditor"
"ide/editor/directives/toggleSwitch"
"ide/editor/controllers/SavingNotificationController"
diff --git a/services/web/public/coffee/ide/editor/components/spellMenu.coffee b/services/web/public/coffee/ide/editor/components/spellMenu.coffee
new file mode 100644
index 0000000000..ff3462e03e
--- /dev/null
+++ b/services/web/public/coffee/ide/editor/components/spellMenu.coffee
@@ -0,0 +1,34 @@
+define ["base"], (App) ->
+ App.component "spellMenu", {
+ bindings: {
+ open: "<"
+ top: "<"
+ left: "<"
+ highlight: "<"
+ replaceWord: "&"
+ learnWord: "&"
+ }
+ template: """
+
+ """
+ }
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
index ef3c5b20f8..617b41b845 100644
--- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
@@ -7,6 +7,7 @@ define [
"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/track-changes/TrackChangesManager"
@@ -15,7 +16,7 @@ define [
"ide/graphics/services/graphics"
"ide/preamble/services/preamble"
"ide/files/services/files"
-], (App, Ace, SearchBox, Vim, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager) ->
+], (App, Ace, SearchBox, Vim, ModeList, UndoManager, AutoCompleteManager, SpellCheckManager, SpellCheckAdapter, HighlightsManager, CursorPositionManager, TrackChangesManager, MetadataManager) ->
EditSession = ace.require('ace/edit_session').EditSession
ModeList = ace.require('ace/ext/modelist')
Vim = ace.require('ace/keyboard/vim').Vim
@@ -103,7 +104,8 @@ define [
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, editor, element, spellCheckCache, $http, $q)
+ 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, editor, element, localStorage)
@@ -361,6 +363,23 @@ define [
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
+
attachToAce = (sharejs_doc) ->
lines = sharejs_doc.getSnapshot().split("\n")
session = editor.getSession()
@@ -406,6 +425,7 @@ define [
editor.initing = false
# now ready to edit document
editor.setReadOnly(scope.readOnly) # respect the readOnly setting, normally false
+ initSpellCheck()
resetScrollMargins()
@@ -467,6 +487,7 @@ define [
scope.$on '$destroy', () ->
if scope.sharejsDoc?
+ tearDownSpellCheck()
detachFromAce(scope.sharejsDoc)
session = editor.getSession()
session?.destroy()
@@ -488,22 +509,14 @@ define [
>Dismiss
-
+
- @row = options.row
- @column = options.column
+ constructor: (@markerId, @range, options) ->
@word = options.word
@suggestions = options.suggestions
class HighlightedWordManager
constructor: (@editor) ->
@reset()
-
- reset: () ->
- @highlights = rows: []
- addHighlight: (highlight) ->
- unless highlight instanceof Highlight
- highlight = new Highlight(highlight)
- range = new Range(
- highlight.row, highlight.column,
- highlight.row, highlight.column + highlight.word.length
- )
- highlight.markerId = @editor.getSession().addMarker range, "spelling-highlight", 'text', false
- @highlights.rows[highlight.row] ||= []
- @highlights.rows[highlight.row].push highlight
+ 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)
- for h, i in @highlights.rows[highlight.row]
- if h == highlight
- @highlights.rows[highlight.row].splice(i, 1)
+ @highlights = @highlights.filter (hl) ->
+ hl != highlight
removeWord: (word) ->
- toRemove = []
- for row in @highlights.rows
- for highlight in (row || [])
- if highlight.word == word
- toRemove.push(highlight)
- for highlight in toRemove
- @removeHighlight highlight
+ @highlights.filter (highlight) ->
+ highlight.word == word
+ .forEach (highlight) =>
+ @removeHighlight(highlight)
- moveHighlight: (highlight, position) ->
- @removeHighlight highlight
- highlight.row = position.row
- highlight.column = position.column
- @addHighlight highlight
-
- clearRows: (from, to) ->
- from ||= 0
- to ||= @highlights.rows.length - 1
- for row in @highlights.rows.slice(from, to + 1)
- for highlight in (row || []).slice(0)
- @removeHighlight highlight
-
- insertRows: (offset, number) ->
- # rows are inserted after offset. i.e. offset row is not modified
- affectedHighlights = []
- for row in @highlights.rows.slice(offset)
- affectedHighlights.push(highlight) for highlight in (row || [])
- for highlight in affectedHighlights
- @moveHighlight highlight,
- row: highlight.row + number
- column: highlight.column
-
- removeRows: (offset, number) ->
- # offset is the first row to delete
- affectedHighlights = []
- for row in @highlights.rows.slice(offset)
- affectedHighlights.push(highlight) for highlight in (row || [])
- for highlight in affectedHighlights
- if highlight.row >= offset + number
- @moveHighlight highlight,
- row: highlight.row - number
- column: highlight.column
- else
- @removeHighlight highlight
+ clearRow: (row) ->
+ @highlights.filter (highlight) ->
+ highlight.range.start.row == row
+ .forEach (highlight) =>
+ @removeHighlight(highlight)
findHighlightWithinRange: (range) ->
- rows = @highlights.rows.slice(range.start.row, range.end.row + 1)
- for row in rows
- for highlight in (row || [])
- if @_doesHighlightOverlapRange(highlight, range.start, range.end)
- return highlight
- return null
-
- applyChange: (change) ->
- start = change.start
- end = change.end
- if change.action == "insert"
- if start.row != end.row
- rowsAdded = end.row - start.row
- @insertRows start.row + 1, rowsAdded
- # make a copy since we're going to modify in place
- oldHighlights = (@highlights.rows[start.row] || []).slice(0)
- for highlight in oldHighlights
- if highlight.column > start.column
- # insertion was fully before this highlight
- @moveHighlight highlight,
- row: end.row
- column: highlight.column + (end.column - start.column)
- else if highlight.column + highlight.word.length >= start.column
- # insertion was inside this highlight
- @removeHighlight highlight
-
- else if change.action == "remove"
- if start.row == end.row
- oldHighlights = (@highlights.rows[start.row] || []).slice(0)
- else
- rowsRemoved = end.row - start.row
- oldHighlights =
- (@highlights.rows[start.row] || []).concat(
- (@highlights.rows[end.row] || [])
- )
- @removeRows start.row + 1, rowsRemoved
-
- for highlight in oldHighlights
- if @_doesHighlightOverlapRange highlight, start, end
- @removeHighlight highlight
- else if @_isHighlightAfterRange highlight, start, end
- @moveHighlight highlight,
- row: start.row
- column: highlight.column - (end.column - start.column)
+ _.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 =
- highlight.row < start.row or
- (highlight.row == start.row and highlight.column + highlight.word.length <= start.column)
+ highlightRow < start.row or
+ (highlightRow == start.row and highlightEndColumn <= start.column)
highlightIsAllAfterRange =
- highlight.row > end.row or
- (highlight.row == end.row and highlight.column >= end.column)
+ highlightRow > end.row or
+ (highlightRow == end.row and highlightStartColumn >= end.column)
!(highlightIsAllBeforeRange or highlightIsAllAfterRange)
- _isHighlightAfterRange: (highlight, start, end) ->
- return true if highlight.row > end.row
- return false if highlight.row < end.row
- highlight.column >= end.column
-
+ 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)
diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckAdapter.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckAdapter.coffee
new file mode 100644
index 0000000000..2afc2cf0ff
--- /dev/null
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckAdapter.coffee
@@ -0,0 +1,56 @@
+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
+ })
+
+ 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()
diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee
index acbd636531..cbe4fdbd64 100644
--- a/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor/spell-check/SpellCheckManager.coffee
@@ -1,129 +1,88 @@
-define [
- "ide/editor/directives/aceEditor/spell-check/HighlightedWordManager"
- "ace/ace"
-], (HighlightedWordManager) ->
- Range = ace.require("ace/range").Range
-
+define [], () ->
class SpellCheckManager
- constructor: (@$scope, @editor, @element, @cache, @$http, @$q) ->
- $(document.body).append @element.find(".spell-check-menu")
+ constructor: (@$scope, @cache, @$http, @$q, @adapter) ->
+ @$scope.spellMenu = {
+ open: false
+ top: '0px'
+ left: '0px'
+ suggestions: []
+ }
@inProgressRequest = null
@updatedLines = []
- @highlightedWordManager = new HighlightedWordManager(@editor)
- @$scope.$watch "spellCheckLanguage", (language, oldLanguage) =>
+ @$scope.$watch 'spellCheckLanguage', (language, oldLanguage) =>
if language != oldLanguage and oldLanguage?
@runFullCheck()
- onChange = (e) =>
- @runCheckOnChange(e)
-
- onScroll = () =>
- @closeContextMenu()
+ @$scope.replaceWord = @adapter.replaceWord
+ @$scope.learnWord = @learnWord
- @editor.on "changeSession", (e) =>
- @highlightedWordManager.reset()
- if @inProgressRequest?
- @inProgressRequest.abort()
-
- if @$scope.spellCheckEnabled and @$scope.spellCheckLanguage and @$scope.spellCheckLanguage != ""
- @runSpellCheckSoon(200)
-
- e.oldSession?.getDocument().off "change", onChange
- e.session.getDocument().on "change", onChange
-
- e.oldSession?.off "changeScrollTop", onScroll
- e.session.on "changeScrollTop", onScroll
-
- @$scope.spellingMenu = {left: '0px', top: '0px'}
-
- @editor.on "nativecontextmenu", (e) =>
- e.domEvent.stopPropagation();
- @closeContextMenu(e.domEvent)
- @openContextMenu(e.domEvent)
-
- $(document).on "click", (e) =>
- if e.which != 3 # Ignore if this was a right click
- @closeContextMenu(e)
+ $(document).on 'click', (e) =>
+ @closeContextMenu() if e.which != 3 # Ignore if right click
return true
- @$scope.replaceWord = (highlight, suggestion) =>
- @replaceWord(highlight, suggestion)
+ init: () ->
+ @updatedLines = Array(@adapter.getLines().length).fill(true)
+ @runSpellCheckSoon(200) if @isSpellCheckEnabled()
- @$scope.learnWord = (highlight) =>
- @learnWord(highlight)
+ isSpellCheckEnabled: () ->
+ return !!(
+ @$scope.spellCheck and
+ @$scope.spellCheckLanguage and
+ @$scope.spellCheckLanguage != ''
+ )
- runFullCheck: () ->
- @highlightedWordManager.clearRows()
- if @$scope.spellCheckLanguage and @$scope.spellCheckLanguage != ""
- @runSpellCheck()
+ onChange: (e) =>
+ if @isSpellCheckEnabled()
+ @markLinesAsUpdated(@adapter.normalizeChangeEvent(e))
+
+ @adapter.highlightedWordManager.clearHighlightTouchingRange(e)
- runCheckOnChange: (e) ->
- if @$scope.spellCheckLanguage and @$scope.spellCheckLanguage != ""
- @highlightedWordManager.applyChange(e)
- @markLinesAsUpdated(e)
@runSpellCheckSoon()
+ onSessionChange: () =>
+ @adapter.highlightedWordManager.reset()
+ @inProgressRequest.abort() if @inProgressRequest?
+
+ @runSpellCheckSoon(200) if @isSpellCheckEnabled()
+
+ onContextMenu: (e) =>
+ @closeContextMenu()
+ @openContextMenu(e)
+
+ onScroll: () => @closeContextMenu()
+
openContextMenu: (e) ->
- position = @editor.renderer.screenToTextCoordinates(e.clientX, e.clientY)
- highlight = @highlightedWordManager.findHighlightWithinRange
- start: position
- end: position
-
- @$scope.$apply () =>
- @$scope.spellingMenu.highlight = highlight
-
+ coords = @adapter.getCoordsFromContextMenuEvent(e)
+ highlight = @adapter.getHighlightFromCoords(coords)
if highlight
- e.stopPropagation()
- e.preventDefault()
-
- @editor.getSession().getSelection().setSelectionRange(
- new Range(
- highlight.row, highlight.column
- highlight.row, highlight.column + highlight.word.length
- )
- )
-
+ @adapter.preventContextMenuEventDefault(e)
+ @adapter.selectHighlightedWord(highlight)
@$scope.$apply () =>
- @$scope.spellingMenu.open = true
- @$scope.spellingMenu.left = e.clientX + 'px'
- @$scope.spellingMenu.top = e.clientY + 'px'
+ @$scope.spellMenu = {
+ open: true
+ top: coords.y + 'px'
+ left: coords.x + 'px'
+ highlight: highlight
+ }
return false
- closeContextMenu: (e) ->
- # this is triggered on scroll, so for performance only apply
- # setting when it changes
- if @$scope?.spellingMenu?.open != false
+ closeContextMenu: () ->
+ # This is triggered on scroll, so for performance only apply setting when
+ # it changes
+ if @$scope?.spellMenu and @$scope.spellMenu.open != false
@$scope.$apply () =>
- @$scope.spellingMenu.open = false
+ @$scope.spellMenu.open = false
- replaceWord: (highlight, text) ->
- @editor.getSession().replace(new Range(
- highlight.row, highlight.column,
- highlight.row, highlight.column + highlight.word.length
- ), text)
-
- learnWord: (highlight) ->
+ learnWord: (highlight) =>
@apiRequest "/learn", word: highlight.word
- @highlightedWordManager.removeWord highlight.word
+ @adapter.highlightedWordManager.removeWord highlight.word
language = @$scope.spellCheckLanguage
@cache?.put("#{language}:#{highlight.word}", true)
- getHighlightedWordAtCursor: () ->
- cursor = @editor.getCursorPosition()
- highlight = @highlightedWordManager.findHighlightWithinRange
- start: cursor
- end: cursor
- return highlight
-
- runSpellCheckSoon: (delay = 1000) ->
- run = () =>
- delete @timeoutId
- @runSpellCheck(@updatedLines)
- @updatedLines = []
- if @timeoutId?
- clearTimeout @timeoutId
- @timeoutId = setTimeout run, delay
+ runFullCheck: () ->
+ @adapter.highlightedWordManager.reset()
+ @runSpellCheck() if @isSpellCheckEnabled()
markLinesAsUpdated: (change) ->
start = change.start
@@ -146,6 +105,15 @@ define [
@updatedLines[start.row] = true
removeLines()
+ runSpellCheckSoon: (delay = 1000) ->
+ run = () =>
+ delete @timeoutId
+ @runSpellCheck(@updatedLines)
+ @updatedLines = []
+ if @timeoutId?
+ clearTimeout @timeoutId
+ @timeoutId = setTimeout run, delay
+
runSpellCheck: (linesToProcess) ->
{words, positions} = @getWords(linesToProcess)
language = @$scope.spellCheckLanguage
@@ -178,11 +146,11 @@ define [
displayResult = (highlights) =>
if linesToProcess?
for shouldProcess, row in linesToProcess
- @highlightedWordManager.clearRows(row, row) if shouldProcess
+ @adapter.highlightedWordManager.clearRow(row) if shouldProcess
else
- @highlightedWordManager.clearRows()
+ @adapter.highlightedWordManager.reset()
for highlight in highlights
- @highlightedWordManager.addHighlight highlight
+ @adapter.highlightedWordManager.addHighlight highlight
if not words.length
displayResult highlights
@@ -212,8 +180,24 @@ define [
seen[key] = true
displayResult highlights
+ apiRequest: (endpoint, data, callback = (error, result) ->)->
+ data.token = window.user.id
+ data._csrf = window.csrfToken
+ # use angular timeout option to cancel request if doc is changed
+ requestHandler = @$q.defer()
+ options = {timeout: requestHandler.promise}
+ httpRequest = @$http.post("/spelling" + endpoint, data, options)
+ .then (response) =>
+ callback(null, response.data)
+ .catch (response) =>
+ callback(new Error('api failure'))
+ # provide a method to cancel the request
+ abortRequest = () ->
+ requestHandler.resolve()
+ return { abort: abortRequest }
+
getWords: (linesToProcess) ->
- lines = @editor.getValue().split("\n")
+ lines = @adapter.getLines()
words = []
positions = []
for line, row in lines
@@ -232,22 +216,6 @@ define [
words.push(word)
return words: words, positions: positions
- apiRequest: (endpoint, data, callback = (error, result) ->)->
- data.token = window.user.id
- data._csrf = window.csrfToken
- # use angular timeout option to cancel request if doc is changed
- requestHandler = @$q.defer()
- options = {timeout: requestHandler.promise}
- httpRequest = @$http.post("/spelling" + endpoint, data, options)
- .then (response) =>
- callback(null, response.data)
- .catch (response) =>
- callback(new Error('api failure'))
- # provide a method to cancel the request
- abortRequest = () ->
- requestHandler.resolve()
- return { abort: abortRequest }
-
blacklistedCommandRegex: ///
\\ # initial backslash
(label # any of these commands
diff --git a/services/web/public/stylesheets/app/editor/rich-text.less b/services/web/public/stylesheets/app/editor/rich-text.less
index 493540e705..37a896269d 100644
--- a/services/web/public/stylesheets/app/editor/rich-text.less
+++ b/services/web/public/stylesheets/app/editor/rich-text.less
@@ -219,4 +219,11 @@
font-style: italic;
color: #999;
}
+
+ .spelling-error {
+ background-image: url(/img/spellcheck-underline.png);
+ background-repeat: repeat-x;
+ background-position: bottom;
+ }
}
+
diff --git a/services/web/test/unit_frontend/coffee/ide/editor/aceEditor/spell-check/SpellCheckManagerTests.coffee b/services/web/test/unit_frontend/coffee/ide/editor/aceEditor/spell-check/SpellCheckManagerTests.coffee
new file mode 100644
index 0000000000..1b0dddced6
--- /dev/null
+++ b/services/web/test/unit_frontend/coffee/ide/editor/aceEditor/spell-check/SpellCheckManagerTests.coffee
@@ -0,0 +1,46 @@
+define [
+ 'ide/editor/directives/aceEditor/spell-check/SpellCheckManager'
+], (SpellCheckManager) ->
+ describe 'SpellCheckManager', ->
+ beforeEach (done) ->
+ @timelord = sinon.useFakeTimers()
+
+ window.user = { id: 1 }
+ window.csrfToken = 'token'
+ @scope = {
+ $watch: sinon.stub()
+ spellCheck: true
+ spellCheckLanguage: 'en'
+ }
+ @highlightedWordManager = {
+ reset: sinon.stub()
+ clearRow: sinon.stub()
+ addHighlight: sinon.stub()
+ }
+ @adapter = {
+ getLines: sinon.stub()
+ highlightedWordManager: @highlightedWordManager
+ }
+ inject ($q, $http, $httpBackend, $cacheFactory) =>
+ @$http = $http
+ @$q = $q
+ @$httpBackend = $httpBackend
+ cache = $cacheFactory('spellCheckTest', {capacity: 1000})
+ @spellCheckManager = new SpellCheckManager(@scope, cache, $http, $q, @adapter)
+ done()
+
+ afterEach ->
+ @timelord.restore()
+
+ it 'runs a full check soon after init', () ->
+ @$httpBackend.when('POST', '/spelling/check').respond({
+ misspellings: [{
+ index: 0
+ suggestions: ['opposition']
+ }]
+ })
+ @adapter.getLines.returns(['oppozition'])
+ @spellCheckManager.init()
+ @timelord.tick(200)
+ @$httpBackend.flush()
+ expect(@highlightedWordManager.addHighlight).to.have.been.called