mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #528 from sharelatex/as-cm-spelling
Rich text spelling
This commit is contained in:
commit
5b3fbe47db
8 changed files with 329 additions and 255 deletions
|
@ -1,5 +1,6 @@
|
|||
define [
|
||||
"ide/editor/Document"
|
||||
"ide/editor/components/spellMenu"
|
||||
"ide/editor/directives/aceEditor"
|
||||
"ide/editor/directives/toggleSwitch"
|
||||
"ide/editor/controllers/SavingNotificationController"
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
define ["base"], (App) ->
|
||||
App.component "spellMenu", {
|
||||
bindings: {
|
||||
open: "<"
|
||||
top: "<"
|
||||
left: "<"
|
||||
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}"
|
||||
>
|
||||
<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>
|
||||
"""
|
||||
}
|
|
@ -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</a>
|
||||
</div>
|
||||
<div class="ace-editor-body"></div>
|
||||
<div
|
||||
class="dropdown context-menu spell-check-menu"
|
||||
ng-show="spellingMenu.open"
|
||||
ng-style="{top: spellingMenu.top, left: spellingMenu.left}"
|
||||
ng-class="{open: spellingMenu.open}"
|
||||
>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="suggestion in spellingMenu.highlight.suggestions | limitTo:8">
|
||||
<a href ng-click="replaceWord(spellingMenu.highlight, suggestion)">{{ suggestion }}</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li>
|
||||
<a href ng-click="learnWord(spellingMenu.highlight)">Add to Dictionary</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<spell-menu
|
||||
open="spellMenu.open"
|
||||
top="spellMenu.top"
|
||||
left="spellMenu.left"
|
||||
highlight="spellMenu.highlight"
|
||||
replace-word="replaceWord(highlight, suggestion)"
|
||||
learn-word="learnWord(highlight)"
|
||||
></spell-menu>
|
||||
<div
|
||||
class="annotation-label"
|
||||
ng-show="annotationLabel.show"
|
||||
|
|
|
@ -4,144 +4,93 @@ define [
|
|||
Range = ace.require("ace/range").Range
|
||||
|
||||
class Highlight
|
||||
constructor: (options) ->
|
||||
@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)
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue