diff --git a/README.md b/README.md index 876a6697f..11eee545e 100644 --- a/README.md +++ b/README.md @@ -17,4 +17,3 @@ Requirements 1. gh-setup atom 2. cd ~/github/atom && `rake install` - diff --git a/docs/getting-started.md b/docs/getting-started.md index 61067b76e..0c494952a 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,6 +1,6 @@ # Getting Started -Welcome to Atom. This documentation is intented to offer a basic introduction +Welcome to Atom. This documentation is intended to offer a basic introduction of how to get productive with this editor. Then we'll delve into more details about configuring, theming, and extending Atom. diff --git a/native/v8_extensions/native.mm b/native/v8_extensions/native.mm index 6af7622fa..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" + "md5ForPath", "exec", "getPlatform", "setWindowState", "getWindowState", "isMisspelled", + "getCorrectionsForMisspelling" }; CefRefPtr nativeObject = CefV8Value::CreateObject(NULL); @@ -521,6 +522,29 @@ namespace v8_extensions { return true; } + else if (name == "isMisspelled") { + NSString *word = stringFromCefV8Value(arguments[0]); + NSRange range = [[NSSpellChecker sharedSpellChecker] checkSpellingOfString:word startingAt:0]; + retval = CefV8Value::CreateBool(range.length > 0); + 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/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index 635f7d519..3510d46f4 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -741,6 +741,7 @@ describe 'Buffer', -> oldTailPosition: [4, 20] newTailPosition: [4, 20] bufferChanged: false + valid: true } observeHandler.reset() @@ -751,6 +752,7 @@ describe 'Buffer', -> oldHeadPosition: [6, 2] newHeadPosition: [6, 5] bufferChanged: true + valid: true } it "calls the given callback when the marker's tail position changes", -> @@ -762,6 +764,7 @@ describe 'Buffer', -> oldTailPosition: [4, 20] newTailPosition: [6, 2] bufferChanged: false + valid: true } observeHandler.reset() @@ -773,6 +776,7 @@ describe 'Buffer', -> oldTailPosition: [6, 2] newTailPosition: [6, 5] bufferChanged: true + valid: true } it "calls the callback when the selection's tail is cleared", -> @@ -784,6 +788,7 @@ describe 'Buffer', -> oldTailPosition: [4, 20] newTailPosition: [4, 23] bufferChanged: false + valid: true } it "only calls the callback once when both the marker's head and tail positions change due to the same operation", -> @@ -795,6 +800,7 @@ describe 'Buffer', -> oldHeadPosition: [4, 23] newHeadPosition: [4, 26] bufferChanged: true + valid: true } observeHandler.reset() @@ -806,6 +812,31 @@ describe 'Buffer', -> oldHeadPosition: [4, 26] newHeadPosition: [1, 1] bufferChanged: false + valid: true + } + + it "calls the callback with the valid flag set to false when the marker is invalidated", -> + buffer.deleteRow(4) + expect(observeHandler.callCount).toBe 1 + expect(observeHandler.argsForCall[0][0]).toEqual { + oldTailPosition: [4, 20] + newTailPosition: [4, 20] + oldHeadPosition: [4, 23] + newHeadPosition: [4, 23] + bufferChanged: true + valid: false + } + + observeHandler.reset() + buffer.undo() + expect(observeHandler.callCount).toBe 1 + expect(observeHandler.argsForCall[0][0]).toEqual { + oldTailPosition: [4, 20] + newTailPosition: [4, 20] + oldHeadPosition: [4, 23] + newHeadPosition: [4, 23] + bufferChanged: true + valid: true } it "allows the observation subscription to be cancelled", -> @@ -830,19 +861,20 @@ describe 'Buffer', -> buffer.undo() expect(buffer.getMarkerRange(marker)).toBeUndefined() - # even "stayValid" markers get destroyed properly - marker2 = buffer.markRange([[4, 20], [4, 23]], stayValid: true) + # even "invalidationStrategy: never" markers get destroyed properly + marker2 = buffer.markRange([[4, 20], [4, 23]], invalidationStrategy: 'never') buffer.delete([[4, 15], [4, 25]]) buffer.destroyMarker(marker2) buffer.undo() expect(buffer.getMarkerRange(marker2)).toBeUndefined() describe "marker updates due to buffer changes", -> - [marker1, marker2] = [] + [marker1, marker2, marker3] = [] beforeEach -> marker1 = buffer.markRange([[4, 20], [4, 23]]) - marker2 = buffer.markRange([[4, 20], [4, 23]], stayValid: true) + marker2 = buffer.markRange([[4, 20], [4, 23]], invalidationStrategy: 'never') + marker3 = buffer.markRange([[4, 20], [4, 23]], invalidationStrategy: 'between') describe "when the buffer changes due to a new operation", -> describe "when the change precedes the marker range", -> @@ -874,20 +906,37 @@ describe 'Buffer', -> buffer.insert([4, 20], '...') expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 26]] + describe "when the invalidation strategy is 'between'", -> + it "invalidates the marker", -> + buffer.insert([4, 20], '...') + expect(buffer.getMarkerRange(marker3)).toBeUndefined() + describe "when the change is an insertion at the end of the marker range", -> it "moves the end point", -> buffer.insert([4, 23], '...') expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 26]] + describe "when the invalidation strategy is 'between'", -> + it "invalidates the marker", -> + buffer.insert([4, 23], '...') + expect(buffer.getMarkerRange(marker3)).toBeUndefined() + describe "when the change surrounds the marker range", -> - describe "when the marker was created with stayValid: false (the default)", -> + describe "when the marker's invalidation strategy is 'contains' (the default)", -> it "invalidates the marker", -> buffer.delete([[4, 15], [4, 25]]) expect(buffer.getMarkerRange(marker1)).toBeUndefined() buffer.undo() expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] - describe "when the marker was created with stayValid: true", -> + describe "when the marker's invalidation strategy is 'between'", -> + it "invalidates the marker", -> + buffer.delete([[4, 15], [4, 25]]) + expect(buffer.getMarkerRange(marker3)).toBeUndefined() + buffer.undo() + expect(buffer.getMarkerRange(marker3)).toEqual [[4, 20], [4, 23]] + + describe "when the marker's invalidation strategy is 'never'", -> it "does not invalidate the marker, but sets it to an empty range at the end of the change", -> buffer.change([[4, 15], [4, 25]], "...") expect(buffer.getMarkerRange(marker2)).toEqual [[4, 18], [4, 18]] @@ -895,14 +944,21 @@ describe 'Buffer', -> expect(buffer.getMarkerRange(marker2)).toEqual [[4, 20], [4, 23]] describe "when the change straddles the start of the marker range", -> - describe "when the marker was created with stayValid: false (the default)", -> + describe "when the marker's invalidation strategy is 'contains' (the default)", -> it "invalidates the marker", -> buffer.delete([[4, 15], [4, 22]]) expect(buffer.getMarkerRange(marker1)).toBeUndefined() buffer.undo() expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] - describe "when the marker was created with stayValid: true", -> + describe "when the marker's invalidation strategy is 'between'", -> + it "invalidates the marker", -> + buffer.delete([[4, 15], [4, 22]]) + expect(buffer.getMarkerRange(marker3)).toBeUndefined() + buffer.undo() + expect(buffer.getMarkerRange(marker3)).toEqual [[4, 20], [4, 23]] + + describe "when the marker's invalidation strategy is 'never'", -> it "moves the start of the marker range to the end of the change", -> buffer.delete([[4, 15], [4, 22]]) expect(buffer.getMarkerRange(marker2)).toEqual [[4, 15], [4, 16]] @@ -910,20 +966,49 @@ describe 'Buffer', -> expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] describe "when the change straddles the end of the marker range", -> - describe "when the marker was created with stayValid: false (the default)", -> + describe "when the marker's invalidation strategy is 'contains' (the default)", -> it "invalidates the marker", -> buffer.delete([[4, 22], [4, 25]]) expect(buffer.getMarkerRange(marker1)).toBeUndefined() buffer.undo() expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] - describe "when the marker was created with stayValid: true", -> + describe "when the marker's invalidation strategy is 'between'", -> + it "invalidates the marker", -> + buffer.delete([[4, 22], [4, 25]]) + expect(buffer.getMarkerRange(marker3)).toBeUndefined() + buffer.undo() + expect(buffer.getMarkerRange(marker3)).toEqual [[4, 20], [4, 23]] + + describe "when the marker's invalidation strategy is 'never'", -> it "moves the end of the marker range to the start of the change", -> buffer.delete([[4, 22], [4, 25]]) expect(buffer.getMarkerRange(marker2)).toEqual [[4, 20], [4, 22]] buffer.undo() expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + describe "when the change is between the start and the end of the marker range", -> + describe "when the marker's invalidation strategy is 'contains' (the default)", -> + it "does not invalidate the marker", -> + buffer.insert([4, 21], 'x') + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 24]] + buffer.undo() + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + + describe "when the marker's invalidation strategy is 'between'", -> + it "invalidates the marker", -> + buffer.insert([4, 21], 'x') + expect(buffer.getMarkerRange(marker3)).toBeUndefined() + buffer.undo() + expect(buffer.getMarkerRange(marker3)).toEqual [[4, 20], [4, 23]] + + describe "when the marker's invalidation strategy is 'never'", -> + it "moves the end of the marker range to the start of the change", -> + buffer.insert([4, 21], 'x') + expect(buffer.getMarkerRange(marker2)).toEqual [[4, 20], [4, 24]] + buffer.undo() + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + describe "when the buffer changes due to the undo or redo of a previous operation", -> it "restores invalidated markers when undoing/redoing in the other direction", -> buffer.change([[4, 21], [4, 24]], "foo") diff --git a/spec/app/display-buffer-spec.coffee b/spec/app/display-buffer-spec.coffee index 34f8fc74b..6f87a7ddb 100644 --- a/spec/app/display-buffer-spec.coffee +++ b/spec/app/display-buffer-spec.coffee @@ -661,6 +661,7 @@ describe "DisplayBuffer", -> newTailScreenPosition: [5, 4] newTailBufferPosition: [8, 4] bufferChanged: false + valid: true } observeHandler.reset() @@ -676,6 +677,7 @@ describe "DisplayBuffer", -> newTailScreenPosition: [5, 4] newTailBufferPosition: [8, 4] bufferChanged: true + valid: true } observeHandler.reset() @@ -691,6 +693,7 @@ describe "DisplayBuffer", -> newTailScreenPosition: [8, 4] newTailBufferPosition: [8, 4] bufferChanged: false + valid: true } observeHandler.reset() @@ -706,6 +709,7 @@ describe "DisplayBuffer", -> newTailScreenPosition: [5, 4] newTailBufferPosition: [8, 4] bufferChanged: false + valid: true } it "calls the callback whenever the marker tail's position changes in the buffer or on screen", -> @@ -721,6 +725,7 @@ describe "DisplayBuffer", -> newTailScreenPosition: [8, 20] newTailBufferPosition: [11, 20] bufferChanged: false + valid: true } observeHandler.reset() @@ -736,6 +741,40 @@ describe "DisplayBuffer", -> newTailScreenPosition: [8, 23] newTailBufferPosition: [11, 23] bufferChanged: true + valid: true + } + + it "calls the callback whenever the marker is invalidated or revalidated", -> + buffer.deleteRow(8) + expect(observeHandler).toHaveBeenCalled() + expect(observeHandler.argsForCall[0][0]).toEqual { + oldHeadScreenPosition: [5, 10] + oldHeadBufferPosition: [8, 10] + newHeadScreenPosition: [5, 10] + newHeadBufferPosition: [8, 10] + oldTailScreenPosition: [5, 4] + oldTailBufferPosition: [8, 4] + newTailScreenPosition: [5, 4] + newTailBufferPosition: [8, 4] + bufferChanged: true + valid: false + } + + observeHandler.reset() + buffer.undo() + + expect(observeHandler).toHaveBeenCalled() + expect(observeHandler.argsForCall[0][0]).toEqual { + oldHeadScreenPosition: [5, 10] + oldHeadBufferPosition: [8, 10] + newHeadScreenPosition: [5, 10] + newHeadBufferPosition: [8, 10] + oldTailScreenPosition: [5, 4] + oldTailBufferPosition: [8, 4] + newTailScreenPosition: [5, 4] + newTailBufferPosition: [8, 4] + bufferChanged: true + valid: true } it "does not call the callback for screen changes that don't change the position of the marker", -> diff --git a/spec/stdlib/onig-reg-exp-spec.coffee b/spec/stdlib/onig-reg-exp-spec.coffee index 14955c317..7f43d47c7 100644 --- a/spec/stdlib/onig-reg-exp-spec.coffee +++ b/spec/stdlib/onig-reg-exp-spec.coffee @@ -3,13 +3,13 @@ OnigRegExp = require 'onig-reg-exp' describe "OnigRegExp", -> describe ".search(string, index)", -> it "returns an array of the match and all capture groups", -> - regex = new OnigRegExp("\\w(\\d+)") + regex = OnigRegExp.create("\\w(\\d+)") result = regex.search("----a123----") expect(result).toEqual ["a123", "123"] expect(result.index).toBe 4 expect(result.indices).toEqual [4, 5] it "returns null if it does not match", -> - regex = new OnigRegExp("\\w(\\d+)") + regex = OnigRegExp.create("\\w(\\d+)") result = regex.search("--------") expect(result).toBeNull() diff --git a/src/app/buffer-change-operation.coffee b/src/app/buffer-change-operation.coffee index 928b4a07b..b3ffda35e 100644 --- a/src/app/buffer-change-operation.coffee +++ b/src/app/buffer-change-operation.coffee @@ -107,5 +107,4 @@ class BufferChangeOperation if validMarker = @buffer.validMarkers[id] validMarker.setRange(previousRange) else if invalidMarker = @buffer.invalidMarkers[id] - @buffer.validMarkers[id] = invalidMarker - + invalidMarker.revalidate() diff --git a/src/app/buffer-marker.coffee b/src/app/buffer-marker.coffee index 245e0142c..199171595 100644 --- a/src/app/buffer-marker.coffee +++ b/src/app/buffer-marker.coffee @@ -8,9 +8,10 @@ class BufferMarker headPosition: null tailPosition: null suppressObserverNotification: false - stayValid: false + invalidationStrategy: null - constructor: ({@id, @buffer, range, @stayValid, noTail, reverse}) -> + constructor: ({@id, @buffer, range, @invalidationStrategy, noTail, reverse}) -> + @invalidationStrategy ?= 'contains' @setRange(range, {noTail, reverse}) setRange: (range, options={}) -> @@ -71,23 +72,30 @@ class BufferMarker newTailPosition = @getTailPosition() @notifyObservers({oldTailPosition, newTailPosition, bufferChanged: false}) - tryToInvalidate: (oldRange) -> - containsStart = oldRange.containsPoint(@getStartPosition(), exclusive: true) - containsEnd = oldRange.containsPoint(@getEndPosition(), exclusive: true) - return unless containsEnd or containsStart + tryToInvalidate: (changedRange) -> + betweenStartAndEnd = @getRange().containsRange(changedRange, exclusive: false) + containsStart = changedRange.containsPoint(@getStartPosition(), exclusive: true) + containsEnd = changedRange.containsPoint(@getEndPosition(), exclusive: true) - if @stayValid - previousRange = @getRange() - if containsStart and containsEnd - @setRange([oldRange.end, oldRange.end]) - else if containsStart - @setRange([oldRange.end, @getEndPosition()]) - else - @setRange([@getStartPosition(), oldRange.start]) - [@id, previousRange] - else - @invalidate() - [@id] + switch @invalidationStrategy + when 'between' + if betweenStartAndEnd or containsStart or containsEnd + @invalidate() + [@id] + when 'contains' + if containsStart or containsEnd + @invalidate() + [@id] + when 'never' + if containsStart or containsEnd + previousRange = @getRange() + if containsStart and containsEnd + @setRange([changedRange.end, changedRange.end]) + else if containsStart + @setRange([changedRange.end, @getEndPosition()]) + else + @setRange([@getStartPosition(), changedRange.start]) + [@id, previousRange] handleBufferChange: (bufferChange) -> @consolidateObserverNotifications true, => @@ -113,23 +121,31 @@ class BufferMarker [newRow, newColumn] observe: (callback) -> - @on 'position-changed', callback + @on 'changed', callback cancel: => @unobserve(callback) unobserve: (callback) -> - @off 'position-changed', callback + @off 'changed', callback containsPoint: (point) -> @getRange().containsPoint(point) - notifyObservers: ({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged}) -> + notifyObservers: ({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged} = {}) -> return if @suppressObserverNotification - return if _.isEqual(newHeadPosition, oldHeadPosition) and _.isEqual(newTailPosition, oldTailPosition) + + if newHeadPosition? and newTailPosition? + return if _.isEqual(newHeadPosition, oldHeadPosition) and _.isEqual(newTailPosition, oldTailPosition) + else if newHeadPosition? + return if _.isEqual(newHeadPosition, oldHeadPosition) + else if newTailPosition? + return if _.isEqual(newTailPosition, oldTailPosition) + oldHeadPosition ?= @getHeadPosition() newHeadPosition ?= @getHeadPosition() oldTailPosition ?= @getTailPosition() newTailPosition ?= @getTailPosition() - @trigger 'position-changed', {oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged} + valid = @buffer.validMarkers[@id]? + @trigger 'changed', {oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged, valid} consolidateObserverNotifications: (bufferChanged, fn) -> @suppressObserverNotification = true @@ -141,8 +157,14 @@ class BufferMarker @suppressObserverNotification = false @notifyObservers({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged}) - invalidate: (preserve) -> + invalidate: -> delete @buffer.validMarkers[@id] @buffer.invalidMarkers[@id] = this + @notifyObservers(bufferChanged: true) + + revalidate: -> + delete @buffer.invalidMarkers[@id] + @buffer.validMarkers[@id] = this + @notifyObservers(bufferChanged: true) _.extend BufferMarker.prototype, EventEmitter diff --git a/src/app/display-buffer-marker.coffee b/src/app/display-buffer-marker.coffee index d4ce6936f..c8ed1648a 100644 --- a/src/app/display-buffer-marker.coffee +++ b/src/app/display-buffer-marker.coffee @@ -7,6 +7,7 @@ class DisplayBufferMarker bufferMarkerSubscription: null headScreenPosition: null tailScreenPosition: null + valid: true constructor: ({@id, @displayBuffer}) -> @buffer = @displayBuffer.buffer @@ -57,11 +58,11 @@ class DisplayBufferMarker observe: (callback) -> @observeBufferMarkerIfNeeded() - @on 'position-changed', callback + @on 'changed', callback cancel: => @unobserve(callback) unobserve: (callback) -> - @off 'position-changed', callback + @off 'changed', callback @unobserveBufferMarkerIfNeeded() observeBufferMarkerIfNeeded: -> @@ -69,13 +70,14 @@ class DisplayBufferMarker @getHeadScreenPosition() # memoize current value @getTailScreenPosition() # memoize current value @bufferMarkerSubscription = - @buffer.observeMarker @id, ({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged}) => + @buffer.observeMarker @id, ({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged, valid}) => @notifyObservers oldHeadBufferPosition: oldHeadPosition newHeadBufferPosition: newHeadPosition oldTailBufferPosition: oldTailPosition newTailBufferPosition: newTailPosition bufferChanged: bufferChanged + valid: valid @displayBuffer.markers[@id] = this unobserveBufferMarkerIfNeeded: -> @@ -83,28 +85,37 @@ class DisplayBufferMarker @bufferMarkerSubscription.cancel() delete @displayBuffer.markers[@id] - notifyObservers: ({oldHeadBufferPosition, oldTailBufferPosition, bufferChanged}) -> + notifyObservers: ({oldHeadBufferPosition, oldTailBufferPosition, bufferChanged, valid} = {}) -> oldHeadScreenPosition = @getHeadScreenPosition() - @headScreenPosition = null - newHeadScreenPosition = @getHeadScreenPosition() - + newHeadScreenPosition = oldHeadScreenPosition oldTailScreenPosition = @getTailScreenPosition() - @tailScreenPosition = null - newTailScreenPosition = @getTailScreenPosition() + newTailScreenPosition = oldTailScreenPosition + valid ?= true - return if _.isEqual(newHeadScreenPosition, oldHeadScreenPosition) and _.isEqual(newTailScreenPosition, oldTailScreenPosition) + if valid + @headScreenPosition = null + newHeadScreenPosition = @getHeadScreenPosition() + @tailScreenPosition = null + newTailScreenPosition = @getTailScreenPosition() + + validChanged = valid isnt @valid + headScreenPositionChanged = not _.isEqual(newHeadScreenPosition, oldHeadScreenPosition) + tailScreenPositionChanged = not _.isEqual(newTailScreenPosition, oldTailScreenPosition) + return unless validChanged or headScreenPositionChanged or tailScreenPositionChanged oldHeadBufferPosition ?= @getHeadBufferPosition() - newHeadBufferPosition = @getHeadBufferPosition() + newHeadBufferPosition = @getHeadBufferPosition() ? oldHeadBufferPosition oldTailBufferPosition ?= @getTailBufferPosition() - newTailBufferPosition = @getTailBufferPosition() + newTailBufferPosition = @getTailBufferPosition() ? oldTailBufferPosition + @valid = valid - @trigger 'position-changed', { + @trigger 'changed', { oldHeadScreenPosition, newHeadScreenPosition, oldTailScreenPosition, newTailScreenPosition, oldHeadBufferPosition, newHeadBufferPosition, oldTailBufferPosition, newTailBufferPosition, bufferChanged + valid } _.extend DisplayBufferMarker.prototype, EventEmitter diff --git a/src/app/display-buffer.coffee b/src/app/display-buffer.coffee index c029a1124..0f24a235d 100644 --- a/src/app/display-buffer.coffee +++ b/src/app/display-buffer.coffee @@ -332,8 +332,9 @@ class DisplayBuffer getMarkers: -> _.values(@markers) - markScreenRange: (screenRange) -> - @markBufferRange(@bufferRangeForScreenRange(screenRange)) + markScreenRange: (args...) -> + bufferRange = @bufferRangeForScreenRange(args.shift()) + @markBufferRange(bufferRange, args...) markBufferRange: (args...) -> @buffer.markRange(args...) diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index f2f9bbbe9..b160d5468 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -538,11 +538,11 @@ class EditSession _.last(@cursors) addCursorAtScreenPosition: (screenPosition) -> - marker = @markScreenPosition(screenPosition, stayValid: true) + marker = @markScreenPosition(screenPosition, invalidationStrategy: 'never') @addSelection(marker).cursor addCursorAtBufferPosition: (bufferPosition) -> - marker = @markBufferPosition(bufferPosition, stayValid: true) + marker = @markBufferPosition(bufferPosition, invalidationStrategy: 'never') @addSelection(marker).cursor addCursor: (marker) -> @@ -565,7 +565,7 @@ class EditSession selection addSelectionForBufferRange: (bufferRange, options={}) -> - options = _.defaults({stayValid: true}, options) + options = _.defaults({invalidationStrategy: 'never'}, options) marker = @markBufferRange(bufferRange, options) @addSelection(marker) diff --git a/src/app/git.coffee b/src/app/git.coffee index 2b46fd4ee..866e2977f 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -24,7 +24,7 @@ class Git ignore: 1 << 14 constructor: (path, options={}) -> - @repo = new GitRepository(path) + @repo = GitRepository.open(path) refreshIndexOnFocus = options.refreshIndexOnFocus ? true if refreshIndexOnFocus $ = require 'jquery' diff --git a/src/app/language-mode.coffee b/src/app/language-mode.coffee index e8db1c774..848ae826f 100644 --- a/src/app/language-mode.coffee +++ b/src/app/language-mode.coffee @@ -30,13 +30,13 @@ class LanguageMode buffer = @editSession.buffer commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '($1)?') - commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})") + commentStartRegex = OnigRegExp.create("^(\\s*)(#{commentStartRegexString})") shouldUncomment = commentStartRegex.test(buffer.lineForRow(start)) if commentEndString = syntax.getProperty(scopes, "editor.commentEnd") if shouldUncomment commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '($1)?') - commentEndRegex = new OnigRegExp("(#{commentEndRegexString})(\\s*)$") + commentEndRegex = OnigRegExp.create("(#{commentEndRegexString})(\\s*)$") startMatch = commentStartRegex.search(buffer.lineForRow(start)) endMatch = commentEndRegex.search(buffer.lineForRow(end)) if startMatch and endMatch @@ -152,12 +152,12 @@ class LanguageMode increaseIndentRegexForScopes: (scopes) -> if increaseIndentPattern = syntax.getProperty(scopes, 'editor.increaseIndentPattern') - new OnigRegExp(increaseIndentPattern) + OnigRegExp.create(increaseIndentPattern) decreaseIndentRegexForScopes: (scopes) -> if decreaseIndentPattern = syntax.getProperty(scopes, 'editor.decreaseIndentPattern') - new OnigRegExp(decreaseIndentPattern) + OnigRegExp.create(decreaseIndentPattern) foldEndRegexForScopes: (scopes) -> if foldEndPattern = syntax.getProperty(scopes, 'editor.foldEndPattern') - new OnigRegExp(foldEndPattern) + OnigRegExp.create(foldEndPattern) diff --git a/src/app/range.coffee b/src/app/range.coffee index 136f52f15..a3870ea66 100644 --- a/src/app/range.coffee +++ b/src/app/range.coffee @@ -57,7 +57,11 @@ class Range else otherRange.intersectsWith(this) - containsPoint: (point, { exclusive } = {}) -> + containsRange: (otherRange, {exclusive} = {}) -> + { start, end } = Range.fromObject(otherRange) + @containsPoint(start, {exclusive}) and @containsPoint(end, {exclusive}) + + containsPoint: (point, {exclusive} = {}) -> point = Point.fromObject(point) if exclusive point.isGreaterThan(@start) and point.isLessThan(@end) diff --git a/src/app/text-mate-grammar.coffee b/src/app/text-mate-grammar.coffee index 9cc6d105d..01acb2f77 100644 --- a/src/app/text-mate-grammar.coffee +++ b/src/app/text-mate-grammar.coffee @@ -28,7 +28,7 @@ class TextMateGrammar constructor: ({ @name, @fileTypes, @scopeName, patterns, repository, @foldingStopMarker, firstLineMatch}) -> @initialRule = new Rule(this, {@scopeName, patterns}) @repository = {} - @firstLineRegex = new OnigRegExp(firstLineMatch) if firstLineMatch + @firstLineRegex = OnigRegExp.create(firstLineMatch) if firstLineMatch @fileTypes ?= [] for name, data of repository @@ -111,7 +111,7 @@ class Rule regex = pattern.regexSource regexes.push regex if regex - regexScanner = new OnigScanner(regexes) + regexScanner = OnigScanner.create(regexes) regexScanner.patterns = patterns @scannersByBaseGrammarName[baseGrammar.name] = regexScanner unless anchored regexScanner diff --git a/src/packages/autocomplete/lib/autocomplete.coffee b/src/packages/autocomplete/lib/autocomplete.coffee index d98ed5a75..fc0e3000b 100644 --- a/src/packages/autocomplete/lib/autocomplete.coffee +++ b/src/packages/autocomplete/lib/autocomplete.coffee @@ -7,4 +7,3 @@ module.exports = rootView.eachEditor (editor) => if editor.attached and not editor.mini @autoCompleteViews.push new AutocompleteView(editor) - 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 new file mode 100644 index 000000000..eeb537671 --- /dev/null +++ b/src/packages/spell-check/lib/misspelling-view.coffee @@ -0,0 +1,61 @@ +{View} = require 'space-pen' +Range = require 'range' +CorrectionsView = require './corrections-view' + +module.exports = +class MisspellingView extends View + @content: -> + @div class: 'misspelling' + + initialize: (range, @editor) -> + @editSession = @editor.activeEditSession + 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 + + @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) + endPixelPosition = @editor.pixelPositionForScreenPosition(@endPosition) + @css + top: startPixelPosition.top + left: startPixelPosition.left + width: endPixelPosition.left - startPixelPosition.left + height: @editor.lineHeight + @show() + + destroy: -> + @misspellingValid = false + @editSession.destroyMarker(@marker) + @correctionsView?.remove() + @remove() diff --git a/src/packages/spell-check/lib/spell-check-handler.coffee b/src/packages/spell-check/lib/spell-check-handler.coffee new file mode 100644 index 000000000..8040f4991 --- /dev/null +++ b/src/packages/spell-check/lib/spell-check-handler.coffee @@ -0,0 +1,14 @@ +module.exports = + findMisspellings: (text) -> + wordRegex = /(?:^|[\s\[\]])([a-zA-Z']+)(?=[\s\.\[\]]|$)/g + row = 0 + misspellings = [] + for line in text.split('\n') + while matches = wordRegex.exec(line) + word = matches[1] + continue unless $native.isMisspelled(word) + startColumn = matches.index + matches[0].length - word.length + endColumn = startColumn + word.length + misspellings.push([[row, startColumn], [row, endColumn]]) + row++ + callTaskMethod('misspellingsFound', misspellings) diff --git a/src/packages/spell-check/lib/spell-check-task.coffee b/src/packages/spell-check/lib/spell-check-task.coffee new file mode 100644 index 000000000..cdec62032 --- /dev/null +++ b/src/packages/spell-check/lib/spell-check-task.coffee @@ -0,0 +1,13 @@ +Task = require 'task' + +module.exports = +class SpellCheckTask extends Task + constructor: (@text, @callback) -> + super('spell-check/lib/spell-check-handler') + + started: -> + @callWorkerMethod('findMisspellings', @text) + + misspellingsFound: (misspellings) -> + @done() + @callback(misspellings) diff --git a/src/packages/spell-check/lib/spell-check-view.coffee b/src/packages/spell-check/lib/spell-check-view.coffee new file mode 100644 index 000000000..2117dc3ef --- /dev/null +++ b/src/packages/spell-check/lib/spell-check-view.coffee @@ -0,0 +1,53 @@ +{View} = require 'space-pen' +_ = require 'underscore' +SpellCheckTask = require './spell-check-task' +MisspellingView = require './misspelling-view' + +module.exports = +class SpellCheckView extends View + @content: -> + @div class: 'spell-check' + + views: [] + + initialize: (@editor) -> + @subscribe @editor, 'editor:path-changed', => @subscribeToBuffer() + @subscribe @editor, 'editor:grammar-changed', => @subscribeToBuffer() + @observeConfig 'editor.fontSize', => @subscribeToBuffer() + @observeConfig 'spell-check.grammars', => @subscribeToBuffer() + + @subscribeToBuffer() + + subscribeToBuffer: -> + @destroyViews() + @task?.abort() + return unless @spellCheckCurrentGrammar() + + @buffer?.off '.spell-check' + @buffer = @editor.getBuffer() + @buffer.on 'contents-modified.spell-check', => @updateMisspellings() + @updateMisspellings() + + spellCheckCurrentGrammar: -> + grammar = @editor.getGrammar().scopeName + _.contains config.get('spell-check.grammars'), grammar + + destroyViews: -> + if @views + view.destroy() for view in @views + @views = [] + + addViews: (misspellings) -> + for misspelling in misspellings + view = new MisspellingView(misspelling, @editor) + @views.push(view) + @append(view) + + updateMisspellings: -> + @task?.abort() + + callback = (misspellings) => + @destroyViews() + @addViews(misspellings) + @task = new SpellCheckTask(@buffer.getText(), callback) + @task.start() diff --git a/src/packages/spell-check/lib/spell-check.coffee b/src/packages/spell-check/lib/spell-check.coffee new file mode 100644 index 000000000..2465b22df --- /dev/null +++ b/src/packages/spell-check/lib/spell-check.coffee @@ -0,0 +1,20 @@ +SpellCheckView = require './spell-check-view' + +module.exports = + configDefaults: + grammars: [ + 'text.plain' + 'source.gfm' + 'text.git-commit' + ] + + activate: -> + if syntax.grammars.length > 1 + @subscribeToEditors() + else + syntax.on 'grammars-loaded', => @subscribeToEditors() + + subscribeToEditors: -> + rootView.eachEditor (editor) -> + if editor.attached and not editor.mini + editor.underlayer.append(new SpellCheckView(editor)) diff --git a/src/packages/spell-check/package.cson b/src/packages/spell-check/package.cson new file mode 100644 index 000000000..1405d4cd7 --- /dev/null +++ b/src/packages/spell-check/package.cson @@ -0,0 +1 @@ +'main': 'lib/spell-check.coffee' diff --git a/src/packages/spell-check/spec/spell-check-spec.coffee b/src/packages/spell-check/spec/spell-check-spec.coffee new file mode 100644 index 000000000..abdb98ac3 --- /dev/null +++ b/src/packages/spell-check/spec/spell-check-spec.coffee @@ -0,0 +1,98 @@ +RootView = require 'root-view' + +describe "Spell check", -> + [editor] = [] + + beforeEach -> + window.rootView = new RootView + rootView.open('sample.js') + config.set('spell-check.grammars', []) + window.loadPackage('spell-check') + rootView.attachToDom() + editor = rootView.getActiveEditor() + + it "decorates all misspelled words", -> + editor.setText("This middle of thiss sentencts has issues.") + config.set('spell-check.grammars', ['source.js']) + + waitsFor -> + editor.find('.misspelling').length > 0 + + runs -> + expect(editor.find('.misspelling').length).toBe 2 + + typo1StartPosition = editor.pixelPositionForBufferPosition([0, 15]) + typo1EndPosition = editor.pixelPositionForBufferPosition([0, 20]) + expect(editor.find('.misspelling:eq(0)').position()).toEqual typo1StartPosition + expect(editor.find('.misspelling:eq(0)').width()).toBe typo1EndPosition.left - typo1StartPosition.left + + typo2StartPosition = editor.pixelPositionForBufferPosition([0, 21]) + typo2EndPosition = editor.pixelPositionForBufferPosition([0, 30]) + expect(editor.find('.misspelling:eq(1)').position()).toEqual typo2StartPosition + expect(editor.find('.misspelling:eq(1)').width()).toBe typo2EndPosition.left - typo2StartPosition.left + + it "hides decorations when a misspelled word is edited", -> + editor.setText('notaword') + advanceClock(editor.getBuffer().stoppedChangingDelay) + config.set('spell-check.grammars', ['source.js']) + + waitsFor -> + editor.find('.misspelling').length > 0 + + runs -> + expect(editor.find('.misspelling').length).toBe 1 + editor.moveCursorToEndOfLine() + editor.insertText('a') + advanceClock(editor.getBuffer().stoppedChangingDelay) + expect(editor.find('.misspelling')).toBeHidden() + + describe "when spell checking for a grammar is removed", -> + it "removes all current decorations", -> + editor.setText('notaword') + advanceClock(editor.getBuffer().stoppedChangingDelay) + config.set('spell-check.grammars', ['source.js']) + + waitsFor -> + editor.find('.misspelling').length > 0 + + runs -> + 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" diff --git a/src/packages/spell-check/stylesheets/spell-check.css b/src/packages/spell-check/stylesheets/spell-check.css new file mode 100644 index 000000000..dc96f515e --- /dev/null +++ b/src/packages/spell-check/stylesheets/spell-check.css @@ -0,0 +1,4 @@ +.misspelling { + border-bottom: 1px dashed rgba(250, 128, 114, .5); + position: absolute; +} diff --git a/src/stdlib/git-repository.coffee b/src/stdlib/git-repository.coffee index 8b6ef83d5..7a52403b4 100644 --- a/src/stdlib/git-repository.coffee +++ b/src/stdlib/git-repository.coffee @@ -1,11 +1,11 @@ module.exports = class GitRepository - constructor: (path) -> + @open: (path) -> unless repo = $git.getRepository(path) throw new Error("No Git repository found searching path: #{path}") repo.constructor = GitRepository repo.__proto__ = GitRepository.prototype - return repo + repo getHead: $git.getHead getPath: $git.getPath diff --git a/src/stdlib/onig-reg-exp.coffee b/src/stdlib/onig-reg-exp.coffee index 93883da20..db7fa4632 100644 --- a/src/stdlib/onig-reg-exp.coffee +++ b/src/stdlib/onig-reg-exp.coffee @@ -1,11 +1,11 @@ module.exports = class OnigRegExp - constructor: (source) -> + @create: (source) -> regexp = $onigRegExp.buildOnigRegExp(source); regexp.constructor = OnigRegExp regexp.__proto__ = OnigRegExp.prototype regexp.source = source - return regexp + regexp search: $onigRegExp.search test: $onigRegExp.test diff --git a/src/stdlib/onig-scanner.coffee b/src/stdlib/onig-scanner.coffee index fb26939ec..80c457c2f 100644 --- a/src/stdlib/onig-scanner.coffee +++ b/src/stdlib/onig-scanner.coffee @@ -1,10 +1,10 @@ module.exports = class OnigScanner - constructor: (sources) -> + @create: (sources) -> scanner = $onigScanner.buildScanner(sources) scanner.constructor = OnigScanner scanner.__proto__ = OnigScanner.prototype scanner.sources = sources - return scanner + scanner findNextMatch: $onigScanner.findNextMatch