diff --git a/native/v8_extensions/native.mm b/native/v8_extensions/native.mm index b19719780..77b924635 100644 --- a/native/v8_extensions/native.mm +++ b/native/v8_extensions/native.mm @@ -27,7 +27,8 @@ namespace v8_extensions { "exists", "read", "write", "absolute", "getAllFilePathsAsync", "traverseTree", "isDirectory", "isFile", "remove", "writeToPasteboard", "readFromPasteboard", "quit", "watchPath", "unwatchPath", "getWatchedPaths", "unwatchAllPaths", "makeDirectory", "move", "moveToTrash", "reload", "lastModified", - "md5ForPath", "exec", "getPlatform", "setWindowState", "getWindowState", "isMisspelled" + "md5ForPath", "exec", "getPlatform", "setWindowState", "getWindowState", "isMisspelled", + "getCorrectionsForMisspelling" }; CefRefPtr nativeObject = CefV8Value::CreateObject(NULL); @@ -528,6 +529,22 @@ namespace v8_extensions { return true; } + else if (name == "getCorrectionsForMisspelling") { + NSString *misspelling = stringFromCefV8Value(arguments[0]); + NSSpellChecker *spellchecker = [NSSpellChecker sharedSpellChecker]; + NSString *language = [spellchecker language]; + NSRange range; + range.location = 0; + range.length = [misspelling length]; + NSArray *guesses = [spellchecker guessesForWordRange:range inString:misspelling language:language inSpellDocumentWithTag:0]; + CefRefPtr v8Guesses = CefV8Value::CreateArray([guesses count]); + for (int i = 0; i < [guesses count]; i++) { + v8Guesses->SetValue(i, CefV8Value::CreateString([[guesses objectAtIndex:i] UTF8String])); + } + retval = v8Guesses; + return true; + } + return false; }; diff --git a/src/packages/spell-check/keymaps/spell-check.cson b/src/packages/spell-check/keymaps/spell-check.cson new file mode 100644 index 000000000..fd34ad96e --- /dev/null +++ b/src/packages/spell-check/keymaps/spell-check.cson @@ -0,0 +1,2 @@ +'.editor': + 'meta-0': 'editor:correct-misspelling' diff --git a/src/packages/spell-check/lib/corrections-view.coffee b/src/packages/spell-check/lib/corrections-view.coffee new file mode 100644 index 000000000..b0041a8c9 --- /dev/null +++ b/src/packages/spell-check/lib/corrections-view.coffee @@ -0,0 +1,71 @@ +{$$} = require 'space-pen' +Range = require 'range' +SelectList = require 'select-list' + +module.exports = +class CorrectionsView extends SelectList + @viewClass: -> "corrections #{super} popover-list" + + editor: null + corrections: null + misspellingRange: null + aboveCursor: false + + initialize: (@editor, @corrections, @misspellingRange) -> + super + + @attach() + + itemForElement: (word) -> + $$ -> + @li word + + selectNextItem: -> + super + + false + + selectPreviousItem: -> + super + + false + + confirmed: (correction) -> + @cancel() + return unless correction + @editor.transact => + @editor.setSelectedBufferRange(@editor.bufferRangeForScreenRange(@misspellingRange)) + @editor.insertText(correction) + + attach: -> + @aboveCursor = false + if @corrections.length > 0 + @setArray(@corrections) + else + @setError("No corrections found") + + @editor.appendToLinesView(this) + @setPosition() + @miniEditor.focus() + + detach: -> + super + + @editor.focus() + + setPosition: -> + { left, top } = @editor.pixelPositionForScreenPosition(@misspellingRange.start) + height = @outerHeight() + potentialTop = top + @editor.lineHeight + potentialBottom = potentialTop - @editor.scrollTop() + height + + if @aboveCursor or potentialBottom > @editor.outerHeight() + @aboveCursor = true + @css(left: left, top: top - height, bottom: 'inherit') + else + @css(left: left, top: potentialTop, bottom: 'inherit') + + populateList: -> + super + + @setPosition() diff --git a/src/packages/spell-check/lib/misspelling-view.coffee b/src/packages/spell-check/lib/misspelling-view.coffee index c56ff2b3f..eeb537671 100644 --- a/src/packages/spell-check/lib/misspelling-view.coffee +++ b/src/packages/spell-check/lib/misspelling-view.coffee @@ -1,5 +1,6 @@ {View} = require 'space-pen' Range = require 'range' +CorrectionsView = require './corrections-view' module.exports = class MisspellingView extends View @@ -11,19 +12,37 @@ class MisspellingView extends View range = @editSession.screenRangeForBufferRange(Range.fromObject(range)) @startPosition = range.start @endPosition = range.end + @misspellingValid = true @marker = @editSession.markScreenRange(range, invalidationStrategy: 'between') @editSession.observeMarker @marker, ({newHeadScreenPosition, newTailScreenPosition, valid}) => @startPosition = newTailScreenPosition @endPosition = newHeadScreenPosition @updateDisplayPosition = valid + @misspellingValid = valid @hide() unless valid - @editor.on 'editor:display-updated', => + @subscribe @editor, 'editor:display-updated', => @updatePosition() if @updateDisplayPosition + @editor.command 'editor:correct-misspelling', => + return unless @misspellingValid and @containsCursor() + + screenRange = @getScreenRange() + misspelling = @editor.getTextInRange(@editor.bufferRangeForScreenRange(screenRange)) + corrections = $native.getCorrectionsForMisspelling(misspelling) + @correctionsView?.remove() + @correctionsView = new CorrectionsView(@editor, corrections, screenRange) + @updatePosition() + getScreenRange: -> + new Range(@startPosition, @endPosition) + + containsCursor: -> + cursor = @editor.getCursorScreenPosition() + @getScreenRange().containsPoint(cursor, exclusive: false) + updatePosition: -> @updateDisplayPosition = false startPixelPosition = @editor.pixelPositionForScreenPosition(@startPosition) @@ -36,5 +55,7 @@ class MisspellingView extends View @show() destroy: -> + @misspellingValid = false @editSession.destroyMarker(@marker) + @correctionsView?.remove() @remove() diff --git a/src/packages/spell-check/spec/spell-check-spec.coffee b/src/packages/spell-check/spec/spell-check-spec.coffee index 935ce5a7e..abdb98ac3 100644 --- a/src/packages/spell-check/spec/spell-check-spec.coffee +++ b/src/packages/spell-check/spec/spell-check-spec.coffee @@ -59,3 +59,40 @@ describe "Spell check", -> expect(editor.find('.misspelling').length).toBe 1 config.set('spell-check.grammars', []) expect(editor.find('.misspelling').length).toBe 0 + + describe "when 'editor:correct-misspelling' is triggered on the editor", -> + describe "when the cursor touches a misspelling that has corrections", -> + it "displays the corrections for the misspelling and replaces the misspelling when a correction is selected", -> + editor.setText('fryday') + advanceClock(editor.getBuffer().stoppedChangingDelay) + config.set('spell-check.grammars', ['source.js']) + + waitsFor -> + editor.find('.misspelling').length > 0 + + runs -> + editor.trigger 'editor:correct-misspelling' + expect(editor.find('.corrections').length).toBe 1 + expect(editor.find('.corrections li').length).toBeGreaterThan 0 + expect(editor.find('.corrections li:first').text()).toBe "Friday" + editor.find('.corrections').view().confirmSelection() + expect(editor.getText()).toBe 'Friday' + expect(editor.getCursorBufferPosition()).toEqual [0, 6] + advanceClock(editor.getBuffer().stoppedChangingDelay) + expect(editor.find('.misspelling')).toBeHidden() + expect(editor.find('.corrections').length).toBe 0 + + describe "when the cursor touches a misspelling that has no corrections", -> + it "displays a message saying no corrections found", -> + editor.setText('asdfasdf') + advanceClock(editor.getBuffer().stoppedChangingDelay) + config.set('spell-check.grammars', ['source.js']) + + waitsFor -> + editor.find('.misspelling').length > 0 + + runs -> + editor.trigger 'editor:correct-misspelling' + expect(editor.find('.corrections').length).toBe 1 + expect(editor.find('.corrections li').length).toBe 0 + expect(editor.find('.corrections .error').text()).toBe "No corrections found"