From 1745b4be6b049d261ee183a4c4267a078a724864 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Tue, 19 Sep 2017 13:42:10 -0500 Subject: [PATCH 01/81] change activeItemPath if item's path changes --- src/pane-element.coffee | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pane-element.coffee b/src/pane-element.coffee index c4866816a..a1a760348 100644 --- a/src/pane-element.coffee +++ b/src/pane-element.coffee @@ -79,6 +79,7 @@ class PaneElement extends HTMLElement activeItemChanged: (item) -> delete @dataset.activeItemName delete @dataset.activeItemPath + @changePathDisposable?.dispose?() return unless item? @@ -89,6 +90,12 @@ class PaneElement extends HTMLElement @dataset.activeItemName = path.basename(itemPath) @dataset.activeItemPath = itemPath + if item.onDidChangePath? + @changePathDisposable = item.onDidChangePath => + itemPath = item.getPath() + @dataset.activeItemName = path.basename(itemPath) + @dataset.activeItemPath = itemPath + unless @itemViews.contains(itemView) @itemViews.appendChild(itemView) From 8b67b7037c92b90eb3e99d99f76dc8ed80774d5a Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Tue, 19 Sep 2017 14:07:34 -0500 Subject: [PATCH 02/81] add specs --- spec/pane-element-spec.coffee | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/spec/pane-element-spec.coffee b/spec/pane-element-spec.coffee index ff7634734..86db4ffe7 100644 --- a/spec/pane-element-spec.coffee +++ b/spec/pane-element-spec.coffee @@ -113,6 +113,49 @@ describe "PaneElement", -> expect(paneElement.dataset.activeItemPath).toBeUndefined() expect(paneElement.dataset.activeItemName).toBeUndefined() + describe "when the path of the item changes", -> + [item1, item2] = [] + + beforeEach -> + item1 = document.createElement('div') + item1.path = '/foo/bar.txt' + item1.changePathCallbacks = [] + item1.setPath = (path) -> + this.path = path + callback() for callback in changePathCallbacks + item1.getPath = -> this.path + item1.onDidChangePath = (callback) -> this.changePathCallbacks.push(callback) + + item2 = document.createElement('div') + + pane.addItem(item1) + pane.addItem(item2) + + it "changes the file path and file name data attributes on the pane if the active item path is changed", -> + + expect(paneElement.dataset.activeItemPath).toBe '/foo/bar.txt' + expect(paneElement.dataset.activeItemName).toBe 'bar.txt' + + item1.setPath "/foo/bar1.txt" + + expect(paneElement.dataset.activeItemPath).toBe '/foo/bar1.txt' + expect(paneElement.dataset.activeItemName).toBe 'bar1.txt' + + pane.activateItem(item2) + + expect(paneElement.dataset.activeItemPath).toBeUndefined() + expect(paneElement.dataset.activeItemName).toBeUndefined() + + item1.setPath "/foo/bar2.txt" + + expect(paneElement.dataset.activeItemPath).toBeUndefined() + expect(paneElement.dataset.activeItemName).toBeUndefined() + + pane.activateItem(item1) + + expect(paneElement.dataset.activeItemPath).toBe '/foo/bar2.txt' + expect(paneElement.dataset.activeItemName).toBe 'bar2.txt' + describe "when an item is removed from the pane", -> describe "when the destroyed item is an element", -> it "removes the item from the itemViews div", -> From 90503f6d4a04d51ed744e53b99083e15ee9621eb Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Tue, 19 Sep 2017 14:11:33 -0500 Subject: [PATCH 03/81] dispose changePathDisposable on destroy --- src/pane-element.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pane-element.coffee b/src/pane-element.coffee index a1a760348..d68b3b834 100644 --- a/src/pane-element.coffee +++ b/src/pane-element.coffee @@ -79,7 +79,7 @@ class PaneElement extends HTMLElement activeItemChanged: (item) -> delete @dataset.activeItemName delete @dataset.activeItemPath - @changePathDisposable?.dispose?() + @changePathDisposable?.dispose() return unless item? @@ -126,6 +126,7 @@ class PaneElement extends HTMLElement paneDestroyed: -> @subscriptions.dispose() + @changePathDisposable?.dispose() flexScaleChanged: (flexScale) -> @style.flexGrow = flexScale From abeebd51ef9a873867d2cf0c6ac148f2209b91d7 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Tue, 19 Sep 2017 14:35:23 -0500 Subject: [PATCH 04/81] add test dispose --- spec/pane-element-spec.coffee | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/spec/pane-element-spec.coffee b/spec/pane-element-spec.coffee index 86db4ffe7..831354936 100644 --- a/spec/pane-element-spec.coffee +++ b/spec/pane-element-spec.coffee @@ -121,10 +121,14 @@ describe "PaneElement", -> item1.path = '/foo/bar.txt' item1.changePathCallbacks = [] item1.setPath = (path) -> - this.path = path + @path = path callback() for callback in changePathCallbacks - item1.getPath = -> this.path - item1.onDidChangePath = (callback) -> this.changePathCallbacks.push(callback) + return + item1.getPath = -> @path + item1.onDidChangePath = (callback) -> + @changePathCallbacks.push(callback) + return dispose: => + @changePathCallbacks = @changePathCallbacks.filter (f) -> f isnt callback item2 = document.createElement('div') From 1e72a7f0e57a1d5c8630a2717a1d0ec916f8f194 Mon Sep 17 00:00:00 2001 From: Tony Brix Date: Tue, 19 Sep 2017 15:06:44 -0500 Subject: [PATCH 05/81] fix tests --- spec/pane-element-spec.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/pane-element-spec.coffee b/spec/pane-element-spec.coffee index 831354936..af34681a6 100644 --- a/spec/pane-element-spec.coffee +++ b/spec/pane-element-spec.coffee @@ -122,11 +122,11 @@ describe "PaneElement", -> item1.changePathCallbacks = [] item1.setPath = (path) -> @path = path - callback() for callback in changePathCallbacks + callback() for callback in @changePathCallbacks return item1.getPath = -> @path item1.onDidChangePath = (callback) -> - @changePathCallbacks.push(callback) + @changePathCallbacks.push callback return dispose: => @changePathCallbacks = @changePathCallbacks.filter (f) -> f isnt callback From 67254766d75a8e69d560665c258acdd27779b284 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Sep 2017 10:21:41 -0700 Subject: [PATCH 06/81] Convert TokenizedBuffer to JS --- src/tokenized-buffer.coffee | 455 ---------------------------- src/tokenized-buffer.js | 586 ++++++++++++++++++++++++++++++++++++ 2 files changed, 586 insertions(+), 455 deletions(-) delete mode 100644 src/tokenized-buffer.coffee create mode 100644 src/tokenized-buffer.js diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee deleted file mode 100644 index e4d954a59..000000000 --- a/src/tokenized-buffer.coffee +++ /dev/null @@ -1,455 +0,0 @@ -_ = require 'underscore-plus' -{CompositeDisposable, Emitter} = require 'event-kit' -{Point, Range} = require 'text-buffer' -Model = require './model' -TokenizedLine = require './tokenized-line' -TokenIterator = require './token-iterator' -ScopeDescriptor = require './scope-descriptor' -TokenizedBufferIterator = require './tokenized-buffer-iterator' -NullGrammar = require './null-grammar' -{toFirstMateScopeId} = require './first-mate-helpers' - -prefixedScopes = new Map() - -module.exports = -class TokenizedBuffer extends Model - grammar: null - buffer: null - tabLength: null - tokenizedLines: null - chunkSize: 50 - invalidRows: null - visible: false - changeCount: 0 - - @deserialize: (state, atomEnvironment) -> - buffer = null - if state.bufferId - buffer = atomEnvironment.project.bufferForIdSync(state.bufferId) - else - # TODO: remove this fallback after everyone transitions to the latest version. - buffer = atomEnvironment.project.bufferForPathSync(state.bufferPath) - return null unless buffer? - - state.buffer = buffer - state.assert = atomEnvironment.assert - new this(state) - - constructor: (params) -> - {grammar, @buffer, @tabLength, @largeFileMode, @assert} = params - - @emitter = new Emitter - @disposables = new CompositeDisposable - @tokenIterator = new TokenIterator(this) - - @disposables.add @buffer.registerTextDecorationLayer(this) - - @setGrammar(grammar ? NullGrammar) - - destroyed: -> - @disposables.dispose() - @tokenizedLines.length = 0 - - buildIterator: -> - new TokenizedBufferIterator(this) - - classNameForScopeId: (id) -> - scope = @grammar.scopeForId(toFirstMateScopeId(id)) - if scope - prefixedScope = prefixedScopes.get(scope) - if prefixedScope - prefixedScope - else - prefixedScope = "syntax--#{scope.replace(/\./g, ' syntax--')}" - prefixedScopes.set(scope, prefixedScope) - prefixedScope - else - null - - getInvalidatedRanges: -> - [] - - onDidInvalidateRange: (fn) -> - @emitter.on 'did-invalidate-range', fn - - serialize: -> - { - deserializer: 'TokenizedBuffer' - bufferPath: @buffer.getPath() - bufferId: @buffer.getId() - tabLength: @tabLength - largeFileMode: @largeFileMode - } - - observeGrammar: (callback) -> - callback(@grammar) - @onDidChangeGrammar(callback) - - onDidChangeGrammar: (callback) -> - @emitter.on 'did-change-grammar', callback - - onDidTokenize: (callback) -> - @emitter.on 'did-tokenize', callback - - setGrammar: (grammar) -> - return unless grammar? and grammar isnt @grammar - - @grammar = grammar - @rootScopeDescriptor = new ScopeDescriptor(scopes: [@grammar.scopeName]) - - @grammarUpdateDisposable?.dispose() - @grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines() - @disposables.add(@grammarUpdateDisposable) - - @retokenizeLines() - - @emitter.emit 'did-change-grammar', grammar - - getGrammarSelectionContent: -> - @buffer.getTextInRange([[0, 0], [10, 0]]) - - hasTokenForSelector: (selector) -> - for tokenizedLine in @tokenizedLines when tokenizedLine? - for token in tokenizedLine.tokens - return true if selector.matches(token.scopes) - false - - retokenizeLines: -> - return unless @alive - @fullyTokenized = false - @tokenizedLines = new Array(@buffer.getLineCount()) - @invalidRows = [] - if @largeFileMode or @grammar.name is 'Null Grammar' - @markTokenizationComplete() - else - @invalidateRow(0) - - setVisible: (@visible) -> - if @visible and @grammar.name isnt 'Null Grammar' and not @largeFileMode - @tokenizeInBackground() - - getTabLength: -> @tabLength - - setTabLength: (@tabLength) -> - - tokenizeInBackground: -> - return if not @visible or @pendingChunk or not @isAlive() - - @pendingChunk = true - _.defer => - @pendingChunk = false - @tokenizeNextChunk() if @isAlive() and @buffer.isAlive() - - tokenizeNextChunk: -> - rowsRemaining = @chunkSize - - while @firstInvalidRow()? and rowsRemaining > 0 - startRow = @invalidRows.shift() - lastRow = @getLastRow() - continue if startRow > lastRow - - row = startRow - loop - previousStack = @stackForRow(row) - @tokenizedLines[row] = @buildTokenizedLineForRow(row, @stackForRow(row - 1), @openScopesForRow(row)) - if --rowsRemaining is 0 - filledRegion = false - endRow = row - break - if row is lastRow or _.isEqual(@stackForRow(row), previousStack) - filledRegion = true - endRow = row - break - row++ - - @validateRow(endRow) - @invalidateRow(endRow + 1) unless filledRegion - - @emitter.emit 'did-invalidate-range', Range(Point(startRow, 0), Point(endRow + 1, 0)) - - if @firstInvalidRow()? - @tokenizeInBackground() - else - @markTokenizationComplete() - - markTokenizationComplete: -> - unless @fullyTokenized - @emitter.emit 'did-tokenize' - @fullyTokenized = true - - firstInvalidRow: -> - @invalidRows[0] - - validateRow: (row) -> - @invalidRows.shift() while @invalidRows[0] <= row - return - - invalidateRow: (row) -> - @invalidRows.push(row) - @invalidRows.sort (a, b) -> a - b - @tokenizeInBackground() - - updateInvalidRows: (start, end, delta) -> - @invalidRows = @invalidRows.map (row) -> - if row < start - row - else if start <= row <= end - end + delta + 1 - else if row > end - row + delta - - bufferDidChange: (e) -> - @changeCount = @buffer.changeCount - - {oldRange, newRange} = e - start = oldRange.start.row - end = oldRange.end.row - delta = newRange.end.row - oldRange.end.row - oldLineCount = oldRange.end.row - oldRange.start.row + 1 - newLineCount = newRange.end.row - newRange.start.row + 1 - - @updateInvalidRows(start, end, delta) - previousEndStack = @stackForRow(end) # used in spill detection below - if @largeFileMode or @grammar.name is 'Null Grammar' - _.spliceWithArray(@tokenizedLines, start, oldLineCount, new Array(newLineCount)) - else - newTokenizedLines = @buildTokenizedLinesForRows(start, end + delta, @stackForRow(start - 1), @openScopesForRow(start)) - _.spliceWithArray(@tokenizedLines, start, oldLineCount, newTokenizedLines) - newEndStack = @stackForRow(end + delta) - if newEndStack and not _.isEqual(newEndStack, previousEndStack) - @invalidateRow(end + delta + 1) - - isFoldableAtRow: (row) -> - @isFoldableCodeAtRow(row) or @isFoldableCommentAtRow(row) - - # Returns a {Boolean} indicating whether the given buffer row starts - # a a foldable row range due to the code's indentation patterns. - isFoldableCodeAtRow: (row) -> - if 0 <= row <= @buffer.getLastRow() - nextRow = @buffer.nextNonBlankRow(row) - tokenizedLine = @tokenizedLines[row] - if @buffer.isRowBlank(row) or tokenizedLine?.isComment() or not nextRow? - false - else - @indentLevelForRow(nextRow) > @indentLevelForRow(row) - else - false - - isFoldableCommentAtRow: (row) -> - previousRow = row - 1 - nextRow = row + 1 - if nextRow > @buffer.getLastRow() - false - else - Boolean( - not (@tokenizedLines[previousRow]?.isComment()) and - @tokenizedLines[row]?.isComment() and - @tokenizedLines[nextRow]?.isComment() - ) - - buildTokenizedLinesForRows: (startRow, endRow, startingStack, startingopenScopes) -> - ruleStack = startingStack - openScopes = startingopenScopes - stopTokenizingAt = startRow + @chunkSize - tokenizedLines = for row in [startRow..endRow] by 1 - if (ruleStack or row is 0) and row < stopTokenizingAt - tokenizedLine = @buildTokenizedLineForRow(row, ruleStack, openScopes) - ruleStack = tokenizedLine.ruleStack - openScopes = @scopesFromTags(openScopes, tokenizedLine.tags) - else - tokenizedLine = undefined - tokenizedLine - - if endRow >= stopTokenizingAt - @invalidateRow(stopTokenizingAt) - @tokenizeInBackground() - - tokenizedLines - - buildTokenizedLineForRow: (row, ruleStack, openScopes) -> - @buildTokenizedLineForRowWithText(row, @buffer.lineForRow(row), ruleStack, openScopes) - - buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) -> - lineEnding = @buffer.lineEndingForRow(row) - {tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false) - new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator, @grammar}) - - tokenizedLineForRow: (bufferRow) -> - if 0 <= bufferRow <= @buffer.getLastRow() - if tokenizedLine = @tokenizedLines[bufferRow] - tokenizedLine - else - text = @buffer.lineForRow(bufferRow) - lineEnding = @buffer.lineEndingForRow(bufferRow) - tags = [@grammar.startIdForScope(@grammar.scopeName), text.length, @grammar.endIdForScope(@grammar.scopeName)] - @tokenizedLines[bufferRow] = new TokenizedLine({openScopes: [], text, tags, lineEnding, @tokenIterator, @grammar}) - - tokenizedLinesForRows: (startRow, endRow) -> - for row in [startRow..endRow] by 1 - @tokenizedLineForRow(row) - - stackForRow: (bufferRow) -> - @tokenizedLines[bufferRow]?.ruleStack - - openScopesForRow: (bufferRow) -> - if precedingLine = @tokenizedLines[bufferRow - 1] - @scopesFromTags(precedingLine.openScopes, precedingLine.tags) - else - [] - - scopesFromTags: (startingScopes, tags) -> - scopes = startingScopes.slice() - for tag in tags when tag < 0 - if (tag % 2) is -1 - scopes.push(tag) - else - matchingStartTag = tag + 1 - loop - break if scopes.pop() is matchingStartTag - if scopes.length is 0 - @assert false, "Encountered an unmatched scope end tag.", (error) => - error.metadata = { - grammarScopeName: @grammar.scopeName - unmatchedEndTag: @grammar.scopeForId(tag) - } - path = require 'path' - error.privateMetadataDescription = "The contents of `#{path.basename(@buffer.getPath())}`" - error.privateMetadata = { - filePath: @buffer.getPath() - fileContents: @buffer.getText() - } - break - scopes - - indentLevelForRow: (bufferRow) -> - line = @buffer.lineForRow(bufferRow) - indentLevel = 0 - - if line is '' - nextRow = bufferRow + 1 - lineCount = @getLineCount() - while nextRow < lineCount - nextLine = @buffer.lineForRow(nextRow) - unless nextLine is '' - indentLevel = Math.ceil(@indentLevelForLine(nextLine)) - break - nextRow++ - - previousRow = bufferRow - 1 - while previousRow >= 0 - previousLine = @buffer.lineForRow(previousRow) - unless previousLine is '' - indentLevel = Math.max(Math.ceil(@indentLevelForLine(previousLine)), indentLevel) - break - previousRow-- - - indentLevel - else - @indentLevelForLine(line) - - indentLevelForLine: (line) -> - indentLength = 0 - for char in line - if char is '\t' - indentLength += @getTabLength() - (indentLength % @getTabLength()) - else if char is ' ' - indentLength++ - else - break - - indentLength / @getTabLength() - - scopeDescriptorForPosition: (position) -> - {row, column} = @buffer.clipPosition(Point.fromObject(position)) - - iterator = @tokenizedLineForRow(row).getTokenIterator() - while iterator.next() - if iterator.getBufferEnd() > column - scopes = iterator.getScopes() - break - - # rebuild scope of last token if we iterated off the end - unless scopes? - scopes = iterator.getScopes() - scopes.push(iterator.getScopeEnds().reverse()...) - - new ScopeDescriptor({scopes}) - - tokenForPosition: (position) -> - {row, column} = Point.fromObject(position) - @tokenizedLineForRow(row).tokenAtBufferColumn(column) - - tokenStartPositionForPosition: (position) -> - {row, column} = Point.fromObject(position) - column = @tokenizedLineForRow(row).tokenStartColumnForBufferColumn(column) - new Point(row, column) - - bufferRangeForScopeAtPosition: (selector, position) -> - position = Point.fromObject(position) - - {openScopes, tags} = @tokenizedLineForRow(position.row) - scopes = openScopes.map (tag) => @grammar.scopeForId(tag) - - startColumn = 0 - for tag, tokenIndex in tags - if tag < 0 - if tag % 2 is -1 - scopes.push(@grammar.scopeForId(tag)) - else - scopes.pop() - else - endColumn = startColumn + tag - if endColumn >= position.column - break - else - startColumn = endColumn - - - return unless selectorMatchesAnyScope(selector, scopes) - - startScopes = scopes.slice() - for startTokenIndex in [(tokenIndex - 1)..0] by -1 - tag = tags[startTokenIndex] - if tag < 0 - if tag % 2 is -1 - startScopes.pop() - else - startScopes.push(@grammar.scopeForId(tag)) - else - break unless selectorMatchesAnyScope(selector, startScopes) - startColumn -= tag - - endScopes = scopes.slice() - for endTokenIndex in [(tokenIndex + 1)...tags.length] by 1 - tag = tags[endTokenIndex] - if tag < 0 - if tag % 2 is -1 - endScopes.push(@grammar.scopeForId(tag)) - else - endScopes.pop() - else - break unless selectorMatchesAnyScope(selector, endScopes) - endColumn += tag - - new Range(new Point(position.row, startColumn), new Point(position.row, endColumn)) - - # Gets the row number of the last line. - # - # Returns a {Number}. - getLastRow: -> - @buffer.getLastRow() - - getLineCount: -> - @buffer.getLineCount() - - logLines: (start=0, end=@buffer.getLastRow()) -> - for row in [start..end] - line = @tokenizedLines[row].text - console.log row, line, line.length - return - -selectorMatchesAnyScope = (selector, scopes) -> - targetClasses = selector.replace(/^\./, '').split('.') - _.any scopes, (scope) -> - scopeClasses = scope.split('.') - _.isSubset(targetClasses, scopeClasses) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js new file mode 100644 index 000000000..80601d1f3 --- /dev/null +++ b/src/tokenized-buffer.js @@ -0,0 +1,586 @@ +const _ = require('underscore-plus') +const {CompositeDisposable, Emitter} = require('event-kit') +const {Point, Range} = require('text-buffer') +const Model = require('./model') +const TokenizedLine = require('./tokenized-line') +const TokenIterator = require('./token-iterator') +const ScopeDescriptor = require('./scope-descriptor') +const TokenizedBufferIterator = require('./tokenized-buffer-iterator') +const NullGrammar = require('./null-grammar') +const {toFirstMateScopeId} = require('./first-mate-helpers') + +let nextId = 0 +const prefixedScopes = new Map() + +module.exports = +class TokenizedBuffer { + static deserialize (state, atomEnvironment) { + const buffer = atomEnvironment.project.bufferForIdSync(state.bufferId) + if (!buffer) return null + + state.buffer = buffer + state.assert = atomEnvironment.assert + return new TokenizedBuffer(state) + } + + constructor (params) { + this.emitter = new Emitter() + this.disposables = new CompositeDisposable() + this.tokenIterator = new TokenIterator(this) + + this.alive = true + this.id = params.id != null ? params.id : nextId++ + this.buffer = params.buffer + this.tabLength = params.tabLength + this.largeFileMode = params.largeFileMode + this.assert = params.assert + + this.setGrammar(params.grammar || NullGrammar) + this.disposables.add(this.buffer.registerTextDecorationLayer(this)) + } + + destroy () { + if (!this.alive) return + this.alive = false + this.disposables.dispose() + this.tokenizedLines.length = 0 + } + + isAlive () { + return this.alive + } + + isDestroyed () { + return !this.alive + } + + buildIterator () { + return new TokenizedBufferIterator(this) + } + + classNameForScopeId (id) { + const scope = this.grammar.scopeForId(toFirstMateScopeId(id)) + if (scope) { + let prefixedScope = prefixedScopes.get(scope) + if (prefixedScope) { + return prefixedScope + } else { + prefixedScope = `syntax--${scope.replace(/\./g, ' syntax--')}` + prefixedScopes.set(scope, prefixedScope) + return prefixedScope + } + } else { + return null + } + } + + getInvalidatedRanges () { + return [] + } + + onDidInvalidateRange (fn) { + return this.emitter.on('did-invalidate-range', fn) + } + + serialize () { + return { + deserializer: 'TokenizedBuffer', + bufferPath: this.buffer.getPath(), + bufferId: this.buffer.getId(), + tabLength: this.tabLength, + largeFileMode: this.largeFileMode + } + } + + observeGrammar (callback) { + callback(this.grammar) + return this.onDidChangeGrammar(callback) + } + + onDidChangeGrammar (callback) { + return this.emitter.on('did-change-grammar', callback) + } + + onDidTokenize (callback) { + return this.emitter.on('did-tokenize', callback) + } + + setGrammar (grammar) { + if (!grammar || grammar === this.grammar) return + + this.grammar = grammar + this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.scopeName]}) + + if (this.grammarUpdateDisposable) this.grammarUpdateDisposable.dispose() + this.grammarUpdateDisposable = this.grammar.onDidUpdate(() => this.retokenizeLines()) + this.disposables.add(this.grammarUpdateDisposable) + + this.retokenizeLines() + this.emitter.emit('did-change-grammar', grammar) + } + + getGrammarSelectionContent () { + return this.buffer.getTextInRange([[0, 0], [10, 0]]) + } + + hasTokenForSelector (selector) { + for (const tokenizedLine of this.tokenizedLines) { + if (tokenizedLine) { + for (let token of tokenizedLine.tokens) { + if (selector.matches(token.scopes)) return true + } + } + } + return false + } + + retokenizeLines () { + if (!this.alive) return + this.fullyTokenized = false + this.tokenizedLines = new Array(this.buffer.getLineCount()) + this.invalidRows = [] + if (this.largeFileMode || this.grammar.name === 'Null Grammar') { + this.markTokenizationComplete() + } else { + this.invalidateRow(0) + } + } + + setVisible (visible) { + this.visible = visible + if (this.visible && this.grammar.name !== 'Null Grammar' && !this.largeFileMode) { + this.tokenizeInBackground() + } + } + + getTabLength () { return this.tabLength } + + setTabLength (tabLength) { + this.tabLength = tabLength + } + + tokenizeInBackground () { + if (!this.visible || this.pendingChunk || !this.alive) return + + this.pendingChunk = true + _.defer(() => { + this.pendingChunk = false + if (this.isAlive() && this.buffer.isAlive()) this.tokenizeNextChunk() + }) + } + + tokenizeNextChunk () { + let rowsRemaining = this.chunkSize + + while (this.firstInvalidRow() != null && rowsRemaining > 0) { + var endRow, filledRegion + const startRow = this.invalidRows.shift() + const lastRow = this.getLastRow() + if (startRow > lastRow) continue + + let row = startRow + while (true) { + const previousStack = this.stackForRow(row) + this.tokenizedLines[row] = this.buildTokenizedLineForRow(row, this.stackForRow(row - 1), this.openScopesForRow(row)) + if (--rowsRemaining === 0) { + filledRegion = false + endRow = row + break + } + if (row === lastRow || _.isEqual(this.stackForRow(row), previousStack)) { + filledRegion = true + endRow = row + break + } + row++ + } + + this.validateRow(endRow) + if (!filledRegion) this.invalidateRow(endRow + 1) + + this.emitter.emit('did-invalidate-range', Range(Point(startRow, 0), Point(endRow + 1, 0))) + } + + if (this.firstInvalidRow() != null) { + this.tokenizeInBackground() + } else { + this.markTokenizationComplete() + } + } + + markTokenizationComplete () { + if (!this.fullyTokenized) { + this.emitter.emit('did-tokenize') + } + this.fullyTokenized = true + } + + firstInvalidRow () { + return this.invalidRows[0] + } + + validateRow (row) { + while (this.invalidRows[0] <= row) this.invalidRows.shift() + } + + invalidateRow (row) { + this.invalidRows.push(row) + this.invalidRows.sort((a, b) => a - b) + this.tokenizeInBackground() + } + + updateInvalidRows (start, end, delta) { + this.invalidRows = this.invalidRows.map((row) => { + if (row < start) { + return row + } else if (start <= row && row <= end) { + return end + delta + 1 + } else if (row > end) { + return row + delta + } + }) + } + + bufferDidChange (e) { + this.changeCount = this.buffer.changeCount + + const {oldRange, newRange} = e + const start = oldRange.start.row + const end = oldRange.end.row + const delta = newRange.end.row - oldRange.end.row + const oldLineCount = (oldRange.end.row - oldRange.start.row) + 1 + const newLineCount = (newRange.end.row - newRange.start.row) + 1 + + this.updateInvalidRows(start, end, delta) + const previousEndStack = this.stackForRow(end) // used in spill detection below + if (this.largeFileMode || (this.grammar.name === 'Null Grammar')) { + _.spliceWithArray(this.tokenizedLines, start, oldLineCount, new Array(newLineCount)) + } else { + const newTokenizedLines = this.buildTokenizedLinesForRows(start, end + delta, this.stackForRow(start - 1), this.openScopesForRow(start)) + _.spliceWithArray(this.tokenizedLines, start, oldLineCount, newTokenizedLines) + const newEndStack = this.stackForRow(end + delta) + if (newEndStack && !_.isEqual(newEndStack, previousEndStack)) { + this.invalidateRow(end + delta + 1) + } + } + } + + isFoldableAtRow (row) { + return this.isFoldableCodeAtRow(row) || this.isFoldableCommentAtRow(row) + } + + // Returns a {Boolean} indicating whether the given buffer row starts + // a a foldable row range due to the code's indentation patterns. + isFoldableCodeAtRow (row) { + if (row >= 0 && row <= this.buffer.getLastRow()) { + const nextRow = this.buffer.nextNonBlankRow(row) + const tokenizedLine = this.tokenizedLines[row] + if (this.buffer.isRowBlank(row) || (tokenizedLine != null ? tokenizedLine.isComment() : undefined) || (nextRow == null)) { + return false + } else { + return this.indentLevelForRow(nextRow) > this.indentLevelForRow(row) + } + } else { + return false + } + } + + isFoldableCommentAtRow (row) { + const previousRow = row - 1 + const nextRow = row + 1 + if (nextRow > this.buffer.getLastRow()) { + return false + } else { + return Boolean( + !(this.tokenizedLines[previousRow] != null ? this.tokenizedLines[previousRow].isComment() : undefined) && + (this.tokenizedLines[row] != null ? this.tokenizedLines[row].isComment() : undefined) && + (this.tokenizedLines[nextRow] != null ? this.tokenizedLines[nextRow].isComment() : undefined) + ) + } + } + + buildTokenizedLinesForRows (startRow, endRow, startingStack, startingopenScopes) { + let ruleStack = startingStack + let openScopes = startingopenScopes + const stopTokenizingAt = startRow + this.chunkSize + const tokenizedLines = [] + for (let row = startRow, end = endRow; row <= end; row++) { + let tokenizedLine + if ((ruleStack || (row === 0)) && row < stopTokenizingAt) { + tokenizedLine = this.buildTokenizedLineForRow(row, ruleStack, openScopes) + ruleStack = tokenizedLine.ruleStack + openScopes = this.scopesFromTags(openScopes, tokenizedLine.tags) + } + tokenizedLines.push(tokenizedLine) + } + + if (endRow >= stopTokenizingAt) { + this.invalidateRow(stopTokenizingAt) + this.tokenizeInBackground() + } + + return tokenizedLines + } + + buildTokenizedLineForRow (row, ruleStack, openScopes) { + return this.buildTokenizedLineForRowWithText(row, this.buffer.lineForRow(row), ruleStack, openScopes) + } + + buildTokenizedLineForRowWithText (row, text, currentRuleStack = this.stackForRow(row - 1), openScopes = this.openScopesForRow(row)) { + const lineEnding = this.buffer.lineEndingForRow(row); + const {tags, ruleStack} = this.grammar.tokenizeLine(text, currentRuleStack, row === 0, false) + return new TokenizedLine({ + openScopes, + text, + tags, + ruleStack, + lineEnding, + tokenIterator: this.tokenIterator, + grammar: this.grammar + }) + } + + tokenizedLineForRow (bufferRow) { + if (bufferRow >= 0 && bufferRow <= this.buffer.getLastRow()) { + const tokenizedLine = this.tokenizedLines[bufferRow] + if (tokenizedLine) { + return tokenizedLine + } else { + const text = this.buffer.lineForRow(bufferRow) + const lineEnding = this.buffer.lineEndingForRow(bufferRow) + const tags = [ + this.grammar.startIdForScope(this.grammar.scopeName), + text.length, + this.grammar.endIdForScope(this.grammar.scopeName) + ] + return this.tokenizedLines[bufferRow] = new TokenizedLine({ + openScopes: [], + text, + tags, + lineEnding, + tokenIterator: this.tokenIterator, + grammar: this.grammar + }) + } + } + } + + tokenizedLinesForRows (startRow, endRow) { + const result = [] + for (let row = startRow, end = endRow; row <= end; row++) { + result.push(this.tokenizedLineForRow(row)) + } + return result + } + + stackForRow (bufferRow) { + return this.tokenizedLines[bufferRow] && this.tokenizedLines[bufferRow].ruleStack + } + + openScopesForRow (bufferRow) { + const precedingLine = this.tokenizedLines[bufferRow - 1] + if (precedingLine) { + return this.scopesFromTags(precedingLine.openScopes, precedingLine.tags) + } else { + return [] + } + } + + scopesFromTags (startingScopes, tags) { + const scopes = startingScopes.slice() + for (const tag of tags) { + if (tag < 0) { + if (tag % 2 === -1) { + scopes.push(tag) + } else { + const matchingStartTag = tag + 1 + while (true) { + if (scopes.pop() === matchingStartTag) break + if (scopes.length === 0) { + this.assert(false, 'Encountered an unmatched scope end tag.', error => { + error.metadata = { + grammarScopeName: this.grammar.scopeName, + unmatchedEndTag: this.grammar.scopeForId(tag) + } + const path = require('path') + error.privateMetadataDescription = `The contents of \`${path.basename(this.buffer.getPath())}\`` + return error.privateMetadata = { + filePath: this.buffer.getPath(), + fileContents: this.buffer.getText() + } + }) + break + } + } + } + } + } + return scopes + } + + indentLevelForRow (bufferRow) { + const line = this.buffer.lineForRow(bufferRow) + let indentLevel = 0 + + if (line === '') { + let nextRow = bufferRow + 1 + const lineCount = this.getLineCount() + while (nextRow < lineCount) { + const nextLine = this.buffer.lineForRow(nextRow) + if (nextLine !== '') { + indentLevel = Math.ceil(this.indentLevelForLine(nextLine)) + break + } + nextRow++ + } + + let previousRow = bufferRow - 1 + while (previousRow >= 0) { + const previousLine = this.buffer.lineForRow(previousRow) + if (previousLine !== '') { + indentLevel = Math.max(Math.ceil(this.indentLevelForLine(previousLine)), indentLevel) + break + } + previousRow-- + } + + return indentLevel + } else { + return this.indentLevelForLine(line) + } + } + + indentLevelForLine (line, tabLength = this.tabLength) { + let indentLength = 0 + for (let i = 0, {length} = line; i < length; i++) { + const char = line[i] + if (char === '\t') { + indentLength += tabLength - (indentLength % tabLength) + } else if (char === ' ') { + indentLength++ + } else { + break + } + } + return indentLength / tabLength + } + + scopeDescriptorForPosition (position) { + let scopes + const {row, column} = this.buffer.clipPosition(Point.fromObject(position)) + + const iterator = this.tokenizedLineForRow(row).getTokenIterator() + while (iterator.next()) { + if (iterator.getBufferEnd() > column) { + scopes = iterator.getScopes() + break + } + } + + // rebuild scope of last token if we iterated off the end + if (!scopes) { + scopes = iterator.getScopes() + scopes.push(...iterator.getScopeEnds().reverse()) + } + + return new ScopeDescriptor({scopes}) + } + + tokenForPosition (position) { + const {row, column} = Point.fromObject(position) + return this.tokenizedLineForRow(row).tokenAtBufferColumn(column) + } + + tokenStartPositionForPosition (position) { + let {row, column} = Point.fromObject(position) + column = this.tokenizedLineForRow(row).tokenStartColumnForBufferColumn(column) + return new Point(row, column) + } + + bufferRangeForScopeAtPosition (selector, position) { + let endColumn, tag, tokenIndex + position = Point.fromObject(position) + + const {openScopes, tags} = this.tokenizedLineForRow(position.row) + const scopes = openScopes.map(tag => this.grammar.scopeForId(tag)) + + let startColumn = 0 + for (tokenIndex = 0; tokenIndex < tags.length; tokenIndex++) { + tag = tags[tokenIndex] + if (tag < 0) { + if ((tag % 2) === -1) { + scopes.push(this.grammar.scopeForId(tag)) + } else { + scopes.pop() + } + } else { + endColumn = startColumn + tag + if (endColumn >= position.column) { + break + } else { + startColumn = endColumn + } + } + } + + if (!selectorMatchesAnyScope(selector, scopes)) return + + const startScopes = scopes.slice() + for (let startTokenIndex = tokenIndex - 1; startTokenIndex >= 0; startTokenIndex--) { + tag = tags[startTokenIndex] + if (tag < 0) { + if ((tag % 2) === -1) { + startScopes.pop() + } else { + startScopes.push(this.grammar.scopeForId(tag)) + } + } else { + if (!selectorMatchesAnyScope(selector, startScopes)) { break } + startColumn -= tag + } + } + + const endScopes = scopes.slice() + for (let endTokenIndex = tokenIndex + 1, end = tags.length; endTokenIndex < end; endTokenIndex++) { + tag = tags[endTokenIndex] + if (tag < 0) { + if ((tag % 2) === -1) { + endScopes.push(this.grammar.scopeForId(tag)) + } else { + endScopes.pop() + } + } else { + if (!selectorMatchesAnyScope(selector, endScopes)) { break } + endColumn += tag + } + } + + return new Range(new Point(position.row, startColumn), new Point(position.row, endColumn)) + } + + // Gets the row number of the last line. + // + // Returns a {Number}. + getLastRow () { + return this.buffer.getLastRow() + } + + getLineCount () { + return this.buffer.getLineCount() + } + + logLines (start = 0, end = this.buffer.getLastRow()) { + for (let row = start; row <= end1; row++) { + const line = this.tokenizedLines[row].text + console.log(row, line, line.length) + } + } +} + +function selectorMatchesAnyScope (selector, scopes) { + const targetClasses = selector.replace(/^\./, '').split('.') + return scopes.some((scope) => { + const scopeClasses = scope.split('.') + return _.isSubset(targetClasses, scopeClasses) + }) +} From 1ca1c545ba43460ff575a838acf3d470e8db02f0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Sep 2017 13:59:04 -0700 Subject: [PATCH 07/81] Convert TokenizedBuffer spec to JS --- spec/tokenized-buffer-spec.coffee | 688 -------------------------- spec/tokenized-buffer-spec.js | 779 ++++++++++++++++++++++++++++++ 2 files changed, 779 insertions(+), 688 deletions(-) delete mode 100644 spec/tokenized-buffer-spec.coffee create mode 100644 spec/tokenized-buffer-spec.js diff --git a/spec/tokenized-buffer-spec.coffee b/spec/tokenized-buffer-spec.coffee deleted file mode 100644 index 2c2379810..000000000 --- a/spec/tokenized-buffer-spec.coffee +++ /dev/null @@ -1,688 +0,0 @@ -NullGrammar = require '../src/null-grammar' -TokenizedBuffer = require '../src/tokenized-buffer' -{Point} = TextBuffer = require 'text-buffer' -_ = require 'underscore-plus' - -describe "TokenizedBuffer", -> - [tokenizedBuffer, buffer] = [] - - beforeEach -> - # enable async tokenization - TokenizedBuffer.prototype.chunkSize = 5 - jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground') - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - afterEach -> - tokenizedBuffer?.destroy() - - startTokenizing = (tokenizedBuffer) -> - tokenizedBuffer.setVisible(true) - - fullyTokenize = (tokenizedBuffer) -> - tokenizedBuffer.setVisible(true) - advanceClock() while tokenizedBuffer.firstInvalidRow()? - - describe "serialization", -> - describe "when the underlying buffer has a path", -> - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - it "deserializes it searching among the buffers in the current project", -> - tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2}) - tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom) - expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer) - - describe "when the underlying buffer has no path", -> - beforeEach -> - buffer = atom.project.bufferForPathSync(null) - - it "deserializes it searching among the buffers in the current project", -> - tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2}) - tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom) - expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer) - - describe "when the buffer is destroyed", -> - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - startTokenizing(tokenizedBuffer) - - it "stops tokenization", -> - tokenizedBuffer.destroy() - spyOn(tokenizedBuffer, 'tokenizeNextChunk') - advanceClock() - expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled() - - describe "when the buffer contains soft-tabs", -> - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - startTokenizing(tokenizedBuffer) - - afterEach -> - tokenizedBuffer.destroy() - buffer.release() - - describe "on construction", -> - it "tokenizes lines chunk at a time in the background", -> - line0 = tokenizedBuffer.tokenizedLines[0] - expect(line0).toBeUndefined() - - line11 = tokenizedBuffer.tokenizedLines[11] - expect(line11).toBeUndefined() - - # tokenize chunk 1 - advanceClock() - expect(tokenizedBuffer.tokenizedLines[0].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[4].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() - - # tokenize chunk 2 - advanceClock() - expect(tokenizedBuffer.tokenizedLines[5].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[9].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[10]).toBeUndefined() - - # tokenize last chunk - advanceClock() - expect(tokenizedBuffer.tokenizedLines[10].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[12].ruleStack?).toBeTruthy() - - describe "when the buffer is partially tokenized", -> - beforeEach -> - # tokenize chunk 1 only - advanceClock() - - describe "when there is a buffer change inside the tokenized region", -> - describe "when lines are added", -> - it "pushes the invalid rows down", -> - expect(tokenizedBuffer.firstInvalidRow()).toBe 5 - buffer.insert([1, 0], '\n\n') - expect(tokenizedBuffer.firstInvalidRow()).toBe 7 - - describe "when lines are removed", -> - it "pulls the invalid rows up", -> - expect(tokenizedBuffer.firstInvalidRow()).toBe 5 - buffer.delete([[1, 0], [3, 0]]) - expect(tokenizedBuffer.firstInvalidRow()).toBe 2 - - describe "when the change invalidates all the lines before the current invalid region", -> - it "retokenizes the invalidated lines and continues into the valid region", -> - expect(tokenizedBuffer.firstInvalidRow()).toBe 5 - buffer.insert([2, 0], '/*') - expect(tokenizedBuffer.firstInvalidRow()).toBe 3 - advanceClock() - expect(tokenizedBuffer.firstInvalidRow()).toBe 8 - - describe "when there is a buffer change surrounding an invalid row", -> - it "pushes the invalid row to the end of the change", -> - buffer.setTextInRange([[4, 0], [6, 0]], "\n\n\n") - expect(tokenizedBuffer.firstInvalidRow()).toBe 8 - - describe "when there is a buffer change inside an invalid region", -> - it "does not attempt to tokenize the lines in the change, and preserves the existing invalid row", -> - expect(tokenizedBuffer.firstInvalidRow()).toBe 5 - buffer.setTextInRange([[6, 0], [7, 0]], "\n\n\n") - expect(tokenizedBuffer.tokenizedLines[6]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[7]).toBeUndefined() - expect(tokenizedBuffer.firstInvalidRow()).toBe 5 - - describe "when the buffer is fully tokenized", -> - beforeEach -> - fullyTokenize(tokenizedBuffer) - - describe "when there is a buffer change that is smaller than the chunk size", -> - describe "when lines are updated, but none are added or removed", -> - it "updates tokens to reflect the change", -> - buffer.setTextInRange([[0, 0], [2, 0]], "foo()\n7\n") - - expect(tokenizedBuffer.tokenizedLines[0].tokens[1]).toEqual(value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js']) - expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual(value: '7', scopes: ['source.js', 'constant.numeric.decimal.js']) - # line 2 is unchanged - expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js']) - - describe "when the change invalidates the tokenization of subsequent lines", -> - it "schedules the invalidated lines to be tokenized in the background", -> - buffer.insert([5, 30], '/* */') - buffer.insert([2, 0], '/*') - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js'] - - advanceClock() - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - - it "resumes highlighting with the state of the previous line", -> - buffer.insert([0, 0], '/*') - buffer.insert([5, 0], '*/') - - buffer.insert([1, 0], 'var ') - expect(tokenizedBuffer.tokenizedLines[1].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - - describe "when lines are both updated and removed", -> - it "updates tokens to reflect the change", -> - buffer.setTextInRange([[1, 0], [3, 0]], "foo()") - - # previous line 0 remains - expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual(value: 'var', scopes: ['source.js', 'storage.type.var.js']) - - # previous line 3 should be combined with input to form line 1 - expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual(value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']) - expect(tokenizedBuffer.tokenizedLines[1].tokens[6]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']) - - # lines below deleted regions should be shifted upward - expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual(value: 'while', scopes: ['source.js', 'keyword.control.js']) - expect(tokenizedBuffer.tokenizedLines[3].tokens[1]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']) - expect(tokenizedBuffer.tokenizedLines[4].tokens[1]).toEqual(value: '<', scopes: ['source.js', 'keyword.operator.comparison.js']) - - describe "when the change invalidates the tokenization of subsequent lines", -> - it "schedules the invalidated lines to be tokenized in the background", -> - buffer.insert([5, 30], '/* */') - buffer.setTextInRange([[2, 0], [3, 0]], '/*') - expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'] - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js'] - - advanceClock() - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - - describe "when lines are both updated and inserted", -> - it "updates tokens to reflect the change", -> - buffer.setTextInRange([[1, 0], [2, 0]], "foo()\nbar()\nbaz()\nquux()") - - # previous line 0 remains - expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual( value: 'var', scopes: ['source.js', 'storage.type.var.js']) - - # 3 new lines inserted - expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual(value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']) - expect(tokenizedBuffer.tokenizedLines[2].tokens[0]).toEqual(value: 'bar', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']) - expect(tokenizedBuffer.tokenizedLines[3].tokens[0]).toEqual(value: 'baz', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']) - - # previous line 2 is joined with quux() on line 4 - expect(tokenizedBuffer.tokenizedLines[4].tokens[0]).toEqual(value: 'quux', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']) - expect(tokenizedBuffer.tokenizedLines[4].tokens[4]).toEqual(value: 'if', scopes: ['source.js', 'keyword.control.js']) - - # previous line 3 is pushed down to become line 5 - expect(tokenizedBuffer.tokenizedLines[5].tokens[3]).toEqual(value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']) - - describe "when the change invalidates the tokenization of subsequent lines", -> - it "schedules the invalidated lines to be tokenized in the background", -> - buffer.insert([5, 30], '/* */') - buffer.insert([2, 0], '/*\nabcde\nabcder') - expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual ['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js'] - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js'] - - advanceClock() # tokenize invalidated lines in background - expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[6].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[7].tokens[0].scopes).toEqual ['source.js', 'comment.block.js'] - expect(tokenizedBuffer.tokenizedLines[8].tokens[0].scopes).not.toBe ['source.js', 'comment.block.js'] - - describe "when there is an insertion that is larger than the chunk size", -> - it "tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background", -> - commentBlock = _.multiplyString("// a comment\n", tokenizedBuffer.chunkSize + 2) - buffer.insert([0, 0], commentBlock) - expect(tokenizedBuffer.tokenizedLines[0].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[4].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() - - advanceClock() - expect(tokenizedBuffer.tokenizedLines[5].ruleStack?).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[6].ruleStack?).toBeTruthy() - - it "does not break out soft tabs across a scope boundary", -> - waitsForPromise -> - atom.packages.activatePackage('language-gfm') - - runs -> - tokenizedBuffer.setTabLength(4) - tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md')) - buffer.setText(' 0 - - expect(length).toBe 4 - - describe "when the buffer contains hard-tabs", -> - beforeEach -> - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - runs -> - buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) - startTokenizing(tokenizedBuffer) - - afterEach -> - tokenizedBuffer.destroy() - buffer.release() - - describe "when the buffer is fully tokenized", -> - beforeEach -> - fullyTokenize(tokenizedBuffer) - - describe "when the grammar is tokenized", -> - it "emits the `tokenized` event", -> - editor = null - tokenizedHandler = jasmine.createSpy("tokenized handler") - - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - tokenizedBuffer = editor.tokenizedBuffer - tokenizedBuffer.onDidTokenize tokenizedHandler - fullyTokenize(tokenizedBuffer) - expect(tokenizedHandler.callCount).toBe(1) - - it "doesn't re-emit the `tokenized` event when it is re-tokenized", -> - editor = null - tokenizedHandler = jasmine.createSpy("tokenized handler") - - waitsForPromise -> - atom.workspace.open('sample.js').then (o) -> editor = o - - runs -> - tokenizedBuffer = editor.tokenizedBuffer - fullyTokenize(tokenizedBuffer) - - tokenizedBuffer.onDidTokenize tokenizedHandler - editor.getBuffer().insert([0, 0], "'") - fullyTokenize(tokenizedBuffer) - expect(tokenizedHandler).not.toHaveBeenCalled() - - describe "when the grammar is updated because a grammar it includes is activated", -> - it "re-emits the `tokenized` event", -> - editor = null - tokenizedBuffer = null - tokenizedHandler = jasmine.createSpy("tokenized handler") - - waitsForPromise -> - atom.workspace.open('coffee.coffee').then (o) -> editor = o - - runs -> - tokenizedBuffer = editor.tokenizedBuffer - tokenizedBuffer.onDidTokenize tokenizedHandler - fullyTokenize(tokenizedBuffer) - tokenizedHandler.reset() - - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - runs -> - fullyTokenize(tokenizedBuffer) - expect(tokenizedHandler.callCount).toBe(1) - - it "retokenizes the buffer", -> - waitsForPromise -> - atom.packages.activatePackage('language-ruby-on-rails') - - waitsForPromise -> - atom.packages.activatePackage('language-ruby') - - runs -> - buffer = atom.project.bufferForPathSync() - buffer.setText "
<%= User.find(2).full_name %>
" - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - {tokens} = tokenizedBuffer.tokenizedLines[0] - expect(tokens[0]).toEqual value: "
", scopes: ["text.html.ruby"] - - waitsForPromise -> - atom.packages.activatePackage('language-html') - - runs -> - fullyTokenize(tokenizedBuffer) - {tokens} = tokenizedBuffer.tokenizedLines[0] - expect(tokens[0]).toEqual value: '<', scopes: ["text.html.ruby", "meta.tag.block.div.html", "punctuation.definition.tag.begin.html"] - - describe ".tokenForPosition(position)", -> - afterEach -> - tokenizedBuffer.destroy() - buffer.release() - - it "returns the correct token (regression)", -> - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - expect(tokenizedBuffer.tokenForPosition([1, 0]).scopes).toEqual ["source.js"] - expect(tokenizedBuffer.tokenForPosition([1, 1]).scopes).toEqual ["source.js"] - expect(tokenizedBuffer.tokenForPosition([1, 2]).scopes).toEqual ["source.js", "storage.type.var.js"] - - describe ".bufferRangeForScopeAtPosition(selector, position)", -> - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - describe "when the selector does not match the token at the position", -> - it "returns a falsy value", -> - expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.bogus', [0, 1])).toBeUndefined() - - describe "when the selector matches a single token at the position", -> - it "returns the range covered by the token", -> - expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 1])).toEqual [[0, 0], [0, 3]] - expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 3])).toEqual [[0, 0], [0, 3]] - - describe "when the selector matches a run of multiple tokens at the position", -> - it "returns the range covered by all contiguous tokens (within a single line)", -> - expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual [[1, 6], [1, 28]] - - describe ".indentLevelForRow(row)", -> - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - describe "when the line is non-empty", -> - it "has an indent level based on the leading whitespace on the line", -> - expect(tokenizedBuffer.indentLevelForRow(0)).toBe 0 - expect(tokenizedBuffer.indentLevelForRow(1)).toBe 1 - expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2 - buffer.insert([2, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2.5 - - describe "when the line is empty", -> - it "assumes the indentation level of the first non-empty line below or above if one exists", -> - buffer.insert([12, 0], ' ') - buffer.insert([12, Infinity], '\n\n') - expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2 - expect(tokenizedBuffer.indentLevelForRow(14)).toBe 2 - - buffer.insert([1, Infinity], '\n\n') - expect(tokenizedBuffer.indentLevelForRow(2)).toBe 2 - expect(tokenizedBuffer.indentLevelForRow(3)).toBe 2 - - buffer.setText('\n\n\n') - expect(tokenizedBuffer.indentLevelForRow(1)).toBe 0 - - describe "when the changed lines are surrounded by whitespace-only lines", -> - it "updates the indentLevel of empty lines that precede the change", -> - expect(tokenizedBuffer.indentLevelForRow(12)).toBe 0 - - buffer.insert([12, 0], '\n') - buffer.insert([13, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(12)).toBe 1 - - it "updates empty line indent guides when the empty line is the last line", -> - buffer.insert([12, 2], '\n') - - # The newline and the tab need to be in two different operations to surface the bug - buffer.insert([12, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(13)).toBe 1 - - buffer.insert([12, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2 - expect(tokenizedBuffer.tokenizedLines[14]).not.toBeDefined() - - it "updates the indentLevel of empty lines surrounding a change that inserts lines", -> - buffer.insert([7, 0], '\n\n') - buffer.insert([5, 0], '\n\n') - expect(tokenizedBuffer.indentLevelForRow(5)).toBe 3 - expect(tokenizedBuffer.indentLevelForRow(6)).toBe 3 - expect(tokenizedBuffer.indentLevelForRow(9)).toBe 3 - expect(tokenizedBuffer.indentLevelForRow(10)).toBe 3 - expect(tokenizedBuffer.indentLevelForRow(11)).toBe 2 - - buffer.setTextInRange([[7, 0], [8, 65]], ' one\n two\n three\n four') - expect(tokenizedBuffer.indentLevelForRow(5)).toBe 4 - expect(tokenizedBuffer.indentLevelForRow(6)).toBe 4 - expect(tokenizedBuffer.indentLevelForRow(11)).toBe 4 - expect(tokenizedBuffer.indentLevelForRow(12)).toBe 4 - expect(tokenizedBuffer.indentLevelForRow(13)).toBe 2 - - it "updates the indentLevel of empty lines surrounding a change that removes lines", -> - buffer.insert([7, 0], '\n\n') - buffer.insert([5, 0], '\n\n') - buffer.setTextInRange([[7, 0], [8, 65]], ' ok') - expect(tokenizedBuffer.indentLevelForRow(5)).toBe 2 - expect(tokenizedBuffer.indentLevelForRow(6)).toBe 2 - expect(tokenizedBuffer.indentLevelForRow(7)).toBe 2 # new text - expect(tokenizedBuffer.indentLevelForRow(8)).toBe 2 - expect(tokenizedBuffer.indentLevelForRow(9)).toBe 2 - expect(tokenizedBuffer.indentLevelForRow(10)).toBe 2 # } - - describe "::isFoldableAtRow(row)", -> - beforeEach -> - buffer = atom.project.bufferForPathSync('sample.js') - buffer.insert [10, 0], " // multi-line\n // comment\n // block\n" - buffer.insert [0, 0], "// multi-line\n// comment\n// block\n" - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - it "includes the first line of multi-line comments", -> - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true # because of indent - expect(tokenizedBuffer.isFoldableAtRow(13)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(14)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(15)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(16)).toBe false - - buffer.insert([0, Infinity], '\n') - - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe false - - buffer.undo() - - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true # because of indent - - it "includes non-comment lines that precede an increase in indentation", -> - buffer.insert([2, 0], ' ') # commented lines preceding an indent aren't foldable - - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(4)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(5)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false - - buffer.insert([7, 0], ' ') - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false - - buffer.undo() - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false - - buffer.insert([7, 0], " \n x\n") - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false - - buffer.insert([9, 0], " ") - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe true - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe false - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe false - - describe "::tokenizedLineForRow(row)", -> - it "returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", -> - buffer = atom.project.bufferForPathSync('sample.js') - grammar = atom.grammars.grammarForScopeName('source.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2}) - line0 = buffer.lineForRow(0) - - jsScopeStartId = grammar.startIdForScope(grammar.scopeName) - jsScopeEndId = grammar.endIdForScope(grammar.scopeName) - startTokenizing(tokenizedBuffer) - expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) - expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([jsScopeStartId, line0.length, jsScopeEndId]) - advanceClock(1) - expect(tokenizedBuffer.tokenizedLines[0]).not.toBeUndefined() - expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) - expect(tokenizedBuffer.tokenizedLineForRow(0).tags).not.toEqual([jsScopeStartId, line0.length, jsScopeEndId]) - - nullScopeStartId = NullGrammar.startIdForScope(NullGrammar.scopeName) - nullScopeEndId = NullGrammar.endIdForScope(NullGrammar.scopeName) - tokenizedBuffer.setGrammar(NullGrammar) - startTokenizing(tokenizedBuffer) - expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) - expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId]) - advanceClock(1) - expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) - expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId]) - - it "returns undefined if the requested row is outside the buffer range", -> - buffer = atom.project.bufferForPathSync('sample.js') - grammar = atom.grammars.grammarForScopeName('source.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2}) - fullyTokenize(tokenizedBuffer) - expect(tokenizedBuffer.tokenizedLineForRow(999)).toBeUndefined() - - describe "when the buffer is configured with the null grammar", -> - it "does not actually tokenize using the grammar", -> - spyOn(NullGrammar, 'tokenizeLine').andCallThrough() - buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar') - buffer.setText('a\nb\nc') - tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2}) - tokenizeCallback = jasmine.createSpy('onDidTokenize') - tokenizedBuffer.onDidTokenize(tokenizeCallback) - - expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() - expect(tokenizeCallback.callCount).toBe(0) - expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() - - fullyTokenize(tokenizedBuffer) - expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() - expect(tokenizeCallback.callCount).toBe(0) - expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() - - describe "text decoration layer API", -> - describe "iterator", -> - it "iterates over the syntactic scope boundaries", -> - buffer = new TextBuffer(text: "var foo = 1 /*\nhello*/var bar = 2\n") - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName("source.js"), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - iterator = tokenizedBuffer.buildIterator() - iterator.seek(Point(0, 0)) - - expectedBoundaries = [ - {position: Point(0, 0), closeTags: [], openTags: ["syntax--source syntax--js", "syntax--storage syntax--type syntax--var syntax--js"]} - {position: Point(0, 3), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []} - {position: Point(0, 8), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]} - {position: Point(0, 9), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []} - {position: Point(0, 10), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]} - {position: Point(0, 11), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []} - {position: Point(0, 12), closeTags: [], openTags: ["syntax--comment syntax--block syntax--js", "syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js"]} - {position: Point(0, 14), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js"], openTags: []} - {position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js"]} - {position: Point(1, 7), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js", "syntax--comment syntax--block syntax--js"], openTags: ["syntax--storage syntax--type syntax--var syntax--js"]} - {position: Point(1, 10), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []} - {position: Point(1, 15), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]} - {position: Point(1, 16), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []} - {position: Point(1, 17), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]} - {position: Point(1, 18), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []} - ] - - loop - boundary = { - position: iterator.getPosition(), - closeTags: iterator.getCloseScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId)), - openTags: iterator.getOpenScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId)) - } - - expect(boundary).toEqual(expectedBoundaries.shift()) - break unless iterator.moveToSuccessor() - - expect(iterator.seek(Point(0, 1)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ - "syntax--source syntax--js", - "syntax--storage syntax--type syntax--var syntax--js" - ]) - expect(iterator.getPosition()).toEqual(Point(0, 3)) - expect(iterator.seek(Point(0, 8)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ - "syntax--source syntax--js" - ]) - expect(iterator.getPosition()).toEqual(Point(0, 8)) - expect(iterator.seek(Point(1, 0)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ - "syntax--source syntax--js", - "syntax--comment syntax--block syntax--js" - ]) - expect(iterator.getPosition()).toEqual(Point(1, 0)) - expect(iterator.seek(Point(1, 18)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ - "syntax--source syntax--js", - "syntax--constant syntax--numeric syntax--decimal syntax--js" - ]) - expect(iterator.getPosition()).toEqual(Point(1, 18)) - - expect(iterator.seek(Point(2, 0)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ - "syntax--source syntax--js" - ]) - iterator.moveToSuccessor() # ensure we don't infinitely loop (regression test) - - it "does not report columns beyond the length of the line", -> - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - runs -> - buffer = new TextBuffer(text: "# hello\n# world") - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName("source.coffee"), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - iterator = tokenizedBuffer.buildIterator() - iterator.seek(Point(0, 0)) - iterator.moveToSuccessor() - iterator.moveToSuccessor() - expect(iterator.getPosition().column).toBe(7) - - iterator.moveToSuccessor() - expect(iterator.getPosition().column).toBe(0) - - iterator.seek(Point(0, 7)) - expect(iterator.getPosition().column).toBe(7) - - iterator.seek(Point(0, 8)) - expect(iterator.getPosition().column).toBe(7) - - it "correctly terminates scopes at the beginning of the line (regression)", -> - grammar = atom.grammars.createGrammar('test', { - 'scopeName': 'text.broken' - 'name': 'Broken grammar' - 'patterns': [ - {'begin': 'start', 'end': '(?=end)', 'name': 'blue.broken'}, - {'match': '.', 'name': 'yellow.broken'} - ] - }) - - buffer = new TextBuffer(text: 'start x\nend x\nx') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - iterator = tokenizedBuffer.buildIterator() - iterator.seek(Point(1, 0)) - - expect(iterator.getPosition()).toEqual([1, 0]) - expect(iterator.getCloseScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual ['syntax--blue syntax--broken'] - expect(iterator.getOpenScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual ['syntax--yellow syntax--broken'] diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js new file mode 100644 index 000000000..783f5545c --- /dev/null +++ b/spec/tokenized-buffer-spec.js @@ -0,0 +1,779 @@ +const NullGrammar = require('../src/null-grammar') +const TokenizedBuffer = require('../src/tokenized-buffer') +const TextBuffer = require('text-buffer') +const {Point} = TextBuffer +const _ = require('underscore-plus') + +describe('TokenizedBuffer', () => { + let tokenizedBuffer, buffer + + beforeEach(() => { + // enable async tokenization + TokenizedBuffer.prototype.chunkSize = 5 + jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground') + + waitsForPromise(() => atom.packages.activatePackage('language-javascript')) + }) + + afterEach(() => tokenizedBuffer && tokenizedBuffer.destroy()) + + function startTokenizing (tokenizedBuffer) { + tokenizedBuffer.setVisible(true) + } + + function fullyTokenize (tokenizedBuffer) { + tokenizedBuffer.setVisible(true) + while (tokenizedBuffer.firstInvalidRow() != null) { + advanceClock() + } + } + + describe('serialization', () => { + describe('when the underlying buffer has a path', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + + waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + }) + + it('deserializes it searching among the buffers in the current project', () => { + const tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2}) + const tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom) + expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer) + }) + }) + + describe('when the underlying buffer has no path', () => { + beforeEach(() => buffer = atom.project.bufferForPathSync(null)) + + it('deserializes it searching among the buffers in the current project', () => { + const tokenizedBufferA = new TokenizedBuffer({buffer, tabLength: 2}) + const tokenizedBufferB = TokenizedBuffer.deserialize(JSON.parse(JSON.stringify(tokenizedBufferA.serialize())), atom) + expect(tokenizedBufferB.buffer).toBe(tokenizedBufferA.buffer) + }) + }) + }) + + describe('when the buffer is destroyed', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + startTokenizing(tokenizedBuffer) + }) + + it('stops tokenization', () => { + tokenizedBuffer.destroy() + spyOn(tokenizedBuffer, 'tokenizeNextChunk') + advanceClock() + expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled() + }) + }) + + describe('when the buffer contains soft-tabs', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + startTokenizing(tokenizedBuffer) + }) + + afterEach(() => { + tokenizedBuffer.destroy() + buffer.release() + }) + + describe('on construction', () => + it('tokenizes lines chunk at a time in the background', () => { + const line0 = tokenizedBuffer.tokenizedLines[0] + expect(line0).toBeUndefined() + + const line11 = tokenizedBuffer.tokenizedLines[11] + expect(line11).toBeUndefined() + + // tokenize chunk 1 + advanceClock() + expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() + + // tokenize chunk 2 + advanceClock() + expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[9].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[10]).toBeUndefined() + + // tokenize last chunk + advanceClock() + expect(tokenizedBuffer.tokenizedLines[10].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[12].ruleStack != null).toBeTruthy() + }) + ) + + describe('when the buffer is partially tokenized', () => { + beforeEach(() => { + // tokenize chunk 1 only + advanceClock() + }) + + describe('when there is a buffer change inside the tokenized region', () => { + describe('when lines are added', () => { + it('pushes the invalid rows down', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.insert([1, 0], '\n\n') + expect(tokenizedBuffer.firstInvalidRow()).toBe(7) + }) + }) + + describe('when lines are removed', () => { + it('pulls the invalid rows up', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.delete([[1, 0], [3, 0]]) + expect(tokenizedBuffer.firstInvalidRow()).toBe(2) + }) + }) + + describe('when the change invalidates all the lines before the current invalid region', () => { + it('retokenizes the invalidated lines and continues into the valid region', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.insert([2, 0], '/*') + expect(tokenizedBuffer.firstInvalidRow()).toBe(3) + advanceClock() + expect(tokenizedBuffer.firstInvalidRow()).toBe(8) + }) + }) + }) + + describe('when there is a buffer change surrounding an invalid row', () => { + it('pushes the invalid row to the end of the change', () => { + buffer.setTextInRange([[4, 0], [6, 0]], '\n\n\n') + expect(tokenizedBuffer.firstInvalidRow()).toBe(8) + }) + }) + + describe('when there is a buffer change inside an invalid region', () => { + it('does not attempt to tokenize the lines in the change, and preserves the existing invalid row', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.setTextInRange([[6, 0], [7, 0]], '\n\n\n') + expect(tokenizedBuffer.tokenizedLines[6]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[7]).toBeUndefined() + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + }) + }) + }) + + describe('when the buffer is fully tokenized', () => { + beforeEach(() => fullyTokenize(tokenizedBuffer)) + + describe('when there is a buffer change that is smaller than the chunk size', () => { + describe('when lines are updated, but none are added or removed', () => { + it('updates tokens to reflect the change', () => { + buffer.setTextInRange([[0, 0], [2, 0]], 'foo()\n7\n') + + expect(tokenizedBuffer.tokenizedLines[0].tokens[1]).toEqual({value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js']}) + expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: '7', scopes: ['source.js', 'constant.numeric.decimal.js']}) + // line 2 is unchanged + expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']}) + }) + + describe('when the change invalidates the tokenization of subsequent lines', () => { + it('schedules the invalidated lines to be tokenized in the background', () => { + buffer.insert([5, 30], '/* */') + buffer.insert([2, 0], '/*') + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js']) + + advanceClock() + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + }) + }) + + it('resumes highlighting with the state of the previous line', () => { + buffer.insert([0, 0], '/*') + buffer.insert([5, 0], '*/') + + buffer.insert([1, 0], 'var ') + expect(tokenizedBuffer.tokenizedLines[1].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + }) + }) + + describe('when lines are both updated and removed', () => { + it('updates tokens to reflect the change', () => { + buffer.setTextInRange([[1, 0], [3, 0]], 'foo()') + + // previous line 0 remains + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({value: 'var', scopes: ['source.js', 'storage.type.var.js']}) + + // previous line 3 should be combined with input to form line 1 + expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[1].tokens[6]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) + + // lines below deleted regions should be shifted upward + expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'while', scopes: ['source.js', 'keyword.control.js']}) + expect(tokenizedBuffer.tokenizedLines[3].tokens[1]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) + expect(tokenizedBuffer.tokenizedLines[4].tokens[1]).toEqual({value: '<', scopes: ['source.js', 'keyword.operator.comparison.js']}) + }) + }) + + describe('when the change invalidates the tokenization of subsequent lines', () => { + it('schedules the invalidated lines to be tokenized in the background', () => { + buffer.insert([5, 30], '/* */') + buffer.setTextInRange([[2, 0], [3, 0]], '/*') + expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']) + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js']) + + advanceClock() + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + }) + }) + + describe('when lines are both updated and inserted', () => { + it('updates tokens to reflect the change', () => { + buffer.setTextInRange([[1, 0], [2, 0]], 'foo()\nbar()\nbaz()\nquux()') + + // previous line 0 remains + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ value: 'var', scopes: ['source.js', 'storage.type.var.js']}) + + // 3 new lines inserted + expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[2].tokens[0]).toEqual({value: 'bar', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[3].tokens[0]).toEqual({value: 'baz', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + + // previous line 2 is joined with quux() on line 4 + expect(tokenizedBuffer.tokenizedLines[4].tokens[0]).toEqual({value: 'quux', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[4].tokens[4]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']}) + + // previous line 3 is pushed down to become line 5 + expect(tokenizedBuffer.tokenizedLines[5].tokens[3]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) + }) + }) + + describe('when the change invalidates the tokenization of subsequent lines', () => { + it('schedules the invalidated lines to be tokenized in the background', () => { + buffer.insert([5, 30], '/* */') + buffer.insert([2, 0], '/*\nabcde\nabcder') + expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']) + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js']) + + advanceClock() // tokenize invalidated lines in background + expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[6].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[7].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[8].tokens[0].scopes).not.toBe(['source.js', 'comment.block.js']) + }) + }) + }) + + describe('when there is an insertion that is larger than the chunk size', () => + it('tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background', () => { + const commentBlock = _.multiplyString('// a comment\n', tokenizedBuffer.chunkSize + 2) + buffer.insert([0, 0], commentBlock) + expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() + + advanceClock() + expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[6].ruleStack != null).toBeTruthy() + }) + ) + + it('does not break out soft tabs across a scope boundary', () => { + waitsForPromise(() => atom.packages.activatePackage('language-gfm')) + + runs(() => { + tokenizedBuffer.setTabLength(4) + tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md')) + buffer.setText(' 0) { length += tag } + } + + expect(length).toBe(4) + }) + }) + }) + }) + + describe('when the buffer contains hard-tabs', () => { + beforeEach(() => { + waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + + runs(() => { + buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) + startTokenizing(tokenizedBuffer) + }) + }) + + afterEach(() => { + tokenizedBuffer.destroy() + buffer.release() + }) + + describe('when the buffer is fully tokenized', () => { + beforeEach(() => fullyTokenize(tokenizedBuffer)) + }) + }) + + describe('when the grammar is tokenized', () => { + it('emits the `tokenized` event', () => { + let editor = null + const tokenizedHandler = jasmine.createSpy('tokenized handler') + + waitsForPromise(() => atom.workspace.open('sample.js').then(o => editor = o)) + + runs(() => { + ({ tokenizedBuffer } = editor) + tokenizedBuffer.onDidTokenize(tokenizedHandler) + fullyTokenize(tokenizedBuffer) + expect(tokenizedHandler.callCount).toBe(1) + }) + }) + + it("doesn't re-emit the `tokenized` event when it is re-tokenized", () => { + let editor = null + const tokenizedHandler = jasmine.createSpy('tokenized handler') + + waitsForPromise(() => atom.workspace.open('sample.js').then(o => editor = o)) + + runs(() => { + ({ tokenizedBuffer } = editor) + fullyTokenize(tokenizedBuffer) + + tokenizedBuffer.onDidTokenize(tokenizedHandler) + editor.getBuffer().insert([0, 0], "'") + fullyTokenize(tokenizedBuffer) + expect(tokenizedHandler).not.toHaveBeenCalled() + }) + }) + }) + + describe('when the grammar is updated because a grammar it includes is activated', () => { + it('re-emits the `tokenized` event', () => { + let editor = null + tokenizedBuffer = null + const tokenizedHandler = jasmine.createSpy('tokenized handler') + + waitsForPromise(() => atom.workspace.open('coffee.coffee').then(o => editor = o)) + + runs(() => { + ({ tokenizedBuffer } = editor) + tokenizedBuffer.onDidTokenize(tokenizedHandler) + fullyTokenize(tokenizedBuffer) + tokenizedHandler.reset() + }) + + waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + + runs(() => { + fullyTokenize(tokenizedBuffer) + expect(tokenizedHandler.callCount).toBe(1) + }) + }) + + it('retokenizes the buffer', () => { + waitsForPromise(() => atom.packages.activatePackage('language-ruby-on-rails')) + + waitsForPromise(() => atom.packages.activatePackage('language-ruby')) + + runs(() => { + buffer = atom.project.bufferForPathSync() + buffer.setText("
<%= User.find(2).full_name %>
") + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + + const {tokens} = tokenizedBuffer.tokenizedLines[0] + expect(tokens[0]).toEqual({value: "
", scopes: ['text.html.ruby']}) + }) + + waitsForPromise(() => atom.packages.activatePackage('language-html')) + + runs(() => { + fullyTokenize(tokenizedBuffer) + const {tokens} = tokenizedBuffer.tokenizedLines[0] + expect(tokens[0]).toEqual({value: '<', scopes: ['text.html.ruby', 'meta.tag.block.any.html', 'punctuation.definition.tag.begin.html']}) + }) + }) + }) + + describe('.tokenForPosition(position)', () => { + afterEach(() => { + tokenizedBuffer.destroy() + buffer.release() + }) + + it('returns the correct token (regression)', () => { + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenForPosition([1, 0]).scopes).toEqual(['source.js']) + expect(tokenizedBuffer.tokenForPosition([1, 1]).scopes).toEqual(['source.js']) + expect(tokenizedBuffer.tokenForPosition([1, 2]).scopes).toEqual(['source.js', 'storage.type.var.js']) + }) + }) + + describe('.bufferRangeForScopeAtPosition(selector, position)', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + }) + + describe('when the selector does not match the token at the position', () => + it('returns a falsy value', () => expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.bogus', [0, 1])).toBeUndefined()) + ) + + describe('when the selector matches a single token at the position', () => { + it('returns the range covered by the token', () => { + expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 1])).toEqual([[0, 0], [0, 3]]) + expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.storage.type.var.js', [0, 3])).toEqual([[0, 0], [0, 3]]) + }) + }) + + describe('when the selector matches a run of multiple tokens at the position', () => { + it('returns the range covered by all contiguous tokens (within a single line)', () => { + expect(tokenizedBuffer.bufferRangeForScopeAtPosition('.function', [1, 18])).toEqual([[1, 6], [1, 28]]) + }) + }) + }) + + describe('.indentLevelForRow(row)', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + }) + + describe('when the line is non-empty', () => { + it('has an indent level based on the leading whitespace on the line', () => { + expect(tokenizedBuffer.indentLevelForRow(0)).toBe(0) + expect(tokenizedBuffer.indentLevelForRow(1)).toBe(1) + expect(tokenizedBuffer.indentLevelForRow(2)).toBe(2) + buffer.insert([2, 0], ' ') + expect(tokenizedBuffer.indentLevelForRow(2)).toBe(2.5) + }) + }) + + describe('when the line is empty', () => { + it('assumes the indentation level of the first non-empty line below or above if one exists', () => { + buffer.insert([12, 0], ' ') + buffer.insert([12, Infinity], '\n\n') + expect(tokenizedBuffer.indentLevelForRow(13)).toBe(2) + expect(tokenizedBuffer.indentLevelForRow(14)).toBe(2) + + buffer.insert([1, Infinity], '\n\n') + expect(tokenizedBuffer.indentLevelForRow(2)).toBe(2) + expect(tokenizedBuffer.indentLevelForRow(3)).toBe(2) + + buffer.setText('\n\n\n') + expect(tokenizedBuffer.indentLevelForRow(1)).toBe(0) + }) + }) + + describe('when the changed lines are surrounded by whitespace-only lines', () => { + it('updates the indentLevel of empty lines that precede the change', () => { + expect(tokenizedBuffer.indentLevelForRow(12)).toBe(0) + + buffer.insert([12, 0], '\n') + buffer.insert([13, 0], ' ') + expect(tokenizedBuffer.indentLevelForRow(12)).toBe(1) + }) + + it('updates empty line indent guides when the empty line is the last line', () => { + buffer.insert([12, 2], '\n') + + // The newline and the tab need to be in two different operations to surface the bug + buffer.insert([12, 0], ' ') + expect(tokenizedBuffer.indentLevelForRow(13)).toBe(1) + + buffer.insert([12, 0], ' ') + expect(tokenizedBuffer.indentLevelForRow(13)).toBe(2) + expect(tokenizedBuffer.tokenizedLines[14]).not.toBeDefined() + }) + + it('updates the indentLevel of empty lines surrounding a change that inserts lines', () => { + buffer.insert([7, 0], '\n\n') + buffer.insert([5, 0], '\n\n') + expect(tokenizedBuffer.indentLevelForRow(5)).toBe(3) + expect(tokenizedBuffer.indentLevelForRow(6)).toBe(3) + expect(tokenizedBuffer.indentLevelForRow(9)).toBe(3) + expect(tokenizedBuffer.indentLevelForRow(10)).toBe(3) + expect(tokenizedBuffer.indentLevelForRow(11)).toBe(2) + + buffer.setTextInRange([[7, 0], [8, 65]], ' one\n two\n three\n four') + expect(tokenizedBuffer.indentLevelForRow(5)).toBe(4) + expect(tokenizedBuffer.indentLevelForRow(6)).toBe(4) + expect(tokenizedBuffer.indentLevelForRow(11)).toBe(4) + expect(tokenizedBuffer.indentLevelForRow(12)).toBe(4) + expect(tokenizedBuffer.indentLevelForRow(13)).toBe(2) + }) + + it('updates the indentLevel of empty lines surrounding a change that removes lines', () => { + buffer.insert([7, 0], '\n\n') + buffer.insert([5, 0], '\n\n') + buffer.setTextInRange([[7, 0], [8, 65]], ' ok') + expect(tokenizedBuffer.indentLevelForRow(5)).toBe(2) + expect(tokenizedBuffer.indentLevelForRow(6)).toBe(2) + expect(tokenizedBuffer.indentLevelForRow(7)).toBe(2) // new text + expect(tokenizedBuffer.indentLevelForRow(8)).toBe(2) + expect(tokenizedBuffer.indentLevelForRow(9)).toBe(2) + expect(tokenizedBuffer.indentLevelForRow(10)).toBe(2) + }) + }) + }) // } + + describe('::isFoldableAtRow(row)', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n') + buffer.insert([0, 0], '// multi-line\n// comment\n// block\n') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + }) + + it('includes the first line of multi-line comments', () => { + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) // because of indent + expect(tokenizedBuffer.isFoldableAtRow(13)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(14)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(15)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(16)).toBe(false) + + buffer.insert([0, Infinity], '\n') + + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(false) + + buffer.undo() + + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) + }) // because of indent + + it('includes non-comment lines that precede an increase in indentation', () => { + buffer.insert([2, 0], ' ') // commented lines preceding an indent aren't foldable + + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(4)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(5)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([7, 0], ' ') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.undo() + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([7, 0], ' \n x\n') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([9, 0], ' ') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + }) + }) + + describe('::tokenizedLineForRow(row)', () => { + it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => { + buffer = atom.project.bufferForPathSync('sample.js') + const grammar = atom.grammars.grammarForScopeName('source.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2}) + const line0 = buffer.lineForRow(0) + + const jsScopeStartId = grammar.startIdForScope(grammar.scopeName) + const jsScopeEndId = grammar.endIdForScope(grammar.scopeName) + startTokenizing(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) + expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([jsScopeStartId, line0.length, jsScopeEndId]) + advanceClock(1) + expect(tokenizedBuffer.tokenizedLines[0]).not.toBeUndefined() + expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) + expect(tokenizedBuffer.tokenizedLineForRow(0).tags).not.toEqual([jsScopeStartId, line0.length, jsScopeEndId]) + + const nullScopeStartId = NullGrammar.startIdForScope(NullGrammar.scopeName) + const nullScopeEndId = NullGrammar.endIdForScope(NullGrammar.scopeName) + tokenizedBuffer.setGrammar(NullGrammar) + startTokenizing(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) + expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId]) + advanceClock(1) + expect(tokenizedBuffer.tokenizedLineForRow(0).text).toBe(line0) + expect(tokenizedBuffer.tokenizedLineForRow(0).tags).toEqual([nullScopeStartId, line0.length, nullScopeEndId]) + }) + + it('returns undefined if the requested row is outside the buffer range', () => { + buffer = atom.project.bufferForPathSync('sample.js') + const grammar = atom.grammars.grammarForScopeName('source.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2}) + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLineForRow(999)).toBeUndefined() + }) + }) + + describe('when the buffer is configured with the null grammar', () => { + it('does not actually tokenize using the grammar', () => { + spyOn(NullGrammar, 'tokenizeLine').andCallThrough() + buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar') + buffer.setText('a\nb\nc') + tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2}) + const tokenizeCallback = jasmine.createSpy('onDidTokenize') + tokenizedBuffer.onDidTokenize(tokenizeCallback) + + expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() + expect(tokenizeCallback.callCount).toBe(0) + expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() + + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() + expect(tokenizeCallback.callCount).toBe(0) + expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() + }) + }) + + describe('text decoration layer API', () => { + describe('iterator', () => { + it('iterates over the syntactic scope boundaries', () => { + buffer = new TextBuffer({text: 'var foo = 1 /*\nhello*/var bar = 2\n'}) + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + + const iterator = tokenizedBuffer.buildIterator() + iterator.seek(Point(0, 0)) + + const expectedBoundaries = [ + {position: Point(0, 0), closeTags: [], openTags: ['syntax--source syntax--js', 'syntax--storage syntax--type syntax--var syntax--js']}, + {position: Point(0, 3), closeTags: ['syntax--storage syntax--type syntax--var syntax--js'], openTags: []}, + {position: Point(0, 8), closeTags: [], openTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js']}, + {position: Point(0, 9), closeTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js'], openTags: []}, + {position: Point(0, 10), closeTags: [], openTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js']}, + {position: Point(0, 11), closeTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js'], openTags: []}, + {position: Point(0, 12), closeTags: [], openTags: ['syntax--comment syntax--block syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js']}, + {position: Point(0, 14), closeTags: ['syntax--punctuation syntax--definition syntax--comment syntax--begin syntax--js'], openTags: []}, + {position: Point(1, 5), closeTags: [], openTags: ['syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js']}, + {position: Point(1, 7), closeTags: ['syntax--punctuation syntax--definition syntax--comment syntax--end syntax--js', 'syntax--comment syntax--block syntax--js'], openTags: ['syntax--storage syntax--type syntax--var syntax--js']}, + {position: Point(1, 10), closeTags: ['syntax--storage syntax--type syntax--var syntax--js'], openTags: []}, + {position: Point(1, 15), closeTags: [], openTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js']}, + {position: Point(1, 16), closeTags: ['syntax--keyword syntax--operator syntax--assignment syntax--js'], openTags: []}, + {position: Point(1, 17), closeTags: [], openTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js']}, + {position: Point(1, 18), closeTags: ['syntax--constant syntax--numeric syntax--decimal syntax--js'], openTags: []} + ] + + while (true) { + const boundary = { + position: iterator.getPosition(), + closeTags: iterator.getCloseScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId)), + openTags: iterator.getOpenScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId)) + } + + expect(boundary).toEqual(expectedBoundaries.shift()) + if (!iterator.moveToSuccessor()) { break } + } + + expect(iterator.seek(Point(0, 1)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + 'syntax--source syntax--js', + 'syntax--storage syntax--type syntax--var syntax--js' + ]) + expect(iterator.getPosition()).toEqual(Point(0, 3)) + expect(iterator.seek(Point(0, 8)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + 'syntax--source syntax--js' + ]) + expect(iterator.getPosition()).toEqual(Point(0, 8)) + expect(iterator.seek(Point(1, 0)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + 'syntax--source syntax--js', + 'syntax--comment syntax--block syntax--js' + ]) + expect(iterator.getPosition()).toEqual(Point(1, 0)) + expect(iterator.seek(Point(1, 18)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + 'syntax--source syntax--js', + 'syntax--constant syntax--numeric syntax--decimal syntax--js' + ]) + expect(iterator.getPosition()).toEqual(Point(1, 18)) + + expect(iterator.seek(Point(2, 0)).map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([ + 'syntax--source syntax--js' + ]) + iterator.moveToSuccessor() + }) // ensure we don't infinitely loop (regression test) + + it('does not report columns beyond the length of the line', () => { + waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + + runs(() => { + buffer = new TextBuffer({text: '# hello\n# world'}) + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + + const iterator = tokenizedBuffer.buildIterator() + iterator.seek(Point(0, 0)) + iterator.moveToSuccessor() + iterator.moveToSuccessor() + expect(iterator.getPosition().column).toBe(7) + + iterator.moveToSuccessor() + expect(iterator.getPosition().column).toBe(0) + + iterator.seek(Point(0, 7)) + expect(iterator.getPosition().column).toBe(7) + + iterator.seek(Point(0, 8)) + expect(iterator.getPosition().column).toBe(7) + }) + }) + + it('correctly terminates scopes at the beginning of the line (regression)', () => { + const grammar = atom.grammars.createGrammar('test', { + 'scopeName': 'text.broken', + 'name': 'Broken grammar', + 'patterns': [ + {'begin': 'start', 'end': '(?=end)', 'name': 'blue.broken'}, + {'match': '.', 'name': 'yellow.broken'} + ] + }) + + buffer = new TextBuffer({text: 'start x\nend x\nx'}) + tokenizedBuffer = new TokenizedBuffer({buffer, grammar, tabLength: 2}) + fullyTokenize(tokenizedBuffer) + + const iterator = tokenizedBuffer.buildIterator() + iterator.seek(Point(1, 0)) + + expect(iterator.getPosition()).toEqual([1, 0]) + expect(iterator.getCloseScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual(['syntax--blue syntax--broken']) + expect(iterator.getOpenScopeIds().map(scopeId => tokenizedBuffer.classNameForScopeId(scopeId))).toEqual(['syntax--yellow syntax--broken']) + }) + }) + }) +}) From 15a57287510521c26a597ff2e1b2a46a340fc659 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Sep 2017 14:27:52 -0700 Subject: [PATCH 08/81] Use async/await in TokenizedBuffer test --- spec/tokenized-buffer-spec.js | 180 ++++++++++++++-------------------- 1 file changed, 76 insertions(+), 104 deletions(-) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index 783f5545c..5eeceb34a 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -3,16 +3,17 @@ const TokenizedBuffer = require('../src/tokenized-buffer') const TextBuffer = require('text-buffer') const {Point} = TextBuffer const _ = require('underscore-plus') +const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') describe('TokenizedBuffer', () => { let tokenizedBuffer, buffer - beforeEach(() => { + beforeEach(async () => { // enable async tokenization TokenizedBuffer.prototype.chunkSize = 5 jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground') - waitsForPromise(() => atom.packages.activatePackage('language-javascript')) + await atom.packages.activatePackage('language-javascript') }) afterEach(() => tokenizedBuffer && tokenizedBuffer.destroy()) @@ -30,10 +31,9 @@ describe('TokenizedBuffer', () => { describe('serialization', () => { describe('when the underlying buffer has a path', () => { - beforeEach(() => { + beforeEach(async () => { buffer = atom.project.bufferForPathSync('sample.js') - - waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + await atom.packages.activatePackage('language-coffee-script') }) it('deserializes it searching among the buffers in the current project', () => { @@ -280,35 +280,31 @@ describe('TokenizedBuffer', () => { }) ) - it('does not break out soft tabs across a scope boundary', () => { - waitsForPromise(() => atom.packages.activatePackage('language-gfm')) + it('does not break out soft tabs across a scope boundary', async () => { + await atom.packages.activatePackage('language-gfm') - runs(() => { - tokenizedBuffer.setTabLength(4) - tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md')) - buffer.setText(' 0) { length += tag } - } + let length = 0 + for (let tag of tokenizedBuffer.tokenizedLines[1].tags) { + if (tag > 0) length += tag + } - expect(length).toBe(4) - }) + expect(length).toBe(4) }) }) }) describe('when the buffer contains hard-tabs', () => { - beforeEach(() => { - waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + beforeEach(async () => { + atom.packages.activatePackage('language-coffee-script') - runs(() => { - buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) - startTokenizing(tokenizedBuffer) - }) + buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) + startTokenizing(tokenizedBuffer) }) afterEach(() => { @@ -322,82 +318,60 @@ describe('TokenizedBuffer', () => { }) describe('when the grammar is tokenized', () => { - it('emits the `tokenized` event', () => { - let editor = null + it('emits the `tokenized` event', async () => { + const editor = await atom.workspace.open('sample.js') + const tokenizedHandler = jasmine.createSpy('tokenized handler') - - waitsForPromise(() => atom.workspace.open('sample.js').then(o => editor = o)) - - runs(() => { - ({ tokenizedBuffer } = editor) - tokenizedBuffer.onDidTokenize(tokenizedHandler) - fullyTokenize(tokenizedBuffer) - expect(tokenizedHandler.callCount).toBe(1) - }) + editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) + fullyTokenize(editor.tokenizedBuffer) + expect(tokenizedHandler.callCount).toBe(1) }) - it("doesn't re-emit the `tokenized` event when it is re-tokenized", () => { - let editor = null + it("doesn't re-emit the `tokenized` event when it is re-tokenized", async () => { + const editor = await atom.workspace.open('sample.js') + fullyTokenize(editor.tokenizedBuffer) + const tokenizedHandler = jasmine.createSpy('tokenized handler') - - waitsForPromise(() => atom.workspace.open('sample.js').then(o => editor = o)) - - runs(() => { - ({ tokenizedBuffer } = editor) - fullyTokenize(tokenizedBuffer) - - tokenizedBuffer.onDidTokenize(tokenizedHandler) - editor.getBuffer().insert([0, 0], "'") - fullyTokenize(tokenizedBuffer) - expect(tokenizedHandler).not.toHaveBeenCalled() - }) + editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) + editor.getBuffer().insert([0, 0], "'") + fullyTokenize(editor.tokenizedBuffer) + expect(tokenizedHandler).not.toHaveBeenCalled() }) }) - describe('when the grammar is updated because a grammar it includes is activated', () => { - it('re-emits the `tokenized` event', () => { - let editor = null - tokenizedBuffer = null + describe('when the grammar is updated because a grammar it includes is activated', async () => { + it('re-emits the `tokenized` event', async () => { + const editor = await atom.workspace.open('coffee.coffee') + const tokenizedHandler = jasmine.createSpy('tokenized handler') + editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) + fullyTokenize(editor.tokenizedBuffer) + tokenizedHandler.reset() - waitsForPromise(() => atom.workspace.open('coffee.coffee').then(o => editor = o)) - - runs(() => { - ({ tokenizedBuffer } = editor) - tokenizedBuffer.onDidTokenize(tokenizedHandler) - fullyTokenize(tokenizedBuffer) - tokenizedHandler.reset() - }) - - waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) - - runs(() => { - fullyTokenize(tokenizedBuffer) - expect(tokenizedHandler.callCount).toBe(1) - }) + await atom.packages.activatePackage('language-coffee-script') + fullyTokenize(editor.tokenizedBuffer) + expect(tokenizedHandler.callCount).toBe(1) }) - it('retokenizes the buffer', () => { - waitsForPromise(() => atom.packages.activatePackage('language-ruby-on-rails')) + it('retokenizes the buffer', async () => { + await atom.packages.activatePackage('language-ruby-on-rails') + await atom.packages.activatePackage('language-ruby') - waitsForPromise(() => atom.packages.activatePackage('language-ruby')) + buffer = atom.project.bufferForPathSync() + buffer.setText("
<%= User.find(2).full_name %>
") - runs(() => { - buffer = atom.project.bufferForPathSync() - buffer.setText("
<%= User.find(2).full_name %>
") - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - - const {tokens} = tokenizedBuffer.tokenizedLines[0] - expect(tokens[0]).toEqual({value: "
", scopes: ['text.html.ruby']}) + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ + value: "
", + scopes: ['text.html.ruby'] }) - waitsForPromise(() => atom.packages.activatePackage('language-html')) - - runs(() => { - fullyTokenize(tokenizedBuffer) - const {tokens} = tokenizedBuffer.tokenizedLines[0] - expect(tokens[0]).toEqual({value: '<', scopes: ['text.html.ruby', 'meta.tag.block.any.html', 'punctuation.definition.tag.begin.html']}) + await atom.packages.activatePackage('language-html') + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ + value: '<', + scopes: ['text.html.ruby', 'meta.tag.block.any.html', 'punctuation.definition.tag.begin.html'] }) }) }) @@ -728,29 +702,27 @@ describe('TokenizedBuffer', () => { iterator.moveToSuccessor() }) // ensure we don't infinitely loop (regression test) - it('does not report columns beyond the length of the line', () => { - waitsForPromise(() => atom.packages.activatePackage('language-coffee-script')) + it('does not report columns beyond the length of the line', async () => { + await atom.packages.activatePackage('language-coffee-script') - runs(() => { - buffer = new TextBuffer({text: '# hello\n# world'}) - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) + buffer = new TextBuffer({text: '# hello\n# world'}) + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) - const iterator = tokenizedBuffer.buildIterator() - iterator.seek(Point(0, 0)) - iterator.moveToSuccessor() - iterator.moveToSuccessor() - expect(iterator.getPosition().column).toBe(7) + const iterator = tokenizedBuffer.buildIterator() + iterator.seek(Point(0, 0)) + iterator.moveToSuccessor() + iterator.moveToSuccessor() + expect(iterator.getPosition().column).toBe(7) - iterator.moveToSuccessor() - expect(iterator.getPosition().column).toBe(0) + iterator.moveToSuccessor() + expect(iterator.getPosition().column).toBe(0) - iterator.seek(Point(0, 7)) - expect(iterator.getPosition().column).toBe(7) + iterator.seek(Point(0, 7)) + expect(iterator.getPosition().column).toBe(7) - iterator.seek(Point(0, 8)) - expect(iterator.getPosition().column).toBe(7) - }) + iterator.seek(Point(0, 8)) + expect(iterator.getPosition().column).toBe(7) }) it('correctly terminates scopes at the beginning of the line (regression)', () => { From 4c2680e68a9d9ed1234b192bc3cdcdd7d8878132 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Sep 2017 14:31:13 -0700 Subject: [PATCH 09/81] Organize TokenizedBuffer test --- spec/tokenized-buffer-spec.js | 584 +++++++++++++++++----------------- 1 file changed, 293 insertions(+), 291 deletions(-) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index 5eeceb34a..4f12ed69f 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -54,324 +54,350 @@ describe('TokenizedBuffer', () => { }) }) - describe('when the buffer is destroyed', () => { - beforeEach(() => { - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - startTokenizing(tokenizedBuffer) - }) - - it('stops tokenization', () => { - tokenizedBuffer.destroy() - spyOn(tokenizedBuffer, 'tokenizeNextChunk') - advanceClock() - expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled() - }) - }) - - describe('when the buffer contains soft-tabs', () => { - beforeEach(() => { - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - startTokenizing(tokenizedBuffer) - }) - - afterEach(() => { - tokenizedBuffer.destroy() - buffer.release() - }) - - describe('on construction', () => - it('tokenizes lines chunk at a time in the background', () => { - const line0 = tokenizedBuffer.tokenizedLines[0] - expect(line0).toBeUndefined() - - const line11 = tokenizedBuffer.tokenizedLines[11] - expect(line11).toBeUndefined() - - // tokenize chunk 1 - advanceClock() - expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() - - // tokenize chunk 2 - advanceClock() - expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[9].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[10]).toBeUndefined() - - // tokenize last chunk - advanceClock() - expect(tokenizedBuffer.tokenizedLines[10].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[12].ruleStack != null).toBeTruthy() - }) - ) - - describe('when the buffer is partially tokenized', () => { + describe('tokenizing', () => { + describe('when the buffer is destroyed', () => { beforeEach(() => { - // tokenize chunk 1 only - advanceClock() + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + startTokenizing(tokenizedBuffer) }) - describe('when there is a buffer change inside the tokenized region', () => { - describe('when lines are added', () => { - it('pushes the invalid rows down', () => { - expect(tokenizedBuffer.firstInvalidRow()).toBe(5) - buffer.insert([1, 0], '\n\n') - expect(tokenizedBuffer.firstInvalidRow()).toBe(7) + it('stops tokenization', () => { + tokenizedBuffer.destroy() + spyOn(tokenizedBuffer, 'tokenizeNextChunk') + advanceClock() + expect(tokenizedBuffer.tokenizeNextChunk).not.toHaveBeenCalled() + }) + }) + + describe('when the buffer contains soft-tabs', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + startTokenizing(tokenizedBuffer) + }) + + afterEach(() => { + tokenizedBuffer.destroy() + buffer.release() + }) + + describe('on construction', () => + it('tokenizes lines chunk at a time in the background', () => { + const line0 = tokenizedBuffer.tokenizedLines[0] + expect(line0).toBeUndefined() + + const line11 = tokenizedBuffer.tokenizedLines[11] + expect(line11).toBeUndefined() + + // tokenize chunk 1 + advanceClock() + expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() + + // tokenize chunk 2 + advanceClock() + expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[9].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[10]).toBeUndefined() + + // tokenize last chunk + advanceClock() + expect(tokenizedBuffer.tokenizedLines[10].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[12].ruleStack != null).toBeTruthy() + }) + ) + + describe('when the buffer is partially tokenized', () => { + beforeEach(() => { + // tokenize chunk 1 only + advanceClock() + }) + + describe('when there is a buffer change inside the tokenized region', () => { + describe('when lines are added', () => { + it('pushes the invalid rows down', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.insert([1, 0], '\n\n') + expect(tokenizedBuffer.firstInvalidRow()).toBe(7) + }) + }) + + describe('when lines are removed', () => { + it('pulls the invalid rows up', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.delete([[1, 0], [3, 0]]) + expect(tokenizedBuffer.firstInvalidRow()).toBe(2) + }) + }) + + describe('when the change invalidates all the lines before the current invalid region', () => { + it('retokenizes the invalidated lines and continues into the valid region', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.insert([2, 0], '/*') + expect(tokenizedBuffer.firstInvalidRow()).toBe(3) + advanceClock() + expect(tokenizedBuffer.firstInvalidRow()).toBe(8) + }) }) }) - describe('when lines are removed', () => { - it('pulls the invalid rows up', () => { - expect(tokenizedBuffer.firstInvalidRow()).toBe(5) - buffer.delete([[1, 0], [3, 0]]) - expect(tokenizedBuffer.firstInvalidRow()).toBe(2) - }) - }) - - describe('when the change invalidates all the lines before the current invalid region', () => { - it('retokenizes the invalidated lines and continues into the valid region', () => { - expect(tokenizedBuffer.firstInvalidRow()).toBe(5) - buffer.insert([2, 0], '/*') - expect(tokenizedBuffer.firstInvalidRow()).toBe(3) - advanceClock() + describe('when there is a buffer change surrounding an invalid row', () => { + it('pushes the invalid row to the end of the change', () => { + buffer.setTextInRange([[4, 0], [6, 0]], '\n\n\n') expect(tokenizedBuffer.firstInvalidRow()).toBe(8) }) }) - }) - describe('when there is a buffer change surrounding an invalid row', () => { - it('pushes the invalid row to the end of the change', () => { - buffer.setTextInRange([[4, 0], [6, 0]], '\n\n\n') - expect(tokenizedBuffer.firstInvalidRow()).toBe(8) + describe('when there is a buffer change inside an invalid region', () => { + it('does not attempt to tokenize the lines in the change, and preserves the existing invalid row', () => { + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + buffer.setTextInRange([[6, 0], [7, 0]], '\n\n\n') + expect(tokenizedBuffer.tokenizedLines[6]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[7]).toBeUndefined() + expect(tokenizedBuffer.firstInvalidRow()).toBe(5) + }) }) }) - describe('when there is a buffer change inside an invalid region', () => { - it('does not attempt to tokenize the lines in the change, and preserves the existing invalid row', () => { - expect(tokenizedBuffer.firstInvalidRow()).toBe(5) - buffer.setTextInRange([[6, 0], [7, 0]], '\n\n\n') - expect(tokenizedBuffer.tokenizedLines[6]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[7]).toBeUndefined() - expect(tokenizedBuffer.firstInvalidRow()).toBe(5) - }) - }) - }) + describe('when the buffer is fully tokenized', () => { + beforeEach(() => fullyTokenize(tokenizedBuffer)) - describe('when the buffer is fully tokenized', () => { - beforeEach(() => fullyTokenize(tokenizedBuffer)) + describe('when there is a buffer change that is smaller than the chunk size', () => { + describe('when lines are updated, but none are added or removed', () => { + it('updates tokens to reflect the change', () => { + buffer.setTextInRange([[0, 0], [2, 0]], 'foo()\n7\n') - describe('when there is a buffer change that is smaller than the chunk size', () => { - describe('when lines are updated, but none are added or removed', () => { - it('updates tokens to reflect the change', () => { - buffer.setTextInRange([[0, 0], [2, 0]], 'foo()\n7\n') + expect(tokenizedBuffer.tokenizedLines[0].tokens[1]).toEqual({value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js']}) + expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: '7', scopes: ['source.js', 'constant.numeric.decimal.js']}) + // line 2 is unchanged + expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']}) + }) - expect(tokenizedBuffer.tokenizedLines[0].tokens[1]).toEqual({value: '(', scopes: ['source.js', 'meta.function-call.js', 'meta.arguments.js', 'punctuation.definition.arguments.begin.bracket.round.js']}) - expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: '7', scopes: ['source.js', 'constant.numeric.decimal.js']}) - // line 2 is unchanged - expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']}) + describe('when the change invalidates the tokenization of subsequent lines', () => { + it('schedules the invalidated lines to be tokenized in the background', () => { + buffer.insert([5, 30], '/* */') + buffer.insert([2, 0], '/*') + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js']) + + advanceClock() + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + }) + }) + + it('resumes highlighting with the state of the previous line', () => { + buffer.insert([0, 0], '/*') + buffer.insert([5, 0], '*/') + + buffer.insert([1, 0], 'var ') + expect(tokenizedBuffer.tokenizedLines[1].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + }) + }) + + describe('when lines are both updated and removed', () => { + it('updates tokens to reflect the change', () => { + buffer.setTextInRange([[1, 0], [3, 0]], 'foo()') + + // previous line 0 remains + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({value: 'var', scopes: ['source.js', 'storage.type.var.js']}) + + // previous line 3 should be combined with input to form line 1 + expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[1].tokens[6]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) + + // lines below deleted regions should be shifted upward + expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'while', scopes: ['source.js', 'keyword.control.js']}) + expect(tokenizedBuffer.tokenizedLines[3].tokens[1]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) + expect(tokenizedBuffer.tokenizedLines[4].tokens[1]).toEqual({value: '<', scopes: ['source.js', 'keyword.operator.comparison.js']}) + }) }) describe('when the change invalidates the tokenization of subsequent lines', () => { it('schedules the invalidated lines to be tokenized in the background', () => { buffer.insert([5, 30], '/* */') - buffer.insert([2, 0], '/*') + buffer.setTextInRange([[2, 0], [3, 0]], '/*') + expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']) expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js']) advanceClock() expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) - expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) }) }) - it('resumes highlighting with the state of the previous line', () => { - buffer.insert([0, 0], '/*') - buffer.insert([5, 0], '*/') + describe('when lines are both updated and inserted', () => { + it('updates tokens to reflect the change', () => { + buffer.setTextInRange([[1, 0], [2, 0]], 'foo()\nbar()\nbaz()\nquux()') - buffer.insert([1, 0], 'var ') - expect(tokenizedBuffer.tokenizedLines[1].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + // previous line 0 remains + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ value: 'var', scopes: ['source.js', 'storage.type.var.js']}) + + // 3 new lines inserted + expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[2].tokens[0]).toEqual({value: 'bar', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[3].tokens[0]).toEqual({value: 'baz', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + + // previous line 2 is joined with quux() on line 4 + expect(tokenizedBuffer.tokenizedLines[4].tokens[0]).toEqual({value: 'quux', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) + expect(tokenizedBuffer.tokenizedLines[4].tokens[4]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']}) + + // previous line 3 is pushed down to become line 5 + expect(tokenizedBuffer.tokenizedLines[5].tokens[3]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) + }) + }) + + describe('when the change invalidates the tokenization of subsequent lines', () => { + it('schedules the invalidated lines to be tokenized in the background', () => { + buffer.insert([5, 30], '/* */') + buffer.insert([2, 0], '/*\nabcde\nabcder') + expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']) + expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js']) + + advanceClock() // tokenize invalidated lines in background + expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[6].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[7].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[8].tokens[0].scopes).not.toBe(['source.js', 'comment.block.js']) + }) }) }) - describe('when lines are both updated and removed', () => { - it('updates tokens to reflect the change', () => { - buffer.setTextInRange([[1, 0], [3, 0]], 'foo()') - - // previous line 0 remains - expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({value: 'var', scopes: ['source.js', 'storage.type.var.js']}) - - // previous line 3 should be combined with input to form line 1 - expect(tokenizedBuffer.tokenizedLines[1].tokens[0]).toEqual({value: 'foo', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) - expect(tokenizedBuffer.tokenizedLines[1].tokens[6]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) - - // lines below deleted regions should be shifted upward - expect(tokenizedBuffer.tokenizedLines[2].tokens[1]).toEqual({value: 'while', scopes: ['source.js', 'keyword.control.js']}) - expect(tokenizedBuffer.tokenizedLines[3].tokens[1]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) - expect(tokenizedBuffer.tokenizedLines[4].tokens[1]).toEqual({value: '<', scopes: ['source.js', 'keyword.operator.comparison.js']}) - }) - }) - - describe('when the change invalidates the tokenization of subsequent lines', () => { - it('schedules the invalidated lines to be tokenized in the background', () => { - buffer.insert([5, 30], '/* */') - buffer.setTextInRange([[2, 0], [3, 0]], '/*') - expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']) - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js']) + describe('when there is an insertion that is larger than the chunk size', () => + it('tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background', () => { + const commentBlock = _.multiplyString('// a comment\n', tokenizedBuffer.chunkSize + 2) + buffer.insert([0, 0], commentBlock) + expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() advanceClock() - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) - expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) + expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy() + expect(tokenizedBuffer.tokenizedLines[6].ruleStack != null).toBeTruthy() }) - }) + ) - describe('when lines are both updated and inserted', () => { - it('updates tokens to reflect the change', () => { - buffer.setTextInRange([[1, 0], [2, 0]], 'foo()\nbar()\nbaz()\nquux()') + it('does not break out soft tabs across a scope boundary', async () => { + await atom.packages.activatePackage('language-gfm') - // previous line 0 remains - expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ value: 'var', scopes: ['source.js', 'storage.type.var.js']}) + tokenizedBuffer.setTabLength(4) + tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md')) + buffer.setText(' 0) length += tag + } - // previous line 2 is joined with quux() on line 4 - expect(tokenizedBuffer.tokenizedLines[4].tokens[0]).toEqual({value: 'quux', scopes: ['source.js', 'meta.function-call.js', 'entity.name.function.js']}) - expect(tokenizedBuffer.tokenizedLines[4].tokens[4]).toEqual({value: 'if', scopes: ['source.js', 'keyword.control.js']}) - - // previous line 3 is pushed down to become line 5 - expect(tokenizedBuffer.tokenizedLines[5].tokens[3]).toEqual({value: '=', scopes: ['source.js', 'keyword.operator.assignment.js']}) - }) - }) - - describe('when the change invalidates the tokenization of subsequent lines', () => { - it('schedules the invalidated lines to be tokenized in the background', () => { - buffer.insert([5, 30], '/* */') - buffer.insert([2, 0], '/*\nabcde\nabcder') - expect(tokenizedBuffer.tokenizedLines[2].tokens[0].scopes).toEqual(['source.js', 'comment.block.js', 'punctuation.definition.comment.begin.js']) - expect(tokenizedBuffer.tokenizedLines[3].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) - expect(tokenizedBuffer.tokenizedLines[4].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) - expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js']) - - advanceClock() // tokenize invalidated lines in background - expect(tokenizedBuffer.tokenizedLines[5].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) - expect(tokenizedBuffer.tokenizedLines[6].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) - expect(tokenizedBuffer.tokenizedLines[7].tokens[0].scopes).toEqual(['source.js', 'comment.block.js']) - expect(tokenizedBuffer.tokenizedLines[8].tokens[0].scopes).not.toBe(['source.js', 'comment.block.js']) - }) + expect(length).toBe(4) }) }) + }) - describe('when there is an insertion that is larger than the chunk size', () => - it('tokenizes the initial chunk synchronously, then tokenizes the remaining lines in the background', () => { - const commentBlock = _.multiplyString('// a comment\n', tokenizedBuffer.chunkSize + 2) - buffer.insert([0, 0], commentBlock) - expect(tokenizedBuffer.tokenizedLines[0].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[4].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[5]).toBeUndefined() + describe('when the buffer contains hard-tabs', () => { + beforeEach(async () => { + atom.packages.activatePackage('language-coffee-script') - advanceClock() - expect(tokenizedBuffer.tokenizedLines[5].ruleStack != null).toBeTruthy() - expect(tokenizedBuffer.tokenizedLines[6].ruleStack != null).toBeTruthy() - }) - ) + buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) + startTokenizing(tokenizedBuffer) + }) - it('does not break out soft tabs across a scope boundary', async () => { - await atom.packages.activatePackage('language-gfm') + afterEach(() => { + tokenizedBuffer.destroy() + buffer.release() + }) - tokenizedBuffer.setTabLength(4) - tokenizedBuffer.setGrammar(atom.grammars.selectGrammar('.md')) - buffer.setText(' { + beforeEach(() => fullyTokenize(tokenizedBuffer)) + }) + }) + + describe('when tokenization completes', () => { + it('emits the `tokenized` event', async () => { + const editor = await atom.workspace.open('sample.js') + + const tokenizedHandler = jasmine.createSpy('tokenized handler') + editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) + fullyTokenize(editor.tokenizedBuffer) + expect(tokenizedHandler.callCount).toBe(1) + }) + + it("doesn't re-emit the `tokenized` event when it is re-tokenized", async () => { + const editor = await atom.workspace.open('sample.js') + fullyTokenize(editor.tokenizedBuffer) + + const tokenizedHandler = jasmine.createSpy('tokenized handler') + editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) + editor.getBuffer().insert([0, 0], "'") + fullyTokenize(editor.tokenizedBuffer) + expect(tokenizedHandler).not.toHaveBeenCalled() + }) + }) + + describe('when the grammar is updated because a grammar it includes is activated', async () => { + it('re-emits the `tokenized` event', async () => { + const editor = await atom.workspace.open('coffee.coffee') + + const tokenizedHandler = jasmine.createSpy('tokenized handler') + editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) + fullyTokenize(editor.tokenizedBuffer) + tokenizedHandler.reset() + + await atom.packages.activatePackage('language-coffee-script') + fullyTokenize(editor.tokenizedBuffer) + expect(tokenizedHandler.callCount).toBe(1) + }) + + it('retokenizes the buffer', async () => { + await atom.packages.activatePackage('language-ruby-on-rails') + await atom.packages.activatePackage('language-ruby') + + buffer = atom.project.bufferForPathSync() + buffer.setText("
<%= User.find(2).full_name %>
") + + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2}) fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ + value: "
", + scopes: ['text.html.ruby'] + }) - let length = 0 - for (let tag of tokenizedBuffer.tokenizedLines[1].tags) { - if (tag > 0) length += tag - } - - expect(length).toBe(4) + await atom.packages.activatePackage('language-html') + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ + value: '<', + scopes: ['text.html.ruby', 'meta.tag.block.any.html', 'punctuation.definition.tag.begin.html'] + }) }) }) - }) - describe('when the buffer contains hard-tabs', () => { - beforeEach(async () => { - atom.packages.activatePackage('language-coffee-script') + describe('when the buffer is configured with the null grammar', () => { + it('does not actually tokenize using the grammar', () => { + spyOn(NullGrammar, 'tokenizeLine').andCallThrough() + buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar') + buffer.setText('a\nb\nc') + tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2}) + const tokenizeCallback = jasmine.createSpy('onDidTokenize') + tokenizedBuffer.onDidTokenize(tokenizeCallback) - buffer = atom.project.bufferForPathSync('sample-with-tabs.coffee') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.coffee'), tabLength: 2}) - startTokenizing(tokenizedBuffer) - }) + expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() + expect(tokenizeCallback.callCount).toBe(0) + expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() - afterEach(() => { - tokenizedBuffer.destroy() - buffer.release() - }) - - describe('when the buffer is fully tokenized', () => { - beforeEach(() => fullyTokenize(tokenizedBuffer)) - }) - }) - - describe('when the grammar is tokenized', () => { - it('emits the `tokenized` event', async () => { - const editor = await atom.workspace.open('sample.js') - - const tokenizedHandler = jasmine.createSpy('tokenized handler') - editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) - fullyTokenize(editor.tokenizedBuffer) - expect(tokenizedHandler.callCount).toBe(1) - }) - - it("doesn't re-emit the `tokenized` event when it is re-tokenized", async () => { - const editor = await atom.workspace.open('sample.js') - fullyTokenize(editor.tokenizedBuffer) - - const tokenizedHandler = jasmine.createSpy('tokenized handler') - editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) - editor.getBuffer().insert([0, 0], "'") - fullyTokenize(editor.tokenizedBuffer) - expect(tokenizedHandler).not.toHaveBeenCalled() - }) - }) - - describe('when the grammar is updated because a grammar it includes is activated', async () => { - it('re-emits the `tokenized` event', async () => { - const editor = await atom.workspace.open('coffee.coffee') - - const tokenizedHandler = jasmine.createSpy('tokenized handler') - editor.tokenizedBuffer.onDidTokenize(tokenizedHandler) - fullyTokenize(editor.tokenizedBuffer) - tokenizedHandler.reset() - - await atom.packages.activatePackage('language-coffee-script') - fullyTokenize(editor.tokenizedBuffer) - expect(tokenizedHandler.callCount).toBe(1) - }) - - it('retokenizes the buffer', async () => { - await atom.packages.activatePackage('language-ruby-on-rails') - await atom.packages.activatePackage('language-ruby') - - buffer = atom.project.bufferForPathSync() - buffer.setText("
<%= User.find(2).full_name %>
") - - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.selectGrammar('test.erb'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ - value: "
", - scopes: ['text.html.ruby'] - }) - - await atom.packages.activatePackage('language-html') - fullyTokenize(tokenizedBuffer) - expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ - value: '<', - scopes: ['text.html.ruby', 'meta.tag.block.any.html', 'punctuation.definition.tag.begin.html'] + fullyTokenize(tokenizedBuffer) + expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() + expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() + expect(tokenizeCallback.callCount).toBe(0) + expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() }) }) }) @@ -502,7 +528,7 @@ describe('TokenizedBuffer', () => { }) }) // } - describe('::isFoldableAtRow(row)', () => { + describe('.isFoldableAtRow(row)', () => { beforeEach(() => { buffer = atom.project.bufferForPathSync('sample.js') buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n') @@ -574,7 +600,7 @@ describe('TokenizedBuffer', () => { }) }) - describe('::tokenizedLineForRow(row)', () => { + describe('.tokenizedLineForRow(row)', () => { it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => { buffer = atom.project.bufferForPathSync('sample.js') const grammar = atom.grammars.grammarForScopeName('source.js') @@ -613,30 +639,6 @@ describe('TokenizedBuffer', () => { }) }) - describe('when the buffer is configured with the null grammar', () => { - it('does not actually tokenize using the grammar', () => { - spyOn(NullGrammar, 'tokenizeLine').andCallThrough() - buffer = atom.project.bufferForPathSync('sample.will-use-the-null-grammar') - buffer.setText('a\nb\nc') - tokenizedBuffer = new TokenizedBuffer({buffer, tabLength: 2}) - const tokenizeCallback = jasmine.createSpy('onDidTokenize') - tokenizedBuffer.onDidTokenize(tokenizeCallback) - - expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() - expect(tokenizeCallback.callCount).toBe(0) - expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() - - fullyTokenize(tokenizedBuffer) - expect(tokenizedBuffer.tokenizedLines[0]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[1]).toBeUndefined() - expect(tokenizedBuffer.tokenizedLines[2]).toBeUndefined() - expect(tokenizeCallback.callCount).toBe(0) - expect(NullGrammar.tokenizeLine).not.toHaveBeenCalled() - }) - }) - describe('text decoration layer API', () => { describe('iterator', () => { it('iterates over the syntactic scope boundaries', () => { From 58035e46822d4c265a0704031565113505c56f9d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Sep 2017 15:15:53 -0700 Subject: [PATCH 10/81] :shirt: --- src/tokenized-buffer.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index 80601d1f3..fbb9de77f 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -1,7 +1,6 @@ const _ = require('underscore-plus') const {CompositeDisposable, Emitter} = require('event-kit') const {Point, Range} = require('text-buffer') -const Model = require('./model') const TokenizedLine = require('./tokenized-line') const TokenIterator = require('./token-iterator') const ScopeDescriptor = require('./scope-descriptor') @@ -275,7 +274,7 @@ class TokenizedBuffer { if (row >= 0 && row <= this.buffer.getLastRow()) { const nextRow = this.buffer.nextNonBlankRow(row) const tokenizedLine = this.tokenizedLines[row] - if (this.buffer.isRowBlank(row) || (tokenizedLine != null ? tokenizedLine.isComment() : undefined) || (nextRow == null)) { + if (this.buffer.isRowBlank(row) || (tokenizedLine && tokenizedLine.isComment()) || nextRow == null) { return false } else { return this.indentLevelForRow(nextRow) > this.indentLevelForRow(row) @@ -288,15 +287,11 @@ class TokenizedBuffer { isFoldableCommentAtRow (row) { const previousRow = row - 1 const nextRow = row + 1 - if (nextRow > this.buffer.getLastRow()) { - return false - } else { - return Boolean( - !(this.tokenizedLines[previousRow] != null ? this.tokenizedLines[previousRow].isComment() : undefined) && - (this.tokenizedLines[row] != null ? this.tokenizedLines[row].isComment() : undefined) && - (this.tokenizedLines[nextRow] != null ? this.tokenizedLines[nextRow].isComment() : undefined) - ) - } + return ( + (!this.tokenizedLines[previousRow] || !this.tokenizedLines[previousRow].isComment()) && + (this.tokenizedLines[row] && this.tokenizedLines[row].isComment()) && + (this.tokenizedLines[nextRow] && this.tokenizedLines[nextRow].isComment()) + ) } buildTokenizedLinesForRows (startRow, endRow, startingStack, startingopenScopes) { @@ -327,7 +322,7 @@ class TokenizedBuffer { } buildTokenizedLineForRowWithText (row, text, currentRuleStack = this.stackForRow(row - 1), openScopes = this.openScopesForRow(row)) { - const lineEnding = this.buffer.lineEndingForRow(row); + const lineEnding = this.buffer.lineEndingForRow(row) const {tags, ruleStack} = this.grammar.tokenizeLine(text, currentRuleStack, row === 0, false) return new TokenizedLine({ openScopes, @@ -353,7 +348,7 @@ class TokenizedBuffer { text.length, this.grammar.endIdForScope(this.grammar.scopeName) ] - return this.tokenizedLines[bufferRow] = new TokenizedLine({ + this.tokenizedLines[bufferRow] = new TokenizedLine({ openScopes: [], text, tags, @@ -361,6 +356,7 @@ class TokenizedBuffer { tokenIterator: this.tokenIterator, grammar: this.grammar }) + return this.tokenizedLines[bufferRow] } } } @@ -404,7 +400,7 @@ class TokenizedBuffer { } const path = require('path') error.privateMetadataDescription = `The contents of \`${path.basename(this.buffer.getPath())}\`` - return error.privateMetadata = { + error.privateMetadata = { filePath: this.buffer.getPath(), fileContents: this.buffer.getText() } @@ -570,7 +566,7 @@ class TokenizedBuffer { } logLines (start = 0, end = this.buffer.getLastRow()) { - for (let row = start; row <= end1; row++) { + for (let row = start; row <= end; row++) { const line = this.tokenizedLines[row].text console.log(row, line, line.length) } From b1a3460ad9f2abd1f6e1bc26479c46edd5f2f0fc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 21 Sep 2017 15:52:07 -0700 Subject: [PATCH 11/81] Fix scope name in TokenizedBuffer test --- spec/tokenized-buffer-spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index 4f12ed69f..a8e01f798 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -372,7 +372,7 @@ describe('TokenizedBuffer', () => { fullyTokenize(tokenizedBuffer) expect(tokenizedBuffer.tokenizedLines[0].tokens[0]).toEqual({ value: '<', - scopes: ['text.html.ruby', 'meta.tag.block.any.html', 'punctuation.definition.tag.begin.html'] + scopes: ['text.html.ruby', 'meta.tag.block.div.html', 'punctuation.definition.tag.begin.html'] }) }) }) From 6c1356cae317313b1dad479fde8d5df3b169ad65 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Sep 2017 12:04:51 -0700 Subject: [PATCH 12/81] Move folding logic from LanguageMode to TokenizedBuffer * Restate the folding logic to not *use* the TextEditor, but instead to *return* ranges which can be folded by the editor. * Convert the LanguageMode spec to JS --- spec/language-mode-spec.coffee | 506 ----------------------------- spec/language-mode-spec.js | 503 ++++++++++++++++++++++++++++ spec/text-editor-component-spec.js | 6 +- spec/tokenized-buffer-spec.js | 311 +++++++++++++----- src/language-mode.coffee | 147 +-------- src/text-editor.coffee | 40 ++- src/tokenized-buffer.js | 142 ++++++-- 7 files changed, 902 insertions(+), 753 deletions(-) delete mode 100644 spec/language-mode-spec.coffee create mode 100644 spec/language-mode-spec.js diff --git a/spec/language-mode-spec.coffee b/spec/language-mode-spec.coffee deleted file mode 100644 index 68d0f7b09..000000000 --- a/spec/language-mode-spec.coffee +++ /dev/null @@ -1,506 +0,0 @@ -describe "LanguageMode", -> - [editor, buffer, languageMode] = [] - - afterEach -> - editor.destroy() - - describe "javascript", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.js', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe ".minIndentLevelForRowRange(startRow, endRow)", -> - it "returns the minimum indent level for the given row range", -> - expect(languageMode.minIndentLevelForRowRange(4, 7)).toBe 2 - expect(languageMode.minIndentLevelForRowRange(5, 7)).toBe 2 - expect(languageMode.minIndentLevelForRowRange(5, 6)).toBe 3 - expect(languageMode.minIndentLevelForRowRange(9, 11)).toBe 1 - expect(languageMode.minIndentLevelForRowRange(10, 10)).toBe 0 - - describe ".toggleLineCommentsForBufferRows(start, end)", -> - it "comments/uncomments lines in the given range", -> - languageMode.toggleLineCommentsForBufferRows(4, 7) - expect(buffer.lineForRow(4)).toBe " // while(items.length > 0) {" - expect(buffer.lineForRow(5)).toBe " // current = items.shift();" - expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);" - expect(buffer.lineForRow(7)).toBe " // }" - - languageMode.toggleLineCommentsForBufferRows(4, 5) - expect(buffer.lineForRow(4)).toBe " while(items.length > 0) {" - expect(buffer.lineForRow(5)).toBe " current = items.shift();" - expect(buffer.lineForRow(6)).toBe " // current < pivot ? left.push(current) : right.push(current);" - expect(buffer.lineForRow(7)).toBe " // }" - - buffer.setText('\tvar i;') - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe "\t// var i;" - - buffer.setText('var i;') - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe "// var i;" - - buffer.setText(' var i;') - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe " // var i;" - - buffer.setText(' ') - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe " // " - - buffer.setText(' a\n \n b') - languageMode.toggleLineCommentsForBufferRows(0, 2) - expect(buffer.lineForRow(0)).toBe " // a" - expect(buffer.lineForRow(1)).toBe " // " - expect(buffer.lineForRow(2)).toBe " // b" - - buffer.setText(' \n // var i;') - languageMode.toggleLineCommentsForBufferRows(0, 1) - expect(buffer.lineForRow(0)).toBe ' ' - expect(buffer.lineForRow(1)).toBe ' var i;' - - describe ".rowRangeForCodeFoldAtBufferRow(bufferRow)", -> - it "returns the start/end rows of the foldable region starting at the given row", -> - expect(languageMode.rowRangeForCodeFoldAtBufferRow(0)).toEqual [0, 12] - expect(languageMode.rowRangeForCodeFoldAtBufferRow(1)).toEqual [1, 9] - expect(languageMode.rowRangeForCodeFoldAtBufferRow(2)).toBeNull() - expect(languageMode.rowRangeForCodeFoldAtBufferRow(4)).toEqual [4, 7] - - describe ".rowRangeForCommentAtBufferRow(bufferRow)", -> - it "returns the start/end rows of the foldable comment starting at the given row", -> - buffer.setText("//this is a multi line comment\n//another line") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toEqual [0, 1] - expect(languageMode.rowRangeForCommentAtBufferRow(1)).toEqual [0, 1] - - buffer.setText("//this is a multi line comment\n//another line\n//and one more") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toEqual [0, 2] - expect(languageMode.rowRangeForCommentAtBufferRow(1)).toEqual [0, 2] - - buffer.setText("//this is a multi line comment\n\n//with an empty line") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined() - expect(languageMode.rowRangeForCommentAtBufferRow(1)).toBeUndefined() - expect(languageMode.rowRangeForCommentAtBufferRow(2)).toBeUndefined() - - buffer.setText("//this is a single line comment\n") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined() - expect(languageMode.rowRangeForCommentAtBufferRow(1)).toBeUndefined() - - buffer.setText("//this is a single line comment") - expect(languageMode.rowRangeForCommentAtBufferRow(0)).toBeUndefined() - - describe ".suggestedIndentForBufferRow", -> - it "bases indentation off of the previous non-blank line", -> - expect(languageMode.suggestedIndentForBufferRow(0)).toBe 0 - expect(languageMode.suggestedIndentForBufferRow(1)).toBe 1 - expect(languageMode.suggestedIndentForBufferRow(2)).toBe 2 - expect(languageMode.suggestedIndentForBufferRow(5)).toBe 3 - expect(languageMode.suggestedIndentForBufferRow(7)).toBe 2 - expect(languageMode.suggestedIndentForBufferRow(9)).toBe 1 - expect(languageMode.suggestedIndentForBufferRow(11)).toBe 1 - - it "does not take invisibles into account", -> - editor.update({showInvisibles: true}) - expect(languageMode.suggestedIndentForBufferRow(0)).toBe 0 - expect(languageMode.suggestedIndentForBufferRow(1)).toBe 1 - expect(languageMode.suggestedIndentForBufferRow(2)).toBe 2 - expect(languageMode.suggestedIndentForBufferRow(5)).toBe 3 - expect(languageMode.suggestedIndentForBufferRow(7)).toBe 2 - expect(languageMode.suggestedIndentForBufferRow(9)).toBe 1 - expect(languageMode.suggestedIndentForBufferRow(11)).toBe 1 - - describe "rowRangeForParagraphAtBufferRow", -> - describe "with code and comments", -> - beforeEach -> - buffer.setText ''' - var quicksort = function () { - /* Single line comment block */ - var sort = function(items) {}; - - /* - A multiline - comment is here - */ - var sort = function(items) {}; - - // A comment - // - // Multiple comment - // lines - var sort = function(items) {}; - // comment line after fn - - var nosort = function(items) { - return item; - } - - }; - ''' - - it "will limit paragraph range to comments", -> - range = languageMode.rowRangeForParagraphAtBufferRow(0) - expect(range).toEqual [[0, 0], [0, 29]] - - range = languageMode.rowRangeForParagraphAtBufferRow(10) - expect(range).toEqual [[10, 0], [10, 14]] - range = languageMode.rowRangeForParagraphAtBufferRow(11) - expect(range).toBeFalsy() - range = languageMode.rowRangeForParagraphAtBufferRow(12) - expect(range).toEqual [[12, 0], [13, 10]] - - range = languageMode.rowRangeForParagraphAtBufferRow(14) - expect(range).toEqual [[14, 0], [14, 32]] - - range = languageMode.rowRangeForParagraphAtBufferRow(15) - expect(range).toEqual [[15, 0], [15, 26]] - - range = languageMode.rowRangeForParagraphAtBufferRow(18) - expect(range).toEqual [[17, 0], [19, 3]] - - describe "coffeescript", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('coffee.coffee', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-coffee-script') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe ".toggleLineCommentsForBufferRows(start, end)", -> - it "comments/uncomments lines in the given range", -> - languageMode.toggleLineCommentsForBufferRows(4, 6) - expect(buffer.lineForRow(4)).toBe " # pivot = items.shift()" - expect(buffer.lineForRow(5)).toBe " # left = []" - expect(buffer.lineForRow(6)).toBe " # right = []" - - languageMode.toggleLineCommentsForBufferRows(4, 5) - expect(buffer.lineForRow(4)).toBe " pivot = items.shift()" - expect(buffer.lineForRow(5)).toBe " left = []" - expect(buffer.lineForRow(6)).toBe " # right = []" - - it "comments/uncomments lines when empty line", -> - languageMode.toggleLineCommentsForBufferRows(4, 7) - expect(buffer.lineForRow(4)).toBe " # pivot = items.shift()" - expect(buffer.lineForRow(5)).toBe " # left = []" - expect(buffer.lineForRow(6)).toBe " # right = []" - expect(buffer.lineForRow(7)).toBe " # " - - languageMode.toggleLineCommentsForBufferRows(4, 5) - expect(buffer.lineForRow(4)).toBe " pivot = items.shift()" - expect(buffer.lineForRow(5)).toBe " left = []" - expect(buffer.lineForRow(6)).toBe " # right = []" - expect(buffer.lineForRow(7)).toBe " # " - - describe "fold suggestion", -> - describe ".rowRangeForCodeFoldAtBufferRow(bufferRow)", -> - it "returns the start/end rows of the foldable region starting at the given row", -> - expect(languageMode.rowRangeForCodeFoldAtBufferRow(0)).toEqual [0, 20] - expect(languageMode.rowRangeForCodeFoldAtBufferRow(1)).toEqual [1, 17] - expect(languageMode.rowRangeForCodeFoldAtBufferRow(2)).toBeNull() - expect(languageMode.rowRangeForCodeFoldAtBufferRow(19)).toEqual [19, 20] - - describe "css", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('css.css', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-css') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe ".toggleLineCommentsForBufferRows(start, end)", -> - it "comments/uncomments lines in the given range", -> - languageMode.toggleLineCommentsForBufferRows(0, 1) - expect(buffer.lineForRow(0)).toBe "/*body {" - expect(buffer.lineForRow(1)).toBe " font-size: 1234px;*/" - expect(buffer.lineForRow(2)).toBe " width: 110%;" - expect(buffer.lineForRow(3)).toBe " font-weight: bold !important;" - - languageMode.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(0)).toBe "/*body {" - expect(buffer.lineForRow(1)).toBe " font-size: 1234px;*/" - expect(buffer.lineForRow(2)).toBe " /*width: 110%;*/" - expect(buffer.lineForRow(3)).toBe " font-weight: bold !important;" - - languageMode.toggleLineCommentsForBufferRows(0, 1) - expect(buffer.lineForRow(0)).toBe "body {" - expect(buffer.lineForRow(1)).toBe " font-size: 1234px;" - expect(buffer.lineForRow(2)).toBe " /*width: 110%;*/" - expect(buffer.lineForRow(3)).toBe " font-weight: bold !important;" - - it "uncomments lines with leading whitespace", -> - buffer.setTextInRange([[2, 0], [2, Infinity]], " /*width: 110%;*/") - languageMode.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(2)).toBe " width: 110%;" - - it "uncomments lines with trailing whitespace", -> - buffer.setTextInRange([[2, 0], [2, Infinity]], "/*width: 110%;*/ ") - languageMode.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(2)).toBe "width: 110%; " - - it "uncomments lines with leading and trailing whitespace", -> - buffer.setTextInRange([[2, 0], [2, Infinity]], " /*width: 110%;*/ ") - languageMode.toggleLineCommentsForBufferRows(2, 2) - expect(buffer.lineForRow(2)).toBe " width: 110%; " - - describe "less", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.less', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-less') - - waitsForPromise -> - atom.packages.activatePackage('language-css') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe "when commenting lines", -> - it "only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart`", -> - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe "// @color: #4D926F;" - - describe "xml", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.xml', autoIndent: false).then (o) -> - editor = o - editor.setText("") - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-xml') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe "when uncommenting lines", -> - it "removes the leading whitespace from the comment end pattern match", -> - languageMode.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe "test" - - describe "folding", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.js', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - it "maintains cursor buffer position when a folding/unfolding", -> - editor.setCursorBufferPosition([5, 5]) - languageMode.foldAll() - expect(editor.getCursorBufferPosition()).toEqual([5, 5]) - - describe ".unfoldAll()", -> - it "unfolds every folded line", -> - initialScreenLineCount = editor.getScreenLineCount() - languageMode.foldBufferRow(0) - languageMode.foldBufferRow(1) - expect(editor.getScreenLineCount()).toBeLessThan initialScreenLineCount - languageMode.unfoldAll() - expect(editor.getScreenLineCount()).toBe initialScreenLineCount - - describe ".foldAll()", -> - it "folds every foldable line", -> - languageMode.foldAll() - - [fold1, fold2, fold3] = languageMode.unfoldAll() - expect([fold1.start.row, fold1.end.row]).toEqual [0, 12] - expect([fold2.start.row, fold2.end.row]).toEqual [1, 9] - expect([fold3.start.row, fold3.end.row]).toEqual [4, 7] - - describe ".foldBufferRow(bufferRow)", -> - describe "when bufferRow can be folded", -> - it "creates a fold based on the syntactic region starting at the given row", -> - languageMode.foldBufferRow(1) - [fold] = languageMode.unfoldAll() - expect([fold.start.row, fold.end.row]).toEqual [1, 9] - - describe "when bufferRow can't be folded", -> - it "searches upward for the first row that begins a syntactic region containing the given buffer row (and folds it)", -> - languageMode.foldBufferRow(8) - [fold] = languageMode.unfoldAll() - expect([fold.start.row, fold.end.row]).toEqual [1, 9] - - describe "when the bufferRow is already folded", -> - it "searches upward for the first row that begins a syntactic region containing the folded row (and folds it)", -> - languageMode.foldBufferRow(2) - expect(editor.isFoldedAtBufferRow(0)).toBe(false) - expect(editor.isFoldedAtBufferRow(1)).toBe(true) - - languageMode.foldBufferRow(1) - expect(editor.isFoldedAtBufferRow(0)).toBe(true) - - describe "when the bufferRow is in a multi-line comment", -> - it "searches upward and downward for surrounding comment lines and folds them as a single fold", -> - buffer.insert([1, 0], " //this is a comment\n // and\n //more docs\n\n//second comment") - languageMode.foldBufferRow(1) - [fold] = languageMode.unfoldAll() - expect([fold.start.row, fold.end.row]).toEqual [1, 3] - - describe "when the bufferRow is a single-line comment", -> - it "searches upward for the first row that begins a syntactic region containing the folded row (and folds it)", -> - buffer.insert([1, 0], " //this is a single line comment\n") - languageMode.foldBufferRow(1) - [fold] = languageMode.unfoldAll() - expect([fold.start.row, fold.end.row]).toEqual [0, 13] - - describe ".foldAllAtIndentLevel(indentLevel)", -> - it "folds blocks of text at the given indentation level", -> - languageMode.foldAllAtIndentLevel(0) - expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" + editor.displayLayer.foldCharacter - expect(editor.getLastScreenRow()).toBe 0 - - languageMode.foldAllAtIndentLevel(1) - expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" - expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {" + editor.displayLayer.foldCharacter - expect(editor.getLastScreenRow()).toBe 4 - - languageMode.foldAllAtIndentLevel(2) - expect(editor.lineTextForScreenRow(0)).toBe "var quicksort = function () {" - expect(editor.lineTextForScreenRow(1)).toBe " var sort = function(items) {" - expect(editor.lineTextForScreenRow(2)).toBe " if (items.length <= 1) return items;" - expect(editor.getLastScreenRow()).toBe 9 - - describe "folding with comments", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample-with-comments.js', autoIndent: false).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-javascript') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe ".unfoldAll()", -> - it "unfolds every folded line", -> - initialScreenLineCount = editor.getScreenLineCount() - languageMode.foldBufferRow(0) - languageMode.foldBufferRow(5) - expect(editor.getScreenLineCount()).toBeLessThan initialScreenLineCount - languageMode.unfoldAll() - expect(editor.getScreenLineCount()).toBe initialScreenLineCount - - describe ".foldAll()", -> - it "folds every foldable line", -> - languageMode.foldAll() - - folds = languageMode.unfoldAll() - expect(folds.length).toBe 8 - expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30] - expect([folds[1].start.row, folds[1].end.row]).toEqual [1, 4] - expect([folds[2].start.row, folds[2].end.row]).toEqual [5, 27] - expect([folds[3].start.row, folds[3].end.row]).toEqual [6, 8] - expect([folds[4].start.row, folds[4].end.row]).toEqual [11, 16] - expect([folds[5].start.row, folds[5].end.row]).toEqual [17, 20] - expect([folds[6].start.row, folds[6].end.row]).toEqual [21, 22] - expect([folds[7].start.row, folds[7].end.row]).toEqual [24, 25] - - describe ".foldAllAtIndentLevel()", -> - it "folds every foldable range at a given indentLevel", -> - languageMode.foldAllAtIndentLevel(2) - - folds = languageMode.unfoldAll() - expect(folds.length).toBe 5 - expect([folds[0].start.row, folds[0].end.row]).toEqual [6, 8] - expect([folds[1].start.row, folds[1].end.row]).toEqual [11, 16] - expect([folds[2].start.row, folds[2].end.row]).toEqual [17, 20] - expect([folds[3].start.row, folds[3].end.row]).toEqual [21, 22] - expect([folds[4].start.row, folds[4].end.row]).toEqual [24, 25] - - it "does not fold anything but the indentLevel", -> - languageMode.foldAllAtIndentLevel(0) - - folds = languageMode.unfoldAll() - expect(folds.length).toBe 1 - expect([folds[0].start.row, folds[0].end.row]).toEqual [0, 30] - - describe ".isFoldableAtBufferRow(bufferRow)", -> - it "returns true if the line starts a multi-line comment", -> - expect(languageMode.isFoldableAtBufferRow(1)).toBe true - expect(languageMode.isFoldableAtBufferRow(6)).toBe true - expect(languageMode.isFoldableAtBufferRow(8)).toBe false - expect(languageMode.isFoldableAtBufferRow(11)).toBe true - expect(languageMode.isFoldableAtBufferRow(15)).toBe false - expect(languageMode.isFoldableAtBufferRow(17)).toBe true - expect(languageMode.isFoldableAtBufferRow(21)).toBe true - expect(languageMode.isFoldableAtBufferRow(24)).toBe true - expect(languageMode.isFoldableAtBufferRow(28)).toBe false - - it "returns true for lines that end with a comment and are followed by an indented line", -> - expect(languageMode.isFoldableAtBufferRow(5)).toBe true - - it "does not return true for a line in the middle of a comment that's followed by an indented line", -> - expect(languageMode.isFoldableAtBufferRow(7)).toBe false - editor.buffer.insert([8, 0], ' ') - expect(languageMode.isFoldableAtBufferRow(7)).toBe false - - describe "css", -> - beforeEach -> - waitsForPromise -> - atom.workspace.open('css.css', autoIndent: true).then (o) -> - editor = o - {buffer, languageMode} = editor - - waitsForPromise -> - atom.packages.activatePackage('language-source') - atom.packages.activatePackage('language-css') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - describe "suggestedIndentForBufferRow", -> - it "does not return negative values (regression)", -> - editor.setText('.test {\npadding: 0;\n}') - expect(editor.suggestedIndentForBufferRow(2)).toBe 0 diff --git a/spec/language-mode-spec.js b/spec/language-mode-spec.js new file mode 100644 index 000000000..34f341bfc --- /dev/null +++ b/spec/language-mode-spec.js @@ -0,0 +1,503 @@ +const dedent = require('dedent') +const {Point, Range} = require('text-buffer') +const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') + +describe('LanguageMode', () => { + let editor + + afterEach(() => { + editor.destroy() + }) + + describe('javascript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('.toggleLineCommentsForBufferRows(start, end)', () => { + it('comments/uncomments lines in the given range', () => { + editor.toggleLineCommentsForBufferRows(4, 7) + expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {') + expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();') + expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(7)).toBe(' // }') + + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();') + expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(7)).toBe(' // }') + + editor.setText('\tvar i;') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('\t// var i;') + + editor.setText('var i;') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('// var i;') + + editor.setText(' var i;') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe(' // var i;') + + editor.setText(' ') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe(' // ') + + editor.setText(' a\n \n b') + editor.toggleLineCommentsForBufferRows(0, 2) + expect(editor.lineTextForBufferRow(0)).toBe(' // a') + expect(editor.lineTextForBufferRow(1)).toBe(' // ') + expect(editor.lineTextForBufferRow(2)).toBe(' // b') + + editor.setText(' \n // var i;') + editor.toggleLineCommentsForBufferRows(0, 1) + expect(editor.lineTextForBufferRow(0)).toBe(' ') + expect(editor.lineTextForBufferRow(1)).toBe(' var i;') + }) + }) + + describe('.rowRangeForCodeFoldAtBufferRow(bufferRow)', () => { + it('returns the start/end rows of the foldable region starting at the given row', () => { + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [12, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [9, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [9, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(4, Infinity))).toEqual([[4, Infinity], [7, Infinity]]) + }) + }) + + describe('.suggestedIndentForBufferRow', () => { + it('bases indentation off of the previous non-blank line', () => { + expect(editor.suggestedIndentForBufferRow(0)).toBe(0) + expect(editor.suggestedIndentForBufferRow(1)).toBe(1) + expect(editor.suggestedIndentForBufferRow(2)).toBe(2) + expect(editor.suggestedIndentForBufferRow(5)).toBe(3) + expect(editor.suggestedIndentForBufferRow(7)).toBe(2) + expect(editor.suggestedIndentForBufferRow(9)).toBe(1) + expect(editor.suggestedIndentForBufferRow(11)).toBe(1) + }) + + it('does not take invisibles into account', () => { + editor.update({showInvisibles: true}) + expect(editor.suggestedIndentForBufferRow(0)).toBe(0) + expect(editor.suggestedIndentForBufferRow(1)).toBe(1) + expect(editor.suggestedIndentForBufferRow(2)).toBe(2) + expect(editor.suggestedIndentForBufferRow(5)).toBe(3) + expect(editor.suggestedIndentForBufferRow(7)).toBe(2) + expect(editor.suggestedIndentForBufferRow(9)).toBe(1) + expect(editor.suggestedIndentForBufferRow(11)).toBe(1) + }) + }) + + describe('rowRangeForParagraphAtBufferRow', () => { + describe('with code and comments', () => { + beforeEach(() => + editor.setText(dedent ` + var quicksort = function () { + /* Single line comment block */ + var sort = function(items) {}; + + /* + A multiline + comment is here + */ + var sort = function(items) {}; + + // A comment + // + // Multiple comment + // lines + var sort = function(items) {}; + // comment line after fn + + var nosort = function(items) { + item; + } + + }; + `) + ) + + it('will limit paragraph range to comments', () => { + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(0)).toEqual([[0, 0], [0, 29]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(1)).toEqual([[1, 0], [1, 33]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(2)).toEqual([[2, 0], [2, 32]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(3)).toBeFalsy() + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(4)).toEqual([[4, 0], [7, 4]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(5)).toEqual([[4, 0], [7, 4]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(6)).toEqual([[4, 0], [7, 4]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(7)).toEqual([[4, 0], [7, 4]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(8)).toEqual([[8, 0], [8, 32]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(9)).toBeFalsy() + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(10)).toEqual([[10, 0], [13, 10]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(11)).toEqual([[10, 0], [13, 10]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(12)).toEqual([[10, 0], [13, 10]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(14)).toEqual([[14, 0], [14, 32]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(15)).toEqual([[15, 0], [15, 26]]) + expect(editor.languageMode.rowRangeForParagraphAtBufferRow(18)).toEqual([[17, 0], [19, 3]]) + }) + }) + }) + }) + + describe('coffeescript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) + await atom.packages.activatePackage('language-coffee-script') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('.toggleLineCommentsForBufferRows(start, end)', () => { + it('comments/uncomments lines in the given range', () => { + editor.toggleLineCommentsForBufferRows(4, 6) + expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + }) + + it('comments/uncomments lines when empty line', () => { + editor.toggleLineCommentsForBufferRows(4, 7) + expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + expect(editor.lineTextForBufferRow(7)).toBe(' # ') + + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + expect(editor.lineTextForBufferRow(7)).toBe(' # ') + }) + }) + + describe('fold suggestion', () => { + describe('.rowRangeForCodeFoldAtBufferRow(bufferRow)', () => { + it('returns the start/end rows of the foldable region starting at the given row', () => { + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [20, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [17, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [17, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(19, Infinity))).toEqual([[19, Infinity], [20, Infinity]]) + }) + }) + }) + }) + + describe('css', () => { + beforeEach(async () => { + editor = await atom.workspace.open('css.css', {autoIndent: false}) + await atom.packages.activatePackage('language-css') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('.toggleLineCommentsForBufferRows(start, end)', () => { + it('comments/uncomments lines in the given range', () => { + editor.toggleLineCommentsForBufferRows(0, 1) + expect(editor.lineTextForBufferRow(0)).toBe('/*body {') + expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;*/') + expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;') + expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(0)).toBe('/*body {') + expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;*/') + expect(editor.lineTextForBufferRow(2)).toBe(' /*width: 110%;*/') + expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + + editor.toggleLineCommentsForBufferRows(0, 1) + expect(editor.lineTextForBufferRow(0)).toBe('body {') + expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;') + expect(editor.lineTextForBufferRow(2)).toBe(' /*width: 110%;*/') + expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + }) + + it('uncomments lines with leading whitespace', () => { + editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/') + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;') + }) + + it('uncomments lines with trailing whitespace', () => { + editor.setTextInBufferRange([[2, 0], [2, Infinity]], '/*width: 110%;*/ ') + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(2)).toBe('width: 110%; ') + }) + + it('uncomments lines with leading and trailing whitespace', () => { + editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/ ') + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%; ') + }) + }) + }) + + describe('less', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.less', {autoIndent: false}) + await atom.packages.activatePackage('language-less') + await atom.packages.activatePackage('language-css') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('when commenting lines', () => { + it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart`', () => { + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('// @color: #4D926F;') + }) + }) + }) + + describe('xml', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.xml', {autoIndent: false}) + editor.setText('') + await atom.packages.activatePackage('language-xml') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('when uncommenting lines', () => { + it('removes the leading whitespace from the comment end pattern match', () => { + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('test') + }) + }) + }) + + describe('folding', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + it('maintains cursor buffer position when a folding/unfolding', () => { + editor.setCursorBufferPosition([5, 5]) + editor.foldAll() + expect(editor.getCursorBufferPosition()).toEqual([5, 5]) + }) + + describe('.unfoldAll()', () => { + it('unfolds every folded line', () => { + const initialScreenLineCount = editor.getScreenLineCount() + editor.foldBufferRow(0) + editor.foldBufferRow(1) + expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount) + editor.unfoldAll() + expect(editor.getScreenLineCount()).toBe(initialScreenLineCount) + }) + }) + + describe('.foldAll()', () => { + it('folds every foldable line', () => { + editor.foldAll() + + const [fold1, fold2, fold3] = editor.unfoldAll() + expect([fold1.start.row, fold1.end.row]).toEqual([0, 12]) + expect([fold2.start.row, fold2.end.row]).toEqual([1, 9]) + expect([fold3.start.row, fold3.end.row]).toEqual([4, 7]) + }) + }) + + describe('.foldBufferRow(bufferRow)', () => { + describe('when bufferRow can be folded', () => { + it('creates a fold based on the syntactic region starting at the given row', () => { + editor.foldBufferRow(1) + const [fold] = editor.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual([1, 9]) + }) + }) + + describe("when bufferRow can't be folded", () => { + it('searches upward for the first row that begins a syntactic region containing the given buffer row (and folds it)', () => { + editor.foldBufferRow(8) + const [fold] = editor.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual([1, 9]) + }) + }) + + describe('when the bufferRow is already folded', () => { + it('searches upward for the first row that begins a syntactic region containing the folded row (and folds it)', () => { + editor.foldBufferRow(2) + expect(editor.isFoldedAtBufferRow(0)).toBe(false) + expect(editor.isFoldedAtBufferRow(1)).toBe(true) + + editor.foldBufferRow(1) + expect(editor.isFoldedAtBufferRow(0)).toBe(true) + }) + }) + + describe('when the bufferRow is in a multi-line comment', () => { + it('searches upward and downward for surrounding comment lines and folds them as a single fold', () => { + editor.buffer.insert([1, 0], ' //this is a comment\n // and\n //more docs\n\n//second comment') + editor.foldBufferRow(1) + const [fold] = editor.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual([1, 3]) + }) + }) + + describe('when the bufferRow is a single-line comment', () => { + it('searches upward for the first row that begins a syntactic region containing the folded row (and folds it)', () => { + editor.buffer.insert([1, 0], ' //this is a single line comment\n') + editor.foldBufferRow(1) + const [fold] = editor.unfoldAll() + expect([fold.start.row, fold.end.row]).toEqual([0, 13]) + }) + }) + }) + + describe('.foldAllAtIndentLevel(indentLevel)', () => { + it('folds blocks of text at the given indentation level', () => { + editor.foldAllAtIndentLevel(0) + expect(editor.lineTextForScreenRow(0)).toBe(`var quicksort = function () {${editor.displayLayer.foldCharacter}`) + expect(editor.getLastScreenRow()).toBe(0) + + editor.foldAllAtIndentLevel(1) + expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {') + expect(editor.lineTextForScreenRow(1)).toBe(` var sort = function(items) {${editor.displayLayer.foldCharacter}`) + expect(editor.getLastScreenRow()).toBe(4) + + editor.foldAllAtIndentLevel(2) + expect(editor.lineTextForScreenRow(0)).toBe('var quicksort = function () {') + expect(editor.lineTextForScreenRow(1)).toBe(' var sort = function(items) {') + expect(editor.lineTextForScreenRow(2)).toBe(' if (items.length <= 1) return items;') + expect(editor.getLastScreenRow()).toBe(9) + }) + }) + }) + + describe('folding with comments', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('.unfoldAll()', () => { + it('unfolds every folded line', () => { + const initialScreenLineCount = editor.getScreenLineCount() + editor.foldBufferRow(0) + editor.foldBufferRow(5) + expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount) + editor.unfoldAll() + expect(editor.getScreenLineCount()).toBe(initialScreenLineCount) + }) + }) + + describe('.foldAll()', () => { + it('folds every foldable line', () => { + editor.foldAll() + + const folds = editor.unfoldAll() + expect(folds.length).toBe(8) + expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30]) + expect([folds[1].start.row, folds[1].end.row]).toEqual([1, 4]) + expect([folds[2].start.row, folds[2].end.row]).toEqual([5, 27]) + expect([folds[3].start.row, folds[3].end.row]).toEqual([6, 8]) + expect([folds[4].start.row, folds[4].end.row]).toEqual([11, 16]) + expect([folds[5].start.row, folds[5].end.row]).toEqual([17, 20]) + expect([folds[6].start.row, folds[6].end.row]).toEqual([21, 22]) + expect([folds[7].start.row, folds[7].end.row]).toEqual([24, 25]) + }) + }) + + describe('.foldAllAtIndentLevel()', () => { + it('folds every foldable range at a given indentLevel', () => { + editor.foldAllAtIndentLevel(2) + + const folds = editor.unfoldAll() + expect(folds.length).toBe(5) + expect([folds[0].start.row, folds[0].end.row]).toEqual([6, 8]) + expect([folds[1].start.row, folds[1].end.row]).toEqual([11, 16]) + expect([folds[2].start.row, folds[2].end.row]).toEqual([17, 20]) + expect([folds[3].start.row, folds[3].end.row]).toEqual([21, 22]) + expect([folds[4].start.row, folds[4].end.row]).toEqual([24, 25]) + }) + + it('does not fold anything but the indentLevel', () => { + editor.foldAllAtIndentLevel(0) + + const folds = editor.unfoldAll() + expect(folds.length).toBe(1) + expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30]) + }) + }) + + describe('.isFoldableAtBufferRow(bufferRow)', () => { + it('returns true if the line starts a multi-line comment', () => { + expect(editor.isFoldableAtBufferRow(1)).toBe(true) + expect(editor.isFoldableAtBufferRow(6)).toBe(true) + expect(editor.isFoldableAtBufferRow(8)).toBe(false) + expect(editor.isFoldableAtBufferRow(11)).toBe(true) + expect(editor.isFoldableAtBufferRow(15)).toBe(false) + expect(editor.isFoldableAtBufferRow(17)).toBe(true) + expect(editor.isFoldableAtBufferRow(21)).toBe(true) + expect(editor.isFoldableAtBufferRow(24)).toBe(true) + expect(editor.isFoldableAtBufferRow(28)).toBe(false) + }) + + it('returns true for lines that end with a comment and are followed by an indented line', () => { + expect(editor.isFoldableAtBufferRow(5)).toBe(true) + }) + + it("does not return true for a line in the middle of a comment that's followed by an indented line", () => { + expect(editor.isFoldableAtBufferRow(7)).toBe(false) + editor.buffer.insert([8, 0], ' ') + expect(editor.isFoldableAtBufferRow(7)).toBe(false) + }) + }) + }) + + describe('css', () => { + beforeEach(async () => { + editor = await atom.workspace.open('css.css', {autoIndent: true}) + await atom.packages.activatePackage('language-source') + await atom.packages.activatePackage('language-css') + }) + + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + }) + + describe('suggestedIndentForBufferRow', () => { + it('does not return negative values (regression)', () => { + editor.setText('.test {\npadding: 0;\n}') + expect(editor.suggestedIndentForBufferRow(2)).toBe(0) + }) + }) + }) +}) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 3fd40cdad..82764c438 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -3343,9 +3343,9 @@ describe('TextEditorComponent', () => { await component.getNextUpdatePromise() expect(editor.isFoldedAtScreenRow(5)).toBe(true) - target = element.querySelectorAll('.line-number')[6].querySelector('.icon-right') - component.didMouseDownOnLineNumberGutter({target, button: 0, clientY: clientTopForLine(component, 5)}) - expect(editor.isFoldedAtScreenRow(5)).toBe(false) + target = element.querySelectorAll('.line-number')[4].querySelector('.icon-right') + component.didMouseDownOnLineNumberGutter({target, button: 0, clientY: clientTopForLine(component, 4)}) + expect(editor.isFoldedAtScreenRow(4)).toBe(false) }) it('autoscrolls when dragging near the top or bottom of the gutter', async () => { diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index a8e01f798..134a1a0b1 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -1,8 +1,9 @@ const NullGrammar = require('../src/null-grammar') const TokenizedBuffer = require('../src/tokenized-buffer') const TextBuffer = require('text-buffer') -const {Point} = TextBuffer +const {Point, Range} = TextBuffer const _ = require('underscore-plus') +const dedent = require('dedent') const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') describe('TokenizedBuffer', () => { @@ -12,7 +13,6 @@ describe('TokenizedBuffer', () => { // enable async tokenization TokenizedBuffer.prototype.chunkSize = 5 jasmine.unspy(TokenizedBuffer.prototype, 'tokenizeInBackground') - await atom.packages.activatePackage('language-javascript') }) @@ -528,78 +528,6 @@ describe('TokenizedBuffer', () => { }) }) // } - describe('.isFoldableAtRow(row)', () => { - beforeEach(() => { - buffer = atom.project.bufferForPathSync('sample.js') - buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n') - buffer.insert([0, 0], '// multi-line\n// comment\n// block\n') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - }) - - it('includes the first line of multi-line comments', () => { - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) // because of indent - expect(tokenizedBuffer.isFoldableAtRow(13)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(14)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(15)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(16)).toBe(false) - - buffer.insert([0, Infinity], '\n') - - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(false) - - buffer.undo() - - expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) - }) // because of indent - - it('includes non-comment lines that precede an increase in indentation', () => { - buffer.insert([2, 0], ' ') // commented lines preceding an indent aren't foldable - - expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(4)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(5)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - - buffer.insert([7, 0], ' ') - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - - buffer.undo() - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - - buffer.insert([7, 0], ' \n x\n') - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - - buffer.insert([9, 0], ' ') - - expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) - expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) - expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) - }) - }) - describe('.tokenizedLineForRow(row)', () => { it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => { buffer = atom.project.bufferForPathSync('sample.js') @@ -750,4 +678,239 @@ describe('TokenizedBuffer', () => { }) }) }) + + describe('.isFoldableAtRow(row)', () => { + beforeEach(() => { + buffer = atom.project.bufferForPathSync('sample.js') + buffer.insert([10, 0], ' // multi-line\n // comment\n // block\n') + buffer.insert([0, 0], '// multi-line\n// comment\n// block\n') + tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) + fullyTokenize(tokenizedBuffer) + }) + + it('includes the first line of multi-line comments', () => { + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) // because of indent + expect(tokenizedBuffer.isFoldableAtRow(13)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(14)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(15)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(16)).toBe(false) + + buffer.insert([0, Infinity], '\n') + + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(false) + + buffer.undo() + + expect(tokenizedBuffer.isFoldableAtRow(0)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) + }) // because of indent + + it('includes non-comment lines that precede an increase in indentation', () => { + buffer.insert([2, 0], ' ') // commented lines preceding an indent aren't foldable + + expect(tokenizedBuffer.isFoldableAtRow(1)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(2)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(3)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(4)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(5)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([7, 0], ' ') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.undo() + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([7, 0], ' \n x\n') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + + buffer.insert([9, 0], ' ') + + expect(tokenizedBuffer.isFoldableAtRow(6)).toBe(true) + expect(tokenizedBuffer.isFoldableAtRow(7)).toBe(false) + expect(tokenizedBuffer.isFoldableAtRow(8)).toBe(false) + }) + }) + + describe('.getFoldableRangesAtIndentLevel', () => { + it('returns the ranges that can be folded at the given indent level', () => { + buffer = new TextBuffer(dedent ` + if (a) { + b(); + if (c) { + d() + if (e) { + f() + } + g() + } + h() + } + i() + if (j) { + k() + } + `) + + tokenizedBuffer = new TokenizedBuffer({buffer}) + + expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2))).toBe(dedent ` + if (a) {⋯ + } + i() + if (j) {⋯ + } + `) + + expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(1, 2))).toBe(dedent ` + if (a) { + b(); + if (c) {⋯ + } + h() + } + i() + if (j) { + k() + } + `) + + expect(simulateFold(tokenizedBuffer.getFoldableRangesAtIndentLevel(2, 2))).toBe(dedent ` + if (a) { + b(); + if (c) { + d() + if (e) {⋯ + } + g() + } + h() + } + i() + if (j) { + k() + } + `) + }) + }) + + describe('.getFoldableRanges', () => { + it('returns the ranges that can be folded', () => { + buffer = new TextBuffer(dedent ` + if (a) { + b(); + if (c) { + d() + if (e) { + f() + } + g() + } + h() + } + i() + if (j) { + k() + } + `) + + tokenizedBuffer = new TokenizedBuffer({buffer}) + + expect(tokenizedBuffer.getFoldableRanges(2).map(r => r.toString())).toEqual([ + ...tokenizedBuffer.getFoldableRangesAtIndentLevel(0, 2), + ...tokenizedBuffer.getFoldableRangesAtIndentLevel(1, 2), + ...tokenizedBuffer.getFoldableRangesAtIndentLevel(2, 2), + ].sort((a, b) => (a.start.row - b.start.row) || (a.end.row - b.end.row)).map(r => r.toString())) + }) + }) + + describe('.getFoldableRangeContainingPoint', () => { + it('returns the range for the smallest fold that contains the given range', () => { + buffer = new TextBuffer(dedent ` + if (a) { + b(); + if (c) { + d() + if (e) { + f() + } + g() + } + h() + } + i() + if (j) { + k() + } + `) + + tokenizedBuffer = new TokenizedBuffer({buffer}) + + expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 5), 2)).toBeNull() + + let range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, 10), 2) + expect(simulateFold([range])).toBe(dedent ` + if (a) {⋯ + } + i() + if (j) { + k() + } + `) + + range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity), 2) + expect(simulateFold([range])).toBe(dedent ` + if (a) {⋯ + } + i() + if (j) { + k() + } + `) + + range = tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, 20), 2) + expect(simulateFold([range])).toBe(dedent ` + if (a) { + b(); + if (c) {⋯ + } + h() + } + i() + if (j) { + k() + } + `) + }) + }) + + function simulateFold (ranges) { + buffer.transact(() => { + for (const range of ranges.reverse()) { + buffer.setTextInRange(range, '⋯') + } + }) + let text = buffer.getText() + buffer.undo() + return text + } }) diff --git a/src/language-mode.coffee b/src/language-mode.coffee index 1839f1c59..953d328b2 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -1,9 +1,11 @@ -{Range} = require 'text-buffer' +{Range, Point} = require 'text-buffer' _ = require 'underscore-plus' {OnigRegExp} = require 'oniguruma' ScopeDescriptor = require './scope-descriptor' NullGrammar = require './null-grammar' +NON_WHITESPACE_REGEX = /\S/ + module.exports = class LanguageMode # Sets up a `LanguageMode` for the given {TextEditor}. @@ -90,148 +92,28 @@ class LanguageMode buffer.setTextInRange([[row, 0], [row, indentString.length]], indentString + commentStartString) return - # Folds all the foldable lines in the buffer. - foldAll: -> - @unfoldAll() - foldedRowRanges = {} - for currentRow in [0..@buffer.getLastRow()] by 1 - rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] - continue unless startRow? - continue if foldedRowRanges[rowRange] - - @editor.foldBufferRowRange(startRow, endRow) - foldedRowRanges[rowRange] = true - return - - # Unfolds all the foldable lines in the buffer. - unfoldAll: -> - @editor.displayLayer.destroyAllFolds() - - # Fold all comment and code blocks at a given indentLevel - # - # indentLevel - A {Number} indicating indentLevel; 0 based. - foldAllAtIndentLevel: (indentLevel) -> - @unfoldAll() - foldedRowRanges = {} - for currentRow in [0..@buffer.getLastRow()] by 1 - rowRange = [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] - continue unless startRow? - continue if foldedRowRanges[rowRange] - - # assumption: startRow will always be the min indent level for the entire range - if @editor.indentationForBufferRow(startRow) is indentLevel - @editor.foldBufferRowRange(startRow, endRow) - foldedRowRanges[rowRange] = true - return - - # Given a buffer row, creates a fold at it. - # - # bufferRow - A {Number} indicating the buffer row - # - # Returns the new {Fold}. - foldBufferRow: (bufferRow) -> - for currentRow in [bufferRow..0] by -1 - [startRow, endRow] = @rowRangeForFoldAtBufferRow(currentRow) ? [] - continue unless startRow? and startRow <= bufferRow <= endRow - unless @editor.isFoldedAtBufferRow(startRow) - return @editor.foldBufferRowRange(startRow, endRow) - - # Find the row range for a fold at a given bufferRow. Will handle comments - # and code. - # - # bufferRow - A {Number} indicating the buffer row - # - # Returns an {Array} of the [startRow, endRow]. Returns null if no range. - rowRangeForFoldAtBufferRow: (bufferRow) -> - rowRange = @rowRangeForCommentAtBufferRow(bufferRow) - rowRange ?= @rowRangeForCodeFoldAtBufferRow(bufferRow) - rowRange - - rowRangeForCommentAtBufferRow: (bufferRow) -> - return unless @editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment() - - startRow = bufferRow - endRow = bufferRow - - if bufferRow > 0 - for currentRow in [bufferRow-1..0] by -1 - break unless @editor.tokenizedBuffer.tokenizedLines[currentRow]?.isComment() - startRow = currentRow - - if bufferRow < @buffer.getLastRow() - for currentRow in [bufferRow+1..@buffer.getLastRow()] by 1 - break unless @editor.tokenizedBuffer.tokenizedLines[currentRow]?.isComment() - endRow = currentRow - - return [startRow, endRow] if startRow isnt endRow - - rowRangeForCodeFoldAtBufferRow: (bufferRow) -> - return null unless @isFoldableAtBufferRow(bufferRow) - - startIndentLevel = @editor.indentationForBufferRow(bufferRow) - scopeDescriptor = @editor.scopeDescriptorForBufferPosition([bufferRow, 0]) - for row in [(bufferRow + 1)..@editor.getLastBufferRow()] by 1 - continue if @editor.isBufferRowBlank(row) - indentation = @editor.indentationForBufferRow(row) - if indentation <= startIndentLevel - includeRowInFold = indentation is startIndentLevel and @foldEndRegexForScopeDescriptor(scopeDescriptor)?.searchSync(@editor.lineTextForBufferRow(row)) - foldEndRow = row if includeRowInFold - break - - foldEndRow = row - - [bufferRow, foldEndRow] - - isFoldableAtBufferRow: (bufferRow) -> - @editor.tokenizedBuffer.isFoldableAtRow(bufferRow) - - # Returns a {Boolean} indicating whether the line at the given buffer - # row is a comment. - isLineCommentedAtBufferRow: (bufferRow) -> - return false unless 0 <= bufferRow <= @editor.getLastBufferRow() - @editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment() ? false - # Find a row range for a 'paragraph' around specified bufferRow. A paragraph # is a block of text bounded by and empty line or a block of text that is not # the same type (comments next to source code). rowRangeForParagraphAtBufferRow: (bufferRow) -> - scope = @editor.scopeDescriptorForBufferPosition([bufferRow, 0]) - commentStrings = @editor.getCommentStrings(scope) - commentStartRegex = null - if commentStrings?.commentStartString? and not commentStrings.commentEndString? - commentStartRegexString = _.escapeRegExp(commentStrings.commentStartString).replace(/(\s+)$/, '(?:$1)?') - commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})") + return unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(bufferRow)) - filterCommentStart = (line) -> - if commentStartRegex? - matches = commentStartRegex.searchSync(line) - line = line.substring(matches[0].end) if matches?.length - line - - return unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(bufferRow))) - - if @isLineCommentedAtBufferRow(bufferRow) - isOriginalRowComment = true - range = @rowRangeForCommentAtBufferRow(bufferRow) - [firstRow, lastRow] = range or [bufferRow, bufferRow] - else - isOriginalRowComment = false - [firstRow, lastRow] = [0, @editor.getLastBufferRow()-1] + isCommented = @editor.tokenizedBuffer.isRowCommented(bufferRow) startRow = bufferRow - while startRow > firstRow - break if @isLineCommentedAtBufferRow(startRow - 1) isnt isOriginalRowComment - break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(startRow - 1))) + while startRow > 0 + break unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(startRow - 1)) + break if @editor.tokenizedBuffer.isRowCommented(startRow - 1) isnt isCommented startRow-- endRow = bufferRow - lastRow = @editor.getLastBufferRow() - while endRow < lastRow - break if @isLineCommentedAtBufferRow(endRow + 1) isnt isOriginalRowComment - break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(endRow + 1))) + rowCount = @editor.getLineCount() + while endRow < rowCount + break unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(endRow + 1)) + break if @editor.tokenizedBuffer.isRowCommented(endRow + 1) isnt isCommented endRow++ - new Range([startRow, 0], [endRow, @editor.lineTextForBufferRow(endRow).length]) + new Range(new Point(startRow, 0), new Point(endRow, @editor.buffer.lineLengthForRow(endRow))) # Given a buffer row, this returns a suggested indentation level. # @@ -345,6 +227,3 @@ class LanguageMode decreaseNextIndentRegexForScopeDescriptor: (scopeDescriptor) -> @cacheRegex(@editor.getDecreaseNextIndentPattern(scopeDescriptor)) - - foldEndRegexForScopeDescriptor: (scopeDescriptor) -> - @cacheRegex(@editor.getFoldEndPattern(scopeDescriptor)) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index a84f6f631..117589750 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3311,13 +3311,14 @@ class TextEditor extends Model # indentation level up to the nearest following row with a lower indentation # level. foldCurrentRow: -> - bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row - @foldBufferRow(bufferRow) + {row} = @getCursorBufferPosition() + range = @tokenizedBuffer.getFoldableRangeContainingPoint(Point(row, Infinity)) + @displayLayer.foldBufferRange(range) # Essential: Unfold the most recent cursor's row by one level. unfoldCurrentRow: -> - bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row - @unfoldBufferRow(bufferRow) + position = @getCursorBufferPosition() + @displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position)) # Essential: Fold the given row in buffer coordinates based on its indentation # level. @@ -3327,13 +3328,26 @@ class TextEditor extends Model # # * `bufferRow` A {Number}. foldBufferRow: (bufferRow) -> - @languageMode.foldBufferRow(bufferRow) + position = Point(bufferRow, Infinity) + loop + foldableRange = @tokenizedBuffer.getFoldableRangeContainingPoint(position, @getTabLength()) + if foldableRange + existingFolds = @displayLayer.foldsIntersectingBufferRange(Range(foldableRange.start, foldableRange.start)) + if existingFolds.length is 0 + @displayLayer.foldBufferRange(foldableRange) + else + firstExistingFoldRange = @displayLayer.bufferRangeForFold(existingFolds[0]) + if firstExistingFoldRange.start.isLessThan(position) + position = Point(firstExistingFoldRange.start.row, 0) + continue + return # Essential: Unfold all folds containing the given row in buffer coordinates. # # * `bufferRow` A {Number} unfoldBufferRow: (bufferRow) -> - @displayLayer.destroyFoldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))) + position = Point(bufferRow, Infinity) + @displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position)) # Extended: For each selection, fold the rows it intersects. foldSelectedLines: -> @@ -3342,18 +3356,25 @@ class TextEditor extends Model # Extended: Fold all foldable lines. foldAll: -> - @languageMode.foldAll() + @displayLayer.destroyAllFolds() + for range in @tokenizedBuffer.getFoldableRanges(@getTabLength()) + @displayLayer.foldBufferRange(range) + return # Extended: Unfold all existing folds. unfoldAll: -> - @languageMode.unfoldAll() + result = @displayLayer.destroyAllFolds() @scrollToCursorPosition() + result # Extended: Fold all foldable lines at the given indent level. # # * `level` A {Number}. foldAllAtIndentLevel: (level) -> - @languageMode.foldAllAtIndentLevel(level) + @displayLayer.destroyAllFolds() + for range in @tokenizedBuffer.getFoldableRangesAtIndentLevel(level, @getTabLength()) + @displayLayer.foldBufferRange(range) + return # Extended: Determine whether the given row in buffer coordinates is foldable. # @@ -3547,6 +3568,7 @@ class TextEditor extends Model # for specific syntactic scopes. See the `ScopedSettingsDelegate` in # `text-editor-registry.js` for an example implementation. setScopedSettingsDelegate: (@scopedSettingsDelegate) -> + @tokenizedBuffer.scopedSettingsDelegate = this.scopedSettingsDelegate # Experimental: Retrieve the {Object} that provides the editor with settings # for specific syntactic scopes. diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index fbb9de77f..fd5691740 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -6,8 +6,11 @@ const TokenIterator = require('./token-iterator') const ScopeDescriptor = require('./scope-descriptor') const TokenizedBufferIterator = require('./tokenized-buffer-iterator') const NullGrammar = require('./null-grammar') +const {OnigRegExp} = require('oniguruma') const {toFirstMateScopeId} = require('./first-mate-helpers') +const NON_WHITESPACE_REGEX = /\S/ + let nextId = 0 const prefixedScopes = new Map() @@ -26,6 +29,7 @@ class TokenizedBuffer { this.emitter = new Emitter() this.disposables = new CompositeDisposable() this.tokenIterator = new TokenIterator(this) + this.regexesByPattern = {} this.alive = true this.id = params.id != null ? params.id : nextId++ @@ -265,33 +269,7 @@ class TokenizedBuffer { } isFoldableAtRow (row) { - return this.isFoldableCodeAtRow(row) || this.isFoldableCommentAtRow(row) - } - - // Returns a {Boolean} indicating whether the given buffer row starts - // a a foldable row range due to the code's indentation patterns. - isFoldableCodeAtRow (row) { - if (row >= 0 && row <= this.buffer.getLastRow()) { - const nextRow = this.buffer.nextNonBlankRow(row) - const tokenizedLine = this.tokenizedLines[row] - if (this.buffer.isRowBlank(row) || (tokenizedLine && tokenizedLine.isComment()) || nextRow == null) { - return false - } else { - return this.indentLevelForRow(nextRow) > this.indentLevelForRow(row) - } - } else { - return false - } - } - - isFoldableCommentAtRow (row) { - const previousRow = row - 1 - const nextRow = row + 1 - return ( - (!this.tokenizedLines[previousRow] || !this.tokenizedLines[previousRow].isComment()) && - (this.tokenizedLines[row] && this.tokenizedLines[row].isComment()) && - (this.tokenizedLines[nextRow] && this.tokenizedLines[nextRow].isComment()) - ) + return this.endRowForFoldAtRow(row, 1) != null } buildTokenizedLinesForRows (startRow, endRow, startingStack, startingopenScopes) { @@ -554,6 +532,116 @@ class TokenizedBuffer { return new Range(new Point(position.row, startColumn), new Point(position.row, endColumn)) } + isRowCommented (row) { + return this.tokenizedLines[row] && this.tokenizedLines[row].isComment() + } + + getFoldableRangeContainingPoint (point, tabLength) { + if (point.column >= this.buffer.lineLengthForRow(point.row)) { + const endRow = this.endRowForFoldAtRow(point.row, tabLength) + if (endRow != null) { + return Range(Point(point.row, Infinity), Point(endRow, Infinity)) + } + } + + for (let row = point.row - 1; row >= 0; row--) { + const endRow = this.endRowForFoldAtRow(row, tabLength) + if (endRow != null && endRow > point.row) { + return Range(Point(row, Infinity), Point(endRow, Infinity)) + } + } + return null + } + + getFoldableRangesAtIndentLevel (indentLevel, tabLength) { + const result = [] + let row = 0 + const lineCount = this.buffer.getLineCount() + while (row < lineCount) { + if (this.indentLevelForLine(this.buffer.lineForRow(row), tabLength) === indentLevel) { + const endRow = this.endRowForFoldAtRow(row, tabLength) + if (endRow != null) { + result.push(Range(Point(row, Infinity), Point(endRow, Infinity))) + row = endRow + 1 + continue + } + } + row++ + } + return result + } + + getFoldableRanges (tabLength) { + const result = [] + let row = 0 + const lineCount = this.buffer.getLineCount() + while (row < lineCount) { + const endRow = this.endRowForFoldAtRow(row, tabLength) + if (endRow != null) { + result.push(Range(Point(row, Infinity), Point(endRow, Infinity))) + } + row++ + } + return result + } + + endRowForFoldAtRow (row, tabLength) { + if (this.isRowCommented(row)) { + return this.endRowForCommentFoldAtRow(row) + } else { + return this.endRowForCodeFoldAtRow(row, tabLength) + } + } + + endRowForCommentFoldAtRow (row) { + if (this.isRowCommented(row - 1)) return + + let endRow + for (let nextRow = row + 1, end = this.buffer.getLineCount(); nextRow < end; nextRow++) { + if (!this.isRowCommented(nextRow)) break + endRow = nextRow + } + + return endRow + } + + endRowForCodeFoldAtRow (row, tabLength) { + let foldEndRow + const line = this.buffer.lineForRow(row) + if (!NON_WHITESPACE_REGEX.test(line)) return + const startIndentLevel = this.indentLevelForLine(line, tabLength) + const scopeDescriptor = this.scopeDescriptorForPosition([row, 0]) + const foldEndRegex = this.foldEndRegexForScopeDescriptor(scopeDescriptor) + for (let nextRow = row + 1, end = this.buffer.getLineCount(); nextRow < end; nextRow++) { + const line = this.buffer.lineForRow(nextRow) + if (!NON_WHITESPACE_REGEX.test(line)) continue + const indentation = this.indentLevelForLine(line, tabLength) + if (indentation < startIndentLevel) { + break + } else if (indentation === startIndentLevel) { + if (foldEndRegex && foldEndRegex.searchSync(line)) foldEndRow = nextRow + break + } + foldEndRow = nextRow + } + return foldEndRow + } + + foldEndRegexForScopeDescriptor (scopes) { + if (this.scopedSettingsDelegate) { + return this.regexForPattern(this.scopedSettingsDelegate.getFoldEndPattern(scopes)) + } + } + + regexForPattern (pattern) { + if (pattern) { + if (!this.regexesByPattern[pattern]) { + this.regexesByPattern[pattern] = new OnigRegExp(pattern) + } + return this.regexesByPattern[pattern] + } + } + // Gets the row number of the last line. // // Returns a {Number}. From aed7c1c060ed9585047a596831daa7250e4fa6df Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 22 Sep 2017 21:36:41 +0200 Subject: [PATCH 13/81] :arrow_up: language-mustache@0.14.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ffc655280..0e17b69cb 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "language-json": "0.19.1", "language-less": "0.33.0", "language-make": "0.22.3", - "language-mustache": "0.14.2", + "language-mustache": "0.14.3", "language-objective-c": "0.15.1", "language-perl": "0.37.0", "language-php": "0.42.0", From 0884546d3cee83202a8600f919fc3da5e766d7e5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Sep 2017 14:33:02 -0700 Subject: [PATCH 14/81] Move everything but auto-indent out of LanguageMode --- spec/language-mode-spec.js | 436 +++++++++++++--------------------- spec/text-editor-spec.coffee | 52 ++++ spec/tokenized-buffer-spec.js | 24 ++ src/cursor.coffee | 2 +- src/language-mode.coffee | 106 --------- src/text-editor.coffee | 92 ++++++- 6 files changed, 333 insertions(+), 379 deletions(-) diff --git a/spec/language-mode-spec.js b/spec/language-mode-spec.js index 34f341bfc..cbb9377cb 100644 --- a/spec/language-mode-spec.js +++ b/spec/language-mode-spec.js @@ -9,70 +9,13 @@ describe('LanguageMode', () => { editor.destroy() }) - describe('javascript', () => { - beforeEach(async () => { - editor = await atom.workspace.open('sample.js', {autoIndent: false}) - await atom.packages.activatePackage('language-javascript') - }) - - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) - - describe('.toggleLineCommentsForBufferRows(start, end)', () => { - it('comments/uncomments lines in the given range', () => { - editor.toggleLineCommentsForBufferRows(4, 7) - expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {') - expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();') - expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') - expect(editor.lineTextForBufferRow(7)).toBe(' // }') - - editor.toggleLineCommentsForBufferRows(4, 5) - expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') - expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();') - expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') - expect(editor.lineTextForBufferRow(7)).toBe(' // }') - - editor.setText('\tvar i;') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('\t// var i;') - - editor.setText('var i;') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('// var i;') - - editor.setText(' var i;') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe(' // var i;') - - editor.setText(' ') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe(' // ') - - editor.setText(' a\n \n b') - editor.toggleLineCommentsForBufferRows(0, 2) - expect(editor.lineTextForBufferRow(0)).toBe(' // a') - expect(editor.lineTextForBufferRow(1)).toBe(' // ') - expect(editor.lineTextForBufferRow(2)).toBe(' // b') - - editor.setText(' \n // var i;') - editor.toggleLineCommentsForBufferRows(0, 1) - expect(editor.lineTextForBufferRow(0)).toBe(' ') - expect(editor.lineTextForBufferRow(1)).toBe(' var i;') + describe('.suggestedIndentForBufferRow', () => { + describe('javascript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') }) - }) - describe('.rowRangeForCodeFoldAtBufferRow(bufferRow)', () => { - it('returns the start/end rows of the foldable region starting at the given row', () => { - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [12, Infinity]]) - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [9, Infinity]]) - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [9, Infinity]]) - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(4, Infinity))).toEqual([[4, Infinity], [7, Infinity]]) - }) - }) - - describe('.suggestedIndentForBufferRow', () => { it('bases indentation off of the previous non-blank line', () => { expect(editor.suggestedIndentForBufferRow(0)).toBe(0) expect(editor.suggestedIndentForBufferRow(1)).toBe(1) @@ -95,120 +38,53 @@ describe('LanguageMode', () => { }) }) - describe('rowRangeForParagraphAtBufferRow', () => { - describe('with code and comments', () => { - beforeEach(() => - editor.setText(dedent ` - var quicksort = function () { - /* Single line comment block */ - var sort = function(items) {}; + describe('css', () => { + beforeEach(async () => { + editor = await atom.workspace.open('css.css', {autoIndent: true}) + await atom.packages.activatePackage('language-source') + await atom.packages.activatePackage('language-css') + }) - /* - A multiline - comment is here - */ - var sort = function(items) {}; - - // A comment - // - // Multiple comment - // lines - var sort = function(items) {}; - // comment line after fn - - var nosort = function(items) { - item; - } - - }; - `) - ) - - it('will limit paragraph range to comments', () => { - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(0)).toEqual([[0, 0], [0, 29]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(1)).toEqual([[1, 0], [1, 33]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(2)).toEqual([[2, 0], [2, 32]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(3)).toBeFalsy() - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(4)).toEqual([[4, 0], [7, 4]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(5)).toEqual([[4, 0], [7, 4]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(6)).toEqual([[4, 0], [7, 4]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(7)).toEqual([[4, 0], [7, 4]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(8)).toEqual([[8, 0], [8, 32]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(9)).toBeFalsy() - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(10)).toEqual([[10, 0], [13, 10]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(11)).toEqual([[10, 0], [13, 10]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(12)).toEqual([[10, 0], [13, 10]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(14)).toEqual([[14, 0], [14, 32]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(15)).toEqual([[15, 0], [15, 26]]) - expect(editor.languageMode.rowRangeForParagraphAtBufferRow(18)).toEqual([[17, 0], [19, 3]]) - }) + it('does not return negative values (regression)', () => { + editor.setText('.test {\npadding: 0;\n}') + expect(editor.suggestedIndentForBufferRow(2)).toBe(0) }) }) }) - describe('coffeescript', () => { - beforeEach(async () => { - editor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) - await atom.packages.activatePackage('language-coffee-script') - }) - - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) - - describe('.toggleLineCommentsForBufferRows(start, end)', () => { - it('comments/uncomments lines in the given range', () => { - editor.toggleLineCommentsForBufferRows(4, 6) - expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') - expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') - expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') - - editor.toggleLineCommentsForBufferRows(4, 5) - expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') - expect(editor.lineTextForBufferRow(5)).toBe(' left = []') - expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + describe('.toggleLineCommentsForBufferRows', () => { + describe('xml', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.xml', {autoIndent: false}) + editor.setText('') + await atom.packages.activatePackage('language-xml') }) - it('comments/uncomments lines when empty line', () => { - editor.toggleLineCommentsForBufferRows(4, 7) - expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') - expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') - expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') - expect(editor.lineTextForBufferRow(7)).toBe(' # ') - - editor.toggleLineCommentsForBufferRows(4, 5) - expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') - expect(editor.lineTextForBufferRow(5)).toBe(' left = []') - expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') - expect(editor.lineTextForBufferRow(7)).toBe(' # ') + it('removes the leading whitespace from the comment end pattern match when uncommenting lines', () => { + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('test') }) }) - describe('fold suggestion', () => { - describe('.rowRangeForCodeFoldAtBufferRow(bufferRow)', () => { - it('returns the start/end rows of the foldable region starting at the given row', () => { - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [20, Infinity]]) - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [17, Infinity]]) - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [17, Infinity]]) - expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(19, Infinity))).toEqual([[19, Infinity], [20, Infinity]]) - }) + describe('less', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.less', {autoIndent: false}) + await atom.packages.activatePackage('language-less') + await atom.packages.activatePackage('language-css') + }) + + it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart` when commenting lines', () => { + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('// @color: #4D926F;') }) }) - }) - describe('css', () => { - beforeEach(async () => { - editor = await atom.workspace.open('css.css', {autoIndent: false}) - await atom.packages.activatePackage('language-css') - }) + describe('css', () => { + beforeEach(async () => { + editor = await atom.workspace.open('css.css', {autoIndent: false}) + await atom.packages.activatePackage('language-css') + }) - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) - - describe('.toggleLineCommentsForBufferRows(start, end)', () => { it('comments/uncomments lines in the given range', () => { editor.toggleLineCommentsForBufferRows(0, 1) expect(editor.lineTextForBufferRow(0)).toBe('/*body {') @@ -247,67 +123,107 @@ describe('LanguageMode', () => { expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%; ') }) }) - }) - describe('less', () => { - beforeEach(async () => { - editor = await atom.workspace.open('sample.less', {autoIndent: false}) - await atom.packages.activatePackage('language-less') - await atom.packages.activatePackage('language-css') - }) + describe('coffeescript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) + await atom.packages.activatePackage('language-coffee-script') + }) - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) + it('comments/uncomments lines in the given range', () => { + editor.toggleLineCommentsForBufferRows(4, 6) + expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') - describe('when commenting lines', () => { - it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart`', () => { - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('// @color: #4D926F;') + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + }) + + it('comments/uncomments empty lines', () => { + editor.toggleLineCommentsForBufferRows(4, 7) + expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + expect(editor.lineTextForBufferRow(7)).toBe(' # ') + + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + expect(editor.lineTextForBufferRow(7)).toBe(' # ') }) }) - }) - describe('xml', () => { - beforeEach(async () => { - editor = await atom.workspace.open('sample.xml', {autoIndent: false}) - editor.setText('') - await atom.packages.activatePackage('language-xml') - }) + describe('javascript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) + it('comments/uncomments lines in the given range', () => { + editor.toggleLineCommentsForBufferRows(4, 7) + expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {') + expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();') + expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(7)).toBe(' // }') - describe('when uncommenting lines', () => { - it('removes the leading whitespace from the comment end pattern match', () => { + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();') + console.log(JSON.stringify(editor.lineTextForBufferRow(5))); + return + expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(7)).toBe(' // }') + + editor.setText('\tvar i;') editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('test') + expect(editor.lineTextForBufferRow(0)).toBe('\t// var i;') + + editor.setText('var i;') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('// var i;') + + editor.setText(' var i;') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe(' // var i;') + + editor.setText(' ') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe(' // ') + + editor.setText(' a\n \n b') + editor.toggleLineCommentsForBufferRows(0, 2) + expect(editor.lineTextForBufferRow(0)).toBe(' // a') + expect(editor.lineTextForBufferRow(1)).toBe(' // ') + expect(editor.lineTextForBufferRow(2)).toBe(' // b') + + editor.setText(' \n // var i;') + editor.toggleLineCommentsForBufferRows(0, 1) + expect(editor.lineTextForBufferRow(0)).toBe(' ') + expect(editor.lineTextForBufferRow(1)).toBe(' var i;') }) }) }) describe('folding', () => { beforeEach(async () => { - editor = await atom.workspace.open('sample.js', {autoIndent: false}) await atom.packages.activatePackage('language-javascript') }) - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) - - it('maintains cursor buffer position when a folding/unfolding', () => { + it('maintains cursor buffer position when a folding/unfolding', async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) editor.setCursorBufferPosition([5, 5]) editor.foldAll() expect(editor.getCursorBufferPosition()).toEqual([5, 5]) }) describe('.unfoldAll()', () => { - it('unfolds every folded line', () => { + it('unfolds every folded line', async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + const initialScreenLineCount = editor.getScreenLineCount() editor.foldBufferRow(0) editor.foldBufferRow(1) @@ -315,20 +231,52 @@ describe('LanguageMode', () => { editor.unfoldAll() expect(editor.getScreenLineCount()).toBe(initialScreenLineCount) }) + + it('unfolds every folded line with comments', async () => { + editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false}) + + const initialScreenLineCount = editor.getScreenLineCount() + editor.foldBufferRow(0) + editor.foldBufferRow(5) + expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount) + editor.unfoldAll() + expect(editor.getScreenLineCount()).toBe(initialScreenLineCount) + }) }) describe('.foldAll()', () => { - it('folds every foldable line', () => { - editor.foldAll() + it('folds every foldable line', async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + editor.foldAll() const [fold1, fold2, fold3] = editor.unfoldAll() expect([fold1.start.row, fold1.end.row]).toEqual([0, 12]) expect([fold2.start.row, fold2.end.row]).toEqual([1, 9]) expect([fold3.start.row, fold3.end.row]).toEqual([4, 7]) }) + + it('works with multi-line comments', async () => { + editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false}) + + editor.foldAll() + const folds = editor.unfoldAll() + expect(folds.length).toBe(8) + expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30]) + expect([folds[1].start.row, folds[1].end.row]).toEqual([1, 4]) + expect([folds[2].start.row, folds[2].end.row]).toEqual([5, 27]) + expect([folds[3].start.row, folds[3].end.row]).toEqual([6, 8]) + expect([folds[4].start.row, folds[4].end.row]).toEqual([11, 16]) + expect([folds[5].start.row, folds[5].end.row]).toEqual([17, 20]) + expect([folds[6].start.row, folds[6].end.row]).toEqual([21, 22]) + expect([folds[7].start.row, folds[7].end.row]).toEqual([24, 25]) + }) }) describe('.foldBufferRow(bufferRow)', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js') + }) + describe('when bufferRow can be folded', () => { it('creates a fold based on the syntactic region starting at the given row', () => { editor.foldBufferRow(1) @@ -376,7 +324,9 @@ describe('LanguageMode', () => { }) describe('.foldAllAtIndentLevel(indentLevel)', () => { - it('folds blocks of text at the given indentation level', () => { + it('folds blocks of text at the given indentation level', async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + editor.foldAllAtIndentLevel(0) expect(editor.lineTextForScreenRow(0)).toBe(`var quicksort = function () {${editor.displayLayer.foldCharacter}`) expect(editor.getLastScreenRow()).toBe(0) @@ -392,52 +342,11 @@ describe('LanguageMode', () => { expect(editor.lineTextForScreenRow(2)).toBe(' if (items.length <= 1) return items;') expect(editor.getLastScreenRow()).toBe(9) }) - }) - }) - describe('folding with comments', () => { - beforeEach(async () => { - editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false}) - await atom.packages.activatePackage('language-javascript') - }) + it('folds every foldable range at a given indentLevel', async () => { + editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false}) - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) - - describe('.unfoldAll()', () => { - it('unfolds every folded line', () => { - const initialScreenLineCount = editor.getScreenLineCount() - editor.foldBufferRow(0) - editor.foldBufferRow(5) - expect(editor.getScreenLineCount()).toBeLessThan(initialScreenLineCount) - editor.unfoldAll() - expect(editor.getScreenLineCount()).toBe(initialScreenLineCount) - }) - }) - - describe('.foldAll()', () => { - it('folds every foldable line', () => { - editor.foldAll() - - const folds = editor.unfoldAll() - expect(folds.length).toBe(8) - expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30]) - expect([folds[1].start.row, folds[1].end.row]).toEqual([1, 4]) - expect([folds[2].start.row, folds[2].end.row]).toEqual([5, 27]) - expect([folds[3].start.row, folds[3].end.row]).toEqual([6, 8]) - expect([folds[4].start.row, folds[4].end.row]).toEqual([11, 16]) - expect([folds[5].start.row, folds[5].end.row]).toEqual([17, 20]) - expect([folds[6].start.row, folds[6].end.row]).toEqual([21, 22]) - expect([folds[7].start.row, folds[7].end.row]).toEqual([24, 25]) - }) - }) - - describe('.foldAllAtIndentLevel()', () => { - it('folds every foldable range at a given indentLevel', () => { editor.foldAllAtIndentLevel(2) - const folds = editor.unfoldAll() expect(folds.length).toBe(5) expect([folds[0].start.row, folds[0].end.row]).toEqual([6, 8]) @@ -447,9 +356,10 @@ describe('LanguageMode', () => { expect([folds[4].start.row, folds[4].end.row]).toEqual([24, 25]) }) - it('does not fold anything but the indentLevel', () => { - editor.foldAllAtIndentLevel(0) + it('does not fold anything but the indentLevel', async () => { + editor = await atom.workspace.open('sample-with-comments.js', {autoIndent: false}) + editor.foldAllAtIndentLevel(0) const folds = editor.unfoldAll() expect(folds.length).toBe(1) expect([folds[0].start.row, folds[0].end.row]).toEqual([0, 30]) @@ -457,7 +367,9 @@ describe('LanguageMode', () => { }) describe('.isFoldableAtBufferRow(bufferRow)', () => { - it('returns true if the line starts a multi-line comment', () => { + it('returns true if the line starts a multi-line comment', async () => { + editor = await atom.workspace.open('sample-with-comments.js') + expect(editor.isFoldableAtBufferRow(1)).toBe(true) expect(editor.isFoldableAtBufferRow(6)).toBe(true) expect(editor.isFoldableAtBufferRow(8)).toBe(false) @@ -469,35 +381,19 @@ describe('LanguageMode', () => { expect(editor.isFoldableAtBufferRow(28)).toBe(false) }) - it('returns true for lines that end with a comment and are followed by an indented line', () => { + it('returns true for lines that end with a comment and are followed by an indented line', async () => { + editor = await atom.workspace.open('sample-with-comments.js') + expect(editor.isFoldableAtBufferRow(5)).toBe(true) }) - it("does not return true for a line in the middle of a comment that's followed by an indented line", () => { + it("does not return true for a line in the middle of a comment that's followed by an indented line", async () => { + editor = await atom.workspace.open('sample-with-comments.js') + expect(editor.isFoldableAtBufferRow(7)).toBe(false) editor.buffer.insert([8, 0], ' ') expect(editor.isFoldableAtBufferRow(7)).toBe(false) }) }) }) - - describe('css', () => { - beforeEach(async () => { - editor = await atom.workspace.open('css.css', {autoIndent: true}) - await atom.packages.activatePackage('language-source') - await atom.packages.activatePackage('language-css') - }) - - afterEach(async () => { - await atom.packages.deactivatePackages() - atom.packages.unloadPackages() - }) - - describe('suggestedIndentForBufferRow', () => { - it('does not return negative values (regression)', () => { - editor.setText('.test {\npadding: 0;\n}') - expect(editor.suggestedIndentForBufferRow(2)).toBe(0) - }) - }) - }) }) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index cb70d030c..efe3bf048 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -1168,6 +1168,58 @@ describe "TextEditor", -> editor.setCursorBufferPosition([3, 1]) expect(editor.getCurrentParagraphBufferRange()).toBeUndefined() + it 'will limit paragraph range to comments', -> + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + + runs -> + editor.setGrammar(atom.grammars.grammarForScopeName('source.js')) + editor.setText(""" + var quicksort = function () { + /* Single line comment block */ + var sort = function(items) {}; + + /* + A multiline + comment is here + */ + var sort = function(items) {}; + + // A comment + // + // Multiple comment + // lines + var sort = function(items) {}; + // comment line after fn + + var nosort = function(items) { + item; + } + + }; + """) + + paragraphBufferRangeForRow = (row) -> + editor.setCursorBufferPosition([row, 0]) + editor.getLastCursor().getCurrentParagraphBufferRange() + + expect(paragraphBufferRangeForRow(0)).toEqual([[0, 0], [0, 29]]) + expect(paragraphBufferRangeForRow(1)).toEqual([[1, 0], [1, 33]]) + expect(paragraphBufferRangeForRow(2)).toEqual([[2, 0], [2, 32]]) + expect(paragraphBufferRangeForRow(3)).toBeFalsy() + expect(paragraphBufferRangeForRow(4)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(5)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(6)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(7)).toEqual([[4, 0], [7, 4]]) + expect(paragraphBufferRangeForRow(8)).toEqual([[8, 0], [8, 32]]) + expect(paragraphBufferRangeForRow(9)).toBeFalsy() + expect(paragraphBufferRangeForRow(10)).toEqual([[10, 0], [13, 10]]) + expect(paragraphBufferRangeForRow(11)).toEqual([[10, 0], [13, 10]]) + expect(paragraphBufferRangeForRow(12)).toEqual([[10, 0], [13, 10]]) + expect(paragraphBufferRangeForRow(14)).toEqual([[14, 0], [14, 32]]) + expect(paragraphBufferRangeForRow(15)).toEqual([[15, 0], [15, 26]]) + expect(paragraphBufferRangeForRow(18)).toEqual([[17, 0], [19, 3]]) + describe "getCursorAtScreenPosition(screenPosition)", -> it "returns the cursor at the given screenPosition", -> cursor1 = editor.addCursorAtScreenPosition([0, 2]) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index 134a1a0b1..55db55fe9 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -901,6 +901,30 @@ describe('TokenizedBuffer', () => { } `) }) + + it('works for coffee-script', async () => { + const editor = await atom.workspace.open('coffee.coffee') + await atom.packages.activatePackage('language-coffee-script') + buffer = editor.buffer + tokenizedBuffer = editor.tokenizedBuffer + + expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [20, Infinity]]) + expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [17, Infinity]]) + expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [17, Infinity]]) + expect(tokenizedBuffer.getFoldableRangeContainingPoint(Point(19, Infinity))).toEqual([[19, Infinity], [20, Infinity]]) + }) + + it('works for javascript', async () => { + const editor = await atom.workspace.open('sample.js') + await atom.packages.activatePackage('language-javascript') + buffer = editor.buffer + tokenizedBuffer = editor.tokenizedBuffer + + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(0, Infinity))).toEqual([[0, Infinity], [12, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(1, Infinity))).toEqual([[1, Infinity], [9, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(2, Infinity))).toEqual([[1, Infinity], [9, Infinity]]) + expect(editor.tokenizedBuffer.getFoldableRangeContainingPoint(Point(4, Infinity))).toEqual([[4, Infinity], [7, Infinity]]) + }) }) function simulateFold (ranges) { diff --git a/src/cursor.coffee b/src/cursor.coffee index 6273b0276..2acbfecf4 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -543,7 +543,7 @@ class Cursor extends Model # # Returns a {Range}. getCurrentParagraphBufferRange: -> - @editor.languageMode.rowRangeForParagraphAtBufferRow(@getBufferRow()) + @editor.rowRangeForParagraphAtBufferRow(@getBufferRow()) # Public: Returns the characters preceding the cursor in the current word. getCurrentWordPrefix: -> diff --git a/src/language-mode.coffee b/src/language-mode.coffee index 953d328b2..6d306a38a 100644 --- a/src/language-mode.coffee +++ b/src/language-mode.coffee @@ -4,8 +4,6 @@ _ = require 'underscore-plus' ScopeDescriptor = require './scope-descriptor' NullGrammar = require './null-grammar' -NON_WHITESPACE_REGEX = /\S/ - module.exports = class LanguageMode # Sets up a `LanguageMode` for the given {TextEditor}. @@ -15,106 +13,6 @@ class LanguageMode {@buffer} = @editor @regexesByPattern = {} - destroy: -> - - toggleLineCommentForBufferRow: (row) -> - @toggleLineCommentsForBufferRows(row, row) - - # Wraps the lines between two rows in comments. - # - # If the language doesn't have comment, nothing happens. - # - # startRow - The row {Number} to start at - # endRow - The row {Number} to end at - toggleLineCommentsForBufferRows: (start, end) -> - scope = @editor.scopeDescriptorForBufferPosition([start, 0]) - commentStrings = @editor.getCommentStrings(scope) - return unless commentStrings?.commentStartString - {commentStartString, commentEndString} = commentStrings - - buffer = @editor.buffer - commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?') - commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})") - - if commentEndString - shouldUncomment = commentStartRegex.testSync(buffer.lineForRow(start)) - if shouldUncomment - commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?') - commentEndRegex = new OnigRegExp("(#{commentEndRegexString})(\\s*)$") - startMatch = commentStartRegex.searchSync(buffer.lineForRow(start)) - endMatch = commentEndRegex.searchSync(buffer.lineForRow(end)) - if startMatch and endMatch - buffer.transact -> - columnStart = startMatch[1].length - columnEnd = columnStart + startMatch[2].length - buffer.setTextInRange([[start, columnStart], [start, columnEnd]], "") - - endLength = buffer.lineLengthForRow(end) - endMatch[2].length - endColumn = endLength - endMatch[1].length - buffer.setTextInRange([[end, endColumn], [end, endLength]], "") - else - buffer.transact -> - indentLength = buffer.lineForRow(start).match(/^\s*/)?[0].length ? 0 - buffer.insert([start, indentLength], commentStartString) - buffer.insert([end, buffer.lineLengthForRow(end)], commentEndString) - else - allBlank = true - allBlankOrCommented = true - - for row in [start..end] by 1 - line = buffer.lineForRow(row) - blank = line?.match(/^\s*$/) - - allBlank = false unless blank - allBlankOrCommented = false unless blank or commentStartRegex.testSync(line) - - shouldUncomment = allBlankOrCommented and not allBlank - - if shouldUncomment - for row in [start..end] by 1 - if match = commentStartRegex.searchSync(buffer.lineForRow(row)) - columnStart = match[1].length - columnEnd = columnStart + match[2].length - buffer.setTextInRange([[row, columnStart], [row, columnEnd]], "") - else - if start is end - indent = @editor.indentationForBufferRow(start) - else - indent = @minIndentLevelForRowRange(start, end) - indentString = @editor.buildIndentString(indent) - tabLength = @editor.getTabLength() - indentRegex = new RegExp("(\t|[ ]{#{tabLength}}){#{Math.floor(indent)}}") - for row in [start..end] by 1 - line = buffer.lineForRow(row) - if indentLength = line.match(indentRegex)?[0].length - buffer.insert([row, indentLength], commentStartString) - else - buffer.setTextInRange([[row, 0], [row, indentString.length]], indentString + commentStartString) - return - - # Find a row range for a 'paragraph' around specified bufferRow. A paragraph - # is a block of text bounded by and empty line or a block of text that is not - # the same type (comments next to source code). - rowRangeForParagraphAtBufferRow: (bufferRow) -> - return unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(bufferRow)) - - isCommented = @editor.tokenizedBuffer.isRowCommented(bufferRow) - - startRow = bufferRow - while startRow > 0 - break unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(startRow - 1)) - break if @editor.tokenizedBuffer.isRowCommented(startRow - 1) isnt isCommented - startRow-- - - endRow = bufferRow - rowCount = @editor.getLineCount() - while endRow < rowCount - break unless NON_WHITESPACE_REGEX.test(@editor.lineTextForBufferRow(endRow + 1)) - break if @editor.tokenizedBuffer.isRowCommented(endRow + 1) isnt isCommented - endRow++ - - new Range(new Point(startRow, 0), new Point(endRow, @editor.buffer.lineLengthForRow(endRow))) - # Given a buffer row, this returns a suggested indentation level. # # The indentation level provided is based on the current {LanguageMode}. @@ -166,10 +64,6 @@ class LanguageMode # endRow - The row {Number} to end at # # Returns a {Number} of the indent level of the block of lines. - minIndentLevelForRowRange: (startRow, endRow) -> - indents = (@editor.indentationForBufferRow(row) for row in [startRow..endRow] by 1 when not @editor.isBufferRowBlank(row)) - indents = [0] unless indents.length - Math.min(indents...) # Indents all the rows between two buffer row numbers. # diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 117589750..d75276f06 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3,6 +3,7 @@ path = require 'path' fs = require 'fs-plus' Grim = require 'grim' {CompositeDisposable, Disposable, Emitter} = require 'event-kit' +{OnigRegExp} = require 'oniguruma' {Point, Range} = TextBuffer = require 'text-buffer' LanguageMode = require './language-mode' DecorationManager = require './decoration-manager' @@ -16,6 +17,7 @@ TextEditorComponent = null TextEditorElement = null {isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils' +NON_WHITESPACE_REGEXP = /\S/ ZERO_WIDTH_NBSP = '\ufeff' # Essential: This class represents all essential editing state for a single @@ -482,7 +484,6 @@ class TextEditor extends Model @tokenizedBuffer.destroy() selection.destroy() for selection in @selections.slice() @buffer.release() - @languageMode.destroy() @gutterContainer.destroy() @emitter.emit 'did-destroy' @emitter.clear() @@ -3882,4 +3883,91 @@ class TextEditor extends Model toggleLineCommentForBufferRow: (row) -> @languageMode.toggleLineCommentsForBufferRow(row) - toggleLineCommentsForBufferRows: (start, end) -> @languageMode.toggleLineCommentsForBufferRows(start, end) + toggleLineCommentsForBufferRows: (start, end) -> + scope = @scopeDescriptorForBufferPosition([start, 0]) + commentStrings = @getCommentStrings(scope) + return unless commentStrings?.commentStartString + {commentStartString, commentEndString} = commentStrings + + buffer = @buffer + commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?') + commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})") + + if commentEndString + shouldUncomment = commentStartRegex.testSync(buffer.lineForRow(start)) + if shouldUncomment + commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?') + commentEndRegex = new OnigRegExp("(#{commentEndRegexString})(\\s*)$") + startMatch = commentStartRegex.searchSync(buffer.lineForRow(start)) + endMatch = commentEndRegex.searchSync(buffer.lineForRow(end)) + if startMatch and endMatch + buffer.transact -> + columnStart = startMatch[1].length + columnEnd = columnStart + startMatch[2].length + buffer.setTextInRange([[start, columnStart], [start, columnEnd]], "") + + endLength = buffer.lineLengthForRow(end) - endMatch[2].length + endColumn = endLength - endMatch[1].length + buffer.setTextInRange([[end, endColumn], [end, endLength]], "") + else + buffer.transact -> + indentLength = buffer.lineForRow(start).match(/^\s*/)?[0].length ? 0 + buffer.insert([start, indentLength], commentStartString) + buffer.insert([end, buffer.lineLengthForRow(end)], commentEndString) + else + allBlank = true + allBlankOrCommented = true + + for row in [start..end] by 1 + line = buffer.lineForRow(row) + blank = line?.match(/^\s*$/) + + allBlank = false unless blank + allBlankOrCommented = false unless blank or commentStartRegex.testSync(line) + + shouldUncomment = allBlankOrCommented and not allBlank + + if shouldUncomment + for row in [start..end] by 1 + if match = commentStartRegex.searchSync(buffer.lineForRow(row)) + columnStart = match[1].length + columnEnd = columnStart + match[2].length + buffer.setTextInRange([[row, columnStart], [row, columnEnd]], "") + else + indents = [] + for row in [start..end] by 1 + unless @isBufferRowBlank(row) + indents.push(@indentationForBufferRow(start)) + indents.push(0) if indents.length is 0 + indent = Math.min(indents...) + + indentString = @buildIndentString(indent) + tabLength = @getTabLength() + indentRegex = new RegExp("(\t|[ ]{#{tabLength}}){#{Math.floor(indent)}}") + for row in [start..end] by 1 + line = buffer.lineForRow(row) + if indentLength = line.match(indentRegex)?[0].length + buffer.insert([row, indentLength], commentStartString) + else + buffer.setTextInRange([[row, 0], [row, indentString.length]], indentString + commentStartString) + return + + rowRangeForParagraphAtBufferRow: (bufferRow) -> + return unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(bufferRow)) + + isCommented = @tokenizedBuffer.isRowCommented(bufferRow) + + startRow = bufferRow + while startRow > 0 + break unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(startRow - 1)) + break if @tokenizedBuffer.isRowCommented(startRow - 1) isnt isCommented + startRow-- + + endRow = bufferRow + rowCount = @getLineCount() + while endRow < rowCount + break unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(endRow + 1)) + break if @tokenizedBuffer.isRowCommented(endRow + 1) isnt isCommented + endRow++ + + new Range(new Point(startRow, 0), new Point(endRow, @buffer.lineLengthForRow(endRow))) From 8be9375508cfdd7614d6b575894ebee350b941b7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Sep 2017 14:38:54 -0700 Subject: [PATCH 15/81] Remove unnecessary TokenizedBuffer methods --- src/tokenized-buffer.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index fd5691740..f7e96e88c 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -178,7 +178,7 @@ class TokenizedBuffer { while (this.firstInvalidRow() != null && rowsRemaining > 0) { var endRow, filledRegion const startRow = this.invalidRows.shift() - const lastRow = this.getLastRow() + const lastRow = this.buffer.getLastRow() if (startRow > lastRow) continue let row = startRow @@ -398,7 +398,7 @@ class TokenizedBuffer { if (line === '') { let nextRow = bufferRow + 1 - const lineCount = this.getLineCount() + const lineCount = this.buffer.getLineCount() while (nextRow < lineCount) { const nextLine = this.buffer.lineForRow(nextRow) if (nextLine !== '') { @@ -642,17 +642,6 @@ class TokenizedBuffer { } } - // Gets the row number of the last line. - // - // Returns a {Number}. - getLastRow () { - return this.buffer.getLastRow() - } - - getLineCount () { - return this.buffer.getLineCount() - } - logLines (start = 0, end = this.buffer.getLastRow()) { for (let row = start; row <= end; row++) { const line = this.tokenizedLines[row].text From f762bf9548895aec4155188bdb7f3f042a9ae618 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Fri, 22 Sep 2017 23:39:04 +0200 Subject: [PATCH 16/81] :arrow_up: first-mate@7.0.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e17b69cb..3ecdd6fd9 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "etch": "^0.12.6", "event-kit": "^2.4.0", "find-parent-dir": "^0.3.0", - "first-mate": "7.0.7", + "first-mate": "7.0.8", "focus-trap": "^2.3.0", "fs-admin": "^0.1.6", "fs-plus": "^3.0.1", From 62e94f7b96fdc225f2ae7ddd9472f4c857efb504 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Sep 2017 14:40:34 -0700 Subject: [PATCH 17/81] Rename language-mode-spec.js to text-editor-spec.js This gets the ball rolling toward converting the text editor specs to JS --- spec/{language-mode-spec.js => text-editor-spec.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename spec/{language-mode-spec.js => text-editor-spec.js} (99%) diff --git a/spec/language-mode-spec.js b/spec/text-editor-spec.js similarity index 99% rename from spec/language-mode-spec.js rename to spec/text-editor-spec.js index cbb9377cb..e72417aca 100644 --- a/spec/language-mode-spec.js +++ b/spec/text-editor-spec.js @@ -2,7 +2,7 @@ const dedent = require('dedent') const {Point, Range} = require('text-buffer') const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') -describe('LanguageMode', () => { +describe('TextEditor', () => { let editor afterEach(() => { From 67ec6fb4cf6466825a068ae1b907b4a1efa695bc Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 23 Sep 2017 00:06:17 +0200 Subject: [PATCH 18/81] Revert ":arrow_up: first-mate@7.0.8" This reverts commit f762bf9548895aec4155188bdb7f3f042a9ae618. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ecdd6fd9..0e17b69cb 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "etch": "^0.12.6", "event-kit": "^2.4.0", "find-parent-dir": "^0.3.0", - "first-mate": "7.0.8", + "first-mate": "7.0.7", "focus-trap": "^2.3.0", "fs-admin": "^0.1.6", "fs-plus": "^3.0.1", From 274a699272fa1e894655d71e1eae88bb4e909c62 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Sep 2017 16:20:10 -0700 Subject: [PATCH 19/81] Remove unused method TokenizedBuffer.indentLevelForRow --- spec/tokenized-buffer-spec.js | 85 ----------------------------------- src/tokenized-buffer.js | 32 ------------- 2 files changed, 117 deletions(-) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index 55db55fe9..c0bd29b50 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -443,91 +443,6 @@ describe('TokenizedBuffer', () => { }) }) - describe('.indentLevelForRow(row)', () => { - beforeEach(() => { - buffer = atom.project.bufferForPathSync('sample.js') - tokenizedBuffer = new TokenizedBuffer({buffer, grammar: atom.grammars.grammarForScopeName('source.js'), tabLength: 2}) - fullyTokenize(tokenizedBuffer) - }) - - describe('when the line is non-empty', () => { - it('has an indent level based on the leading whitespace on the line', () => { - expect(tokenizedBuffer.indentLevelForRow(0)).toBe(0) - expect(tokenizedBuffer.indentLevelForRow(1)).toBe(1) - expect(tokenizedBuffer.indentLevelForRow(2)).toBe(2) - buffer.insert([2, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(2)).toBe(2.5) - }) - }) - - describe('when the line is empty', () => { - it('assumes the indentation level of the first non-empty line below or above if one exists', () => { - buffer.insert([12, 0], ' ') - buffer.insert([12, Infinity], '\n\n') - expect(tokenizedBuffer.indentLevelForRow(13)).toBe(2) - expect(tokenizedBuffer.indentLevelForRow(14)).toBe(2) - - buffer.insert([1, Infinity], '\n\n') - expect(tokenizedBuffer.indentLevelForRow(2)).toBe(2) - expect(tokenizedBuffer.indentLevelForRow(3)).toBe(2) - - buffer.setText('\n\n\n') - expect(tokenizedBuffer.indentLevelForRow(1)).toBe(0) - }) - }) - - describe('when the changed lines are surrounded by whitespace-only lines', () => { - it('updates the indentLevel of empty lines that precede the change', () => { - expect(tokenizedBuffer.indentLevelForRow(12)).toBe(0) - - buffer.insert([12, 0], '\n') - buffer.insert([13, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(12)).toBe(1) - }) - - it('updates empty line indent guides when the empty line is the last line', () => { - buffer.insert([12, 2], '\n') - - // The newline and the tab need to be in two different operations to surface the bug - buffer.insert([12, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(13)).toBe(1) - - buffer.insert([12, 0], ' ') - expect(tokenizedBuffer.indentLevelForRow(13)).toBe(2) - expect(tokenizedBuffer.tokenizedLines[14]).not.toBeDefined() - }) - - it('updates the indentLevel of empty lines surrounding a change that inserts lines', () => { - buffer.insert([7, 0], '\n\n') - buffer.insert([5, 0], '\n\n') - expect(tokenizedBuffer.indentLevelForRow(5)).toBe(3) - expect(tokenizedBuffer.indentLevelForRow(6)).toBe(3) - expect(tokenizedBuffer.indentLevelForRow(9)).toBe(3) - expect(tokenizedBuffer.indentLevelForRow(10)).toBe(3) - expect(tokenizedBuffer.indentLevelForRow(11)).toBe(2) - - buffer.setTextInRange([[7, 0], [8, 65]], ' one\n two\n three\n four') - expect(tokenizedBuffer.indentLevelForRow(5)).toBe(4) - expect(tokenizedBuffer.indentLevelForRow(6)).toBe(4) - expect(tokenizedBuffer.indentLevelForRow(11)).toBe(4) - expect(tokenizedBuffer.indentLevelForRow(12)).toBe(4) - expect(tokenizedBuffer.indentLevelForRow(13)).toBe(2) - }) - - it('updates the indentLevel of empty lines surrounding a change that removes lines', () => { - buffer.insert([7, 0], '\n\n') - buffer.insert([5, 0], '\n\n') - buffer.setTextInRange([[7, 0], [8, 65]], ' ok') - expect(tokenizedBuffer.indentLevelForRow(5)).toBe(2) - expect(tokenizedBuffer.indentLevelForRow(6)).toBe(2) - expect(tokenizedBuffer.indentLevelForRow(7)).toBe(2) // new text - expect(tokenizedBuffer.indentLevelForRow(8)).toBe(2) - expect(tokenizedBuffer.indentLevelForRow(9)).toBe(2) - expect(tokenizedBuffer.indentLevelForRow(10)).toBe(2) - }) - }) - }) // } - describe('.tokenizedLineForRow(row)', () => { it("returns the tokenized line for a row, or a placeholder line if it hasn't been tokenized yet", () => { buffer = atom.project.bufferForPathSync('sample.js') diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index f7e96e88c..f51baa950 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -392,38 +392,6 @@ class TokenizedBuffer { return scopes } - indentLevelForRow (bufferRow) { - const line = this.buffer.lineForRow(bufferRow) - let indentLevel = 0 - - if (line === '') { - let nextRow = bufferRow + 1 - const lineCount = this.buffer.getLineCount() - while (nextRow < lineCount) { - const nextLine = this.buffer.lineForRow(nextRow) - if (nextLine !== '') { - indentLevel = Math.ceil(this.indentLevelForLine(nextLine)) - break - } - nextRow++ - } - - let previousRow = bufferRow - 1 - while (previousRow >= 0) { - const previousLine = this.buffer.lineForRow(previousRow) - if (previousLine !== '') { - indentLevel = Math.max(Math.ceil(this.indentLevelForLine(previousLine)), indentLevel) - break - } - previousRow-- - } - - return indentLevel - } else { - return this.indentLevelForLine(line) - } - } - indentLevelForLine (line, tabLength = this.tabLength) { let indentLength = 0 for (let i = 0, {length} = line; i < length; i++) { From e14aa842ff60f64b270c8a7fcece4be7d3c7867f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Sep 2017 16:21:06 -0700 Subject: [PATCH 20/81] Move auto-indent code to TokenizedBuffer, :fire: LanguageMode --- src/language-mode.coffee | 123 --------------------------------------- src/selection.coffee | 2 +- src/text-editor.coffee | 46 ++++++++------- src/tokenized-buffer.js | 117 +++++++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 146 deletions(-) delete mode 100644 src/language-mode.coffee diff --git a/src/language-mode.coffee b/src/language-mode.coffee deleted file mode 100644 index 6d306a38a..000000000 --- a/src/language-mode.coffee +++ /dev/null @@ -1,123 +0,0 @@ -{Range, Point} = require 'text-buffer' -_ = require 'underscore-plus' -{OnigRegExp} = require 'oniguruma' -ScopeDescriptor = require './scope-descriptor' -NullGrammar = require './null-grammar' - -module.exports = -class LanguageMode - # Sets up a `LanguageMode` for the given {TextEditor}. - # - # editor - The {TextEditor} to associate with - constructor: (@editor) -> - {@buffer} = @editor - @regexesByPattern = {} - - # Given a buffer row, this returns a suggested indentation level. - # - # The indentation level provided is based on the current {LanguageMode}. - # - # bufferRow - A {Number} indicating the buffer row - # - # Returns a {Number}. - suggestedIndentForBufferRow: (bufferRow, options) -> - line = @buffer.lineForRow(bufferRow) - tokenizedLine = @editor.tokenizedBuffer.tokenizedLineForRow(bufferRow) - @suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options) - - suggestedIndentForLineAtBufferRow: (bufferRow, line, options) -> - tokenizedLine = @editor.tokenizedBuffer.buildTokenizedLineForRowWithText(bufferRow, line) - @suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options) - - suggestedIndentForTokenizedLineAtBufferRow: (bufferRow, line, tokenizedLine, options) -> - iterator = tokenizedLine.getTokenIterator() - iterator.next() - scopeDescriptor = new ScopeDescriptor(scopes: iterator.getScopes()) - - increaseIndentRegex = @increaseIndentRegexForScopeDescriptor(scopeDescriptor) - decreaseIndentRegex = @decreaseIndentRegexForScopeDescriptor(scopeDescriptor) - decreaseNextIndentRegex = @decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor) - - if options?.skipBlankLines ? true - precedingRow = @buffer.previousNonBlankRow(bufferRow) - return 0 unless precedingRow? - else - precedingRow = bufferRow - 1 - return 0 if precedingRow < 0 - - desiredIndentLevel = @editor.indentationForBufferRow(precedingRow) - return desiredIndentLevel unless increaseIndentRegex - - unless @editor.isBufferRowCommented(precedingRow) - precedingLine = @buffer.lineForRow(precedingRow) - desiredIndentLevel += 1 if increaseIndentRegex?.testSync(precedingLine) - desiredIndentLevel -= 1 if decreaseNextIndentRegex?.testSync(precedingLine) - - unless @buffer.isRowBlank(precedingRow) - desiredIndentLevel -= 1 if decreaseIndentRegex?.testSync(line) - - Math.max(desiredIndentLevel, 0) - - # Calculate a minimum indent level for a range of lines excluding empty lines. - # - # startRow - The row {Number} to start at - # endRow - The row {Number} to end at - # - # Returns a {Number} of the indent level of the block of lines. - - # Indents all the rows between two buffer row numbers. - # - # startRow - The row {Number} to start at - # endRow - The row {Number} to end at - autoIndentBufferRows: (startRow, endRow) -> - @autoIndentBufferRow(row) for row in [startRow..endRow] by 1 - return - - # Given a buffer row, this indents it. - # - # bufferRow - The row {Number}. - # options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}. - autoIndentBufferRow: (bufferRow, options) -> - indentLevel = @suggestedIndentForBufferRow(bufferRow, options) - @editor.setIndentationForBufferRow(bufferRow, indentLevel, options) - - # Given a buffer row, this decreases the indentation. - # - # bufferRow - The row {Number} - autoDecreaseIndentForBufferRow: (bufferRow) -> - scopeDescriptor = @editor.scopeDescriptorForBufferPosition([bufferRow, 0]) - return unless decreaseIndentRegex = @decreaseIndentRegexForScopeDescriptor(scopeDescriptor) - - line = @buffer.lineForRow(bufferRow) - return unless decreaseIndentRegex.testSync(line) - - currentIndentLevel = @editor.indentationForBufferRow(bufferRow) - return if currentIndentLevel is 0 - - precedingRow = @buffer.previousNonBlankRow(bufferRow) - return unless precedingRow? - - precedingLine = @buffer.lineForRow(precedingRow) - desiredIndentLevel = @editor.indentationForBufferRow(precedingRow) - - if increaseIndentRegex = @increaseIndentRegexForScopeDescriptor(scopeDescriptor) - desiredIndentLevel -= 1 unless increaseIndentRegex.testSync(precedingLine) - - if decreaseNextIndentRegex = @decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor) - desiredIndentLevel -= 1 if decreaseNextIndentRegex.testSync(precedingLine) - - if desiredIndentLevel >= 0 and desiredIndentLevel < currentIndentLevel - @editor.setIndentationForBufferRow(bufferRow, desiredIndentLevel) - - cacheRegex: (pattern) -> - if pattern - @regexesByPattern[pattern] ?= new OnigRegExp(pattern) - - increaseIndentRegexForScopeDescriptor: (scopeDescriptor) -> - @cacheRegex(@editor.getIncreaseIndentPattern(scopeDescriptor)) - - decreaseIndentRegexForScopeDescriptor: (scopeDescriptor) -> - @cacheRegex(@editor.getDecreaseIndentPattern(scopeDescriptor)) - - decreaseNextIndentRegexForScopeDescriptor: (scopeDescriptor) -> - @cacheRegex(@editor.getDecreaseNextIndentPattern(scopeDescriptor)) diff --git a/src/selection.coffee b/src/selection.coffee index e361d0b5c..4d3fe8882 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -381,7 +381,7 @@ class Selection extends Model if options.autoIndent and textIsAutoIndentable and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0 autoIndentFirstLine = true firstLine = precedingText + firstInsertedLine - desiredIndentLevel = @editor.languageMode.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine) + desiredIndentLevel = @editor.tokenizedBuffer.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine) indentAdjustment = desiredIndentLevel - @editor.indentLevelForLine(firstLine) @adjustIndent(remainingLines, indentAdjustment) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index d75276f06..8cbcc94f3 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -5,7 +5,6 @@ Grim = require 'grim' {CompositeDisposable, Disposable, Emitter} = require 'event-kit' {OnigRegExp} = require 'oniguruma' {Point, Range} = TextBuffer = require 'text-buffer' -LanguageMode = require './language-mode' DecorationManager = require './decoration-manager' TokenizedBuffer = require './tokenized-buffer' Cursor = require './cursor' @@ -80,7 +79,6 @@ class TextEditor extends Model serializationVersion: 1 buffer: null - languageMode: null cursors: null showCursorOnSelection: null selections: null @@ -245,8 +243,6 @@ class TextEditor extends Model initialColumn = Math.max(parseInt(initialColumn) or 0, 0) @addCursorAtBufferPosition([initialLine, initialColumn]) - @languageMode = new LanguageMode(this) - @gutterContainer = new GutterContainer(this) @lineNumberGutter = @gutterContainer.addGutter name: 'line-number' @@ -3085,7 +3081,8 @@ class TextEditor extends Model else endColumn = @lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length newIndentString = @buildIndentString(newLevel) - @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) + if newIndentString.length isnt endColumn + @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) # Extended: Indent rows intersecting selections by one level. indentSelectedRows: -> @@ -3626,18 +3623,6 @@ class TextEditor extends Model getCommentStrings: (scopes) -> @scopedSettingsDelegate?.getCommentStrings?(scopes) - getIncreaseIndentPattern: (scopes) -> - @scopedSettingsDelegate?.getIncreaseIndentPattern?(scopes) - - getDecreaseIndentPattern: (scopes) -> - @scopedSettingsDelegate?.getDecreaseIndentPattern?(scopes) - - getDecreaseNextIndentPattern: (scopes) -> - @scopedSettingsDelegate?.getDecreaseNextIndentPattern?(scopes) - - getFoldEndPattern: (scopes) -> - @scopedSettingsDelegate?.getFoldEndPattern?(scopes) - ### Section: Event Handlers ### @@ -3873,15 +3858,32 @@ class TextEditor extends Model Section: Language Mode Delegated Methods ### - suggestedIndentForBufferRow: (bufferRow, options) -> @languageMode.suggestedIndentForBufferRow(bufferRow, options) + suggestedIndentForBufferRow: (bufferRow, options) -> @tokenizedBuffer.suggestedIndentForBufferRow(bufferRow, options) - autoIndentBufferRow: (bufferRow, options) -> @languageMode.autoIndentBufferRow(bufferRow, options) + # Given a buffer row, indent it. + # + # * bufferRow - The row {Number}. + # * options - An options {Object} to pass through to {TextEditor::setIndentationForBufferRow}. + autoIndentBufferRow: (bufferRow, options) -> + indentLevel = @suggestedIndentForBufferRow(bufferRow, options) + @setIndentationForBufferRow(bufferRow, indentLevel, options) - autoIndentBufferRows: (startRow, endRow) -> @languageMode.autoIndentBufferRows(startRow, endRow) + # Indents all the rows between two buffer row numbers. + # + # * startRow - The row {Number} to start at + # * endRow - The row {Number} to end at + autoIndentBufferRows: (startRow, endRow) -> + row = startRow + while row <= endRow + @autoIndentBufferRow(row) + row++ + return - autoDecreaseIndentForBufferRow: (bufferRow) -> @languageMode.autoDecreaseIndentForBufferRow(bufferRow) + autoDecreaseIndentForBufferRow: (bufferRow) -> + indentLevel = @tokenizedBuffer.suggestedIndentForEditedBufferRow(bufferRow) + @setIndentationForBufferRow(bufferRow, indentLevel) - toggleLineCommentForBufferRow: (row) -> @languageMode.toggleLineCommentsForBufferRow(row) + toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row) toggleLineCommentsForBufferRows: (start, end) -> scope = @scopeDescriptorForBufferPosition([start, 0]) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index f51baa950..4cb0e7b4e 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -57,6 +57,105 @@ class TokenizedBuffer { return !this.alive } + /* + Section - auto-indent + */ + + // Get the suggested indentation level for an existing line in the buffer. + // + // * bufferRow - A {Number} indicating the buffer row + // + // Returns a {Number}. + suggestedIndentForBufferRow (bufferRow, options) { + const line = this.buffer.lineForRow(bufferRow) + const tokenizedLine = this.tokenizedLineForRow(bufferRow) + return this._suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options) + } + + // Get the suggested indentation level for a given line of text, if it were inserted at the given + // row in the buffer. + // + // * bufferRow - A {Number} indicating the buffer row + // + // Returns a {Number}. + suggestedIndentForLineAtBufferRow (bufferRow, line, options) { + const tokenizedLine = this.buildTokenizedLineForRowWithText(bufferRow, line) + return this._suggestedIndentForTokenizedLineAtBufferRow(bufferRow, line, tokenizedLine, options) + } + + // Get the suggested indentation level for a line in the buffer on which the user is currently + // typing. This may return a different result from {::suggestedIndentForBufferRow} in order + // to avoid unexpected changes in indentation. + // + // * bufferRow - The row {Number} + // + // Returns a {Number}. + suggestedIndentForEditedBufferRow (bufferRow) { + const line = this.buffer.lineForRow(bufferRow) + const currentIndentLevel = this.indentLevelForLine(line) + if (currentIndentLevel === 0) return currentIndentLevel + + const scopeDescriptor = this.scopeDescriptorForPosition([bufferRow, 0]) + const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor) + if (!decreaseIndentRegex) return currentIndentLevel + + if (!decreaseIndentRegex.testSync(line)) return currentIndentLevel + + const precedingRow = this.buffer.previousNonBlankRow(bufferRow) + if (precedingRow == null) return currentIndentLevel + + const precedingLine = this.buffer.lineForRow(precedingRow) + let desiredIndentLevel = this.indentLevelForLine(precedingLine) + + const increaseIndentRegex = this.increaseIndentRegexForScopeDescriptor(scopeDescriptor) + if (increaseIndentRegex) { + if (!increaseIndentRegex.testSync(precedingLine)) desiredIndentLevel -= 1 + } + + const decreaseNextIndentRegex = this.decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor) + if (decreaseNextIndentRegex) { + if (decreaseNextIndentRegex.testSync(precedingLine)) desiredIndentLevel -= 1 + } + + if (desiredIndentLevel < 0) return 0 + if (desiredIndentLevel > currentIndentLevel) return currentIndentLevel + return desiredIndentLevel + } + + _suggestedIndentForTokenizedLineAtBufferRow (bufferRow, line, tokenizedLine, options) { + const iterator = tokenizedLine.getTokenIterator() + iterator.next() + const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()}) + + const increaseIndentRegex = this.increaseIndentRegexForScopeDescriptor(scopeDescriptor) + const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor) + const decreaseNextIndentRegex = this.decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor) + + let precedingRow + if (!options || options.skipBlankLines !== false) { + precedingRow = this.buffer.previousNonBlankRow(bufferRow) + if (precedingRow == null) return 0 + } else { + precedingRow = bufferRow - 1 + if (precedingRow < 0) return 0 + } + + const precedingLine = this.buffer.lineForRow(precedingRow) + let desiredIndentLevel = this.indentLevelForLine(precedingLine) + if (!increaseIndentRegex) return desiredIndentLevel + + if (!this.isRowCommented(precedingRow)) { + if (increaseIndentRegex && increaseIndentRegex.testSync(precedingLine)) desiredIndentLevel += 1 + if (decreaseNextIndentRegex && decreaseNextIndentRegex.testSync(precedingLine)) desiredIndentLevel -= 1 + } + + if (!this.buffer.isRowBlank(precedingRow)) { + if (decreaseIndentRegex && decreaseIndentRegex.testSync(line)) desiredIndentLevel -= 1 + } + + return Math.max(desiredIndentLevel, 0) + } + buildIterator () { return new TokenizedBufferIterator(this) } @@ -595,6 +694,24 @@ class TokenizedBuffer { return foldEndRow } + increaseIndentRegexForScopeDescriptor (scopeDescriptor) { + if (this.scopedSettingsDelegate) { + return this.regexForPattern(this.scopedSettingsDelegate.getIncreaseIndentPattern(scopeDescriptor)) + } + } + + decreaseIndentRegexForScopeDescriptor (scopeDescriptor) { + if (this.scopedSettingsDelegate) { + return this.regexForPattern(this.scopedSettingsDelegate.getDecreaseIndentPattern(scopeDescriptor)) + } + } + + decreaseNextIndentRegexForScopeDescriptor (scopeDescriptor) { + if (this.scopedSettingsDelegate) { + return this.regexForPattern(this.scopedSettingsDelegate.getDecreaseNextIndentPattern(scopeDescriptor)) + } + } + foldEndRegexForScopeDescriptor (scopes) { if (this.scopedSettingsDelegate) { return this.regexForPattern(this.scopedSettingsDelegate.getFoldEndPattern(scopes)) From 9abcad11e49d550dbfb4af9a5d04cd2c7ba7728a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 22 Sep 2017 21:06:15 -0700 Subject: [PATCH 21/81] Add shim for TextEditor.languageMode, will deprecate later --- src/text-editor.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 8cbcc94f3..fe9d03a72 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -122,6 +122,8 @@ class TextEditor extends Model this ) + Object.defineProperty(@prototype, 'languageMode', get: -> @tokenizedBuffer) + @deserialize: (state, atomEnvironment) -> # TODO: Return null on version mismatch when 1.8.0 has been out for a while if state.version isnt @prototype.serializationVersion and state.displayBuffer? From a73de8c0b5dbdf28c8869587d64c7da60893c331 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 23 Sep 2017 00:22:11 -0700 Subject: [PATCH 22/81] Avoid spurious updates in autoDecreaseIndentForBufferRow --- src/text-editor.coffee | 5 ++--- src/tokenized-buffer.js | 13 +++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index fe9d03a72..2aad26f45 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3083,8 +3083,7 @@ class TextEditor extends Model else endColumn = @lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length newIndentString = @buildIndentString(newLevel) - if newIndentString.length isnt endColumn - @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) + @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) # Extended: Indent rows intersecting selections by one level. indentSelectedRows: -> @@ -3883,7 +3882,7 @@ class TextEditor extends Model autoDecreaseIndentForBufferRow: (bufferRow) -> indentLevel = @tokenizedBuffer.suggestedIndentForEditedBufferRow(bufferRow) - @setIndentationForBufferRow(bufferRow, indentLevel) + @setIndentationForBufferRow(bufferRow, indentLevel) if indentLevel? toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index 4cb0e7b4e..b0ee635da 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -85,7 +85,8 @@ class TokenizedBuffer { // Get the suggested indentation level for a line in the buffer on which the user is currently // typing. This may return a different result from {::suggestedIndentForBufferRow} in order - // to avoid unexpected changes in indentation. + // to avoid unexpected changes in indentation. It may also return undefined if no change should + // be made. // // * bufferRow - The row {Number} // @@ -93,16 +94,16 @@ class TokenizedBuffer { suggestedIndentForEditedBufferRow (bufferRow) { const line = this.buffer.lineForRow(bufferRow) const currentIndentLevel = this.indentLevelForLine(line) - if (currentIndentLevel === 0) return currentIndentLevel + if (currentIndentLevel === 0) return const scopeDescriptor = this.scopeDescriptorForPosition([bufferRow, 0]) const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor) - if (!decreaseIndentRegex) return currentIndentLevel + if (!decreaseIndentRegex) return - if (!decreaseIndentRegex.testSync(line)) return currentIndentLevel + if (!decreaseIndentRegex.testSync(line)) return const precedingRow = this.buffer.previousNonBlankRow(bufferRow) - if (precedingRow == null) return currentIndentLevel + if (precedingRow == null) return const precedingLine = this.buffer.lineForRow(precedingRow) let desiredIndentLevel = this.indentLevelForLine(precedingLine) @@ -118,7 +119,7 @@ class TokenizedBuffer { } if (desiredIndentLevel < 0) return 0 - if (desiredIndentLevel > currentIndentLevel) return currentIndentLevel + if (desiredIndentLevel >= currentIndentLevel) return return desiredIndentLevel } From 22c573b16749ea2d20a85d89a295857ab05c2de1 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 23 Sep 2017 23:21:09 +0200 Subject: [PATCH 23/81] :arrow_up: first-mate@7.0.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e17b69cb..3ecdd6fd9 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "etch": "^0.12.6", "event-kit": "^2.4.0", "find-parent-dir": "^0.3.0", - "first-mate": "7.0.7", + "first-mate": "7.0.8", "focus-trap": "^2.3.0", "fs-admin": "^0.1.6", "fs-plus": "^3.0.1", From da6866ba7fcf6130899edb5002bab9a53626b24f Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 23 Sep 2017 23:21:28 +0200 Subject: [PATCH 24/81] :arrow_up: language-html@0.48.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3ecdd6fd9..cc8058287 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "language-gfm": "0.90.1", "language-git": "0.19.1", "language-go": "0.44.2", - "language-html": "0.48.0", + "language-html": "0.48.1", "language-hyperlink": "0.16.2", "language-java": "0.27.4", "language-javascript": "0.127.5", From f58066148dcc89089b65aa17d60e3ffe185cebc7 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 23 Sep 2017 23:21:41 +0200 Subject: [PATCH 25/81] :arrow_up: language-csharp@0.14.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc8058287..be3e9cd20 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,7 @@ "language-c": "0.58.1", "language-clojure": "0.22.4", "language-coffee-script": "0.49.1", - "language-csharp": "0.14.2", + "language-csharp": "0.14.3", "language-css": "0.42.6", "language-gfm": "0.90.1", "language-git": "0.19.1", From 22757d799f5a34b3ce0e132c451688ad59e322ef Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Sat, 23 Sep 2017 23:21:56 +0200 Subject: [PATCH 26/81] :arrow_up: language-php@0.42.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index be3e9cd20..575877dcb 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "language-mustache": "0.14.3", "language-objective-c": "0.15.1", "language-perl": "0.37.0", - "language-php": "0.42.0", + "language-php": "0.42.1", "language-property-list": "0.9.1", "language-python": "0.45.4", "language-ruby": "0.71.3", From 6df3c27da06506dd969ed480c8e7ef15174fb4b5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 23 Sep 2017 15:56:55 -0700 Subject: [PATCH 27/81] Fix unfoldBufferRow --- src/text-editor.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 2aad26f45..5ded33bb1 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3316,7 +3316,8 @@ class TextEditor extends Model # Essential: Unfold the most recent cursor's row by one level. unfoldCurrentRow: -> - position = @getCursorBufferPosition() + {row} = @getCursorBufferPosition() + position = Point(row, Infinity) @displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position)) # Essential: Fold the given row in buffer coordinates based on its indentation From 6e482412bdef9bed69844e390a01a70fe3b50a0a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 23 Sep 2017 16:48:01 -0700 Subject: [PATCH 28/81] :arrow_up: tabs, image-view, archive-view --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 0e17b69cb..2b01789c4 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "solarized-dark-syntax": "1.1.2", "solarized-light-syntax": "1.1.2", "about": "1.7.8", - "archive-view": "0.63.3", + "archive-view": "0.63.4", "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", @@ -113,7 +113,7 @@ "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.6", - "image-view": "0.62.3", + "image-view": "0.62.4", "incompatible-packages": "0.27.3", "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.4", @@ -129,7 +129,7 @@ "status-bar": "1.8.13", "styleguide": "0.49.7", "symbols-view": "0.118.0", - "tabs": "0.107.3", + "tabs": "0.107.4", "timecop": "0.36.0", "tree-view": "0.218.0", "update-package-dependencies": "0.12.0", From 7cd6e266b2ff1f3c67e0019aec613c22bc17986d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 24 Sep 2017 09:16:30 -0700 Subject: [PATCH 29/81] Add back some default properties of TokenizedBuffer --- src/tokenized-buffer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index b0ee635da..546678f57 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -32,6 +32,7 @@ class TokenizedBuffer { this.regexesByPattern = {} this.alive = true + this.visible = false this.id = params.id != null ? params.id : nextId++ this.buffer = params.buffer this.tabLength = params.tabLength @@ -736,6 +737,8 @@ class TokenizedBuffer { } } +module.exports.prototype.chunkSize = 50 + function selectorMatchesAnyScope (selector, scopes) { const targetClasses = selector.replace(/^\./, '').split('.') return scopes.some((scope) => { From cd1a265dd3e141e1e5eb54e0c5c5fe19c954ad78 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 24 Sep 2017 09:54:06 -0700 Subject: [PATCH 30/81] Move .suggestedIndentForBufferRow tests to tokenized-buffer-spec --- spec/text-editor-spec.js | 44 ---------------------------------- spec/tokenized-buffer-spec.js | 45 +++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index e72417aca..6cbb926de 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -1,4 +1,3 @@ -const dedent = require('dedent') const {Point, Range} = require('text-buffer') const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') @@ -9,49 +8,6 @@ describe('TextEditor', () => { editor.destroy() }) - describe('.suggestedIndentForBufferRow', () => { - describe('javascript', () => { - beforeEach(async () => { - editor = await atom.workspace.open('sample.js', {autoIndent: false}) - await atom.packages.activatePackage('language-javascript') - }) - - it('bases indentation off of the previous non-blank line', () => { - expect(editor.suggestedIndentForBufferRow(0)).toBe(0) - expect(editor.suggestedIndentForBufferRow(1)).toBe(1) - expect(editor.suggestedIndentForBufferRow(2)).toBe(2) - expect(editor.suggestedIndentForBufferRow(5)).toBe(3) - expect(editor.suggestedIndentForBufferRow(7)).toBe(2) - expect(editor.suggestedIndentForBufferRow(9)).toBe(1) - expect(editor.suggestedIndentForBufferRow(11)).toBe(1) - }) - - it('does not take invisibles into account', () => { - editor.update({showInvisibles: true}) - expect(editor.suggestedIndentForBufferRow(0)).toBe(0) - expect(editor.suggestedIndentForBufferRow(1)).toBe(1) - expect(editor.suggestedIndentForBufferRow(2)).toBe(2) - expect(editor.suggestedIndentForBufferRow(5)).toBe(3) - expect(editor.suggestedIndentForBufferRow(7)).toBe(2) - expect(editor.suggestedIndentForBufferRow(9)).toBe(1) - expect(editor.suggestedIndentForBufferRow(11)).toBe(1) - }) - }) - - describe('css', () => { - beforeEach(async () => { - editor = await atom.workspace.open('css.css', {autoIndent: true}) - await atom.packages.activatePackage('language-source') - await atom.packages.activatePackage('language-css') - }) - - it('does not return negative values (regression)', () => { - editor.setText('.test {\npadding: 0;\n}') - expect(editor.suggestedIndentForBufferRow(2)).toBe(0) - }) - }) - }) - describe('.toggleLineCommentsForBufferRows', () => { describe('xml', () => { beforeEach(async () => { diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index c0bd29b50..f2e435538 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -594,6 +594,51 @@ describe('TokenizedBuffer', () => { }) }) + describe('.suggestedIndentForBufferRow', () => { + let editor + + describe('javascript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) + + it('bases indentation off of the previous non-blank line', () => { + expect(editor.suggestedIndentForBufferRow(0)).toBe(0) + expect(editor.suggestedIndentForBufferRow(1)).toBe(1) + expect(editor.suggestedIndentForBufferRow(2)).toBe(2) + expect(editor.suggestedIndentForBufferRow(5)).toBe(3) + expect(editor.suggestedIndentForBufferRow(7)).toBe(2) + expect(editor.suggestedIndentForBufferRow(9)).toBe(1) + expect(editor.suggestedIndentForBufferRow(11)).toBe(1) + }) + + it('does not take invisibles into account', () => { + editor.update({showInvisibles: true}) + expect(editor.suggestedIndentForBufferRow(0)).toBe(0) + expect(editor.suggestedIndentForBufferRow(1)).toBe(1) + expect(editor.suggestedIndentForBufferRow(2)).toBe(2) + expect(editor.suggestedIndentForBufferRow(5)).toBe(3) + expect(editor.suggestedIndentForBufferRow(7)).toBe(2) + expect(editor.suggestedIndentForBufferRow(9)).toBe(1) + expect(editor.suggestedIndentForBufferRow(11)).toBe(1) + }) + }) + + describe('css', () => { + beforeEach(async () => { + editor = await atom.workspace.open('css.css', {autoIndent: true}) + await atom.packages.activatePackage('language-source') + await atom.packages.activatePackage('language-css') + }) + + it('does not return negative values (regression)', () => { + editor.setText('.test {\npadding: 0;\n}') + expect(editor.suggestedIndentForBufferRow(2)).toBe(0) + }) + }) + }) + describe('.isFoldableAtRow(row)', () => { beforeEach(() => { buffer = atom.project.bufferForPathSync('sample.js') From 090b753d844c4e54a16578ad9be46c063ac86332 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 24 Sep 2017 10:34:34 -0700 Subject: [PATCH 31/81] Move toggleLineCommentsForBufferRows to TokenizedBuffer --- spec/text-editor-spec.js | 156 --------------------------------- spec/tokenized-buffer-spec.js | 158 ++++++++++++++++++++++++++++++++++ src/text-editor.coffee | 70 +-------------- src/tokenized-buffer.js | 94 ++++++++++++++++++++ 4 files changed, 253 insertions(+), 225 deletions(-) diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index 6cbb926de..82ad3bc90 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -8,162 +8,6 @@ describe('TextEditor', () => { editor.destroy() }) - describe('.toggleLineCommentsForBufferRows', () => { - describe('xml', () => { - beforeEach(async () => { - editor = await atom.workspace.open('sample.xml', {autoIndent: false}) - editor.setText('') - await atom.packages.activatePackage('language-xml') - }) - - it('removes the leading whitespace from the comment end pattern match when uncommenting lines', () => { - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('test') - }) - }) - - describe('less', () => { - beforeEach(async () => { - editor = await atom.workspace.open('sample.less', {autoIndent: false}) - await atom.packages.activatePackage('language-less') - await atom.packages.activatePackage('language-css') - }) - - it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart` when commenting lines', () => { - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('// @color: #4D926F;') - }) - }) - - describe('css', () => { - beforeEach(async () => { - editor = await atom.workspace.open('css.css', {autoIndent: false}) - await atom.packages.activatePackage('language-css') - }) - - it('comments/uncomments lines in the given range', () => { - editor.toggleLineCommentsForBufferRows(0, 1) - expect(editor.lineTextForBufferRow(0)).toBe('/*body {') - expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;*/') - expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;') - expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') - - editor.toggleLineCommentsForBufferRows(2, 2) - expect(editor.lineTextForBufferRow(0)).toBe('/*body {') - expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;*/') - expect(editor.lineTextForBufferRow(2)).toBe(' /*width: 110%;*/') - expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') - - editor.toggleLineCommentsForBufferRows(0, 1) - expect(editor.lineTextForBufferRow(0)).toBe('body {') - expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;') - expect(editor.lineTextForBufferRow(2)).toBe(' /*width: 110%;*/') - expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') - }) - - it('uncomments lines with leading whitespace', () => { - editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/') - editor.toggleLineCommentsForBufferRows(2, 2) - expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;') - }) - - it('uncomments lines with trailing whitespace', () => { - editor.setTextInBufferRange([[2, 0], [2, Infinity]], '/*width: 110%;*/ ') - editor.toggleLineCommentsForBufferRows(2, 2) - expect(editor.lineTextForBufferRow(2)).toBe('width: 110%; ') - }) - - it('uncomments lines with leading and trailing whitespace', () => { - editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/ ') - editor.toggleLineCommentsForBufferRows(2, 2) - expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%; ') - }) - }) - - describe('coffeescript', () => { - beforeEach(async () => { - editor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) - await atom.packages.activatePackage('language-coffee-script') - }) - - it('comments/uncomments lines in the given range', () => { - editor.toggleLineCommentsForBufferRows(4, 6) - expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') - expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') - expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') - - editor.toggleLineCommentsForBufferRows(4, 5) - expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') - expect(editor.lineTextForBufferRow(5)).toBe(' left = []') - expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') - }) - - it('comments/uncomments empty lines', () => { - editor.toggleLineCommentsForBufferRows(4, 7) - expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') - expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') - expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') - expect(editor.lineTextForBufferRow(7)).toBe(' # ') - - editor.toggleLineCommentsForBufferRows(4, 5) - expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') - expect(editor.lineTextForBufferRow(5)).toBe(' left = []') - expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') - expect(editor.lineTextForBufferRow(7)).toBe(' # ') - }) - }) - - describe('javascript', () => { - beforeEach(async () => { - editor = await atom.workspace.open('sample.js', {autoIndent: false}) - await atom.packages.activatePackage('language-javascript') - }) - - it('comments/uncomments lines in the given range', () => { - editor.toggleLineCommentsForBufferRows(4, 7) - expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {') - expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();') - expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') - expect(editor.lineTextForBufferRow(7)).toBe(' // }') - - editor.toggleLineCommentsForBufferRows(4, 5) - expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') - expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();') - console.log(JSON.stringify(editor.lineTextForBufferRow(5))); - return - expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') - expect(editor.lineTextForBufferRow(7)).toBe(' // }') - - editor.setText('\tvar i;') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('\t// var i;') - - editor.setText('var i;') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('// var i;') - - editor.setText(' var i;') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe(' // var i;') - - editor.setText(' ') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe(' // ') - - editor.setText(' a\n \n b') - editor.toggleLineCommentsForBufferRows(0, 2) - expect(editor.lineTextForBufferRow(0)).toBe(' // a') - expect(editor.lineTextForBufferRow(1)).toBe(' // ') - expect(editor.lineTextForBufferRow(2)).toBe(' // b') - - editor.setText(' \n // var i;') - editor.toggleLineCommentsForBufferRows(0, 1) - expect(editor.lineTextForBufferRow(0)).toBe(' ') - expect(editor.lineTextForBufferRow(1)).toBe(' var i;') - }) - }) - }) - describe('folding', () => { beforeEach(async () => { await atom.packages.activatePackage('language-javascript') diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index f2e435538..ccd605800 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -639,6 +639,164 @@ describe('TokenizedBuffer', () => { }) }) + describe('.toggleLineCommentsForBufferRows', () => { + let editor + + describe('xml', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.xml', {autoIndent: false}) + editor.setText('') + await atom.packages.activatePackage('language-xml') + }) + + it('removes the leading whitespace from the comment end pattern match when uncommenting lines', () => { + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('test') + }) + }) + + describe('less', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.less', {autoIndent: false}) + await atom.packages.activatePackage('language-less') + await atom.packages.activatePackage('language-css') + }) + + it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart` when commenting lines', () => { + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('// @color: #4D926F;') + }) + }) + + describe('css', () => { + beforeEach(async () => { + editor = await atom.workspace.open('css.css', {autoIndent: false}) + await atom.packages.activatePackage('language-css') + }) + + it('comments/uncomments lines in the given range', () => { + editor.toggleLineCommentsForBufferRows(0, 1) + expect(editor.lineTextForBufferRow(0)).toBe('/*body {') + expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;*/') + expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;') + expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(0)).toBe('/*body {') + expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;*/') + expect(editor.lineTextForBufferRow(2)).toBe(' /*width: 110%;*/') + expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + + editor.toggleLineCommentsForBufferRows(0, 1) + expect(editor.lineTextForBufferRow(0)).toBe('body {') + expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;') + expect(editor.lineTextForBufferRow(2)).toBe(' /*width: 110%;*/') + expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + }) + + it('uncomments lines with leading whitespace', () => { + editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/') + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;') + }) + + it('uncomments lines with trailing whitespace', () => { + editor.setTextInBufferRange([[2, 0], [2, Infinity]], '/*width: 110%;*/ ') + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(2)).toBe('width: 110%; ') + }) + + it('uncomments lines with leading and trailing whitespace', () => { + editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/ ') + editor.toggleLineCommentsForBufferRows(2, 2) + expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%; ') + }) + }) + + describe('coffeescript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) + await atom.packages.activatePackage('language-coffee-script') + }) + + it('comments/uncomments lines in the given range', () => { + editor.toggleLineCommentsForBufferRows(4, 6) + expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + }) + + it('comments/uncomments empty lines', () => { + editor.toggleLineCommentsForBufferRows(4, 7) + expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + expect(editor.lineTextForBufferRow(7)).toBe(' # ') + + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') + expect(editor.lineTextForBufferRow(5)).toBe(' left = []') + expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + expect(editor.lineTextForBufferRow(7)).toBe(' # ') + }) + }) + + describe('javascript', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js', {autoIndent: false}) + await atom.packages.activatePackage('language-javascript') + }) + + it('comments/uncomments lines in the given range', () => { + editor.toggleLineCommentsForBufferRows(4, 7) + expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {') + expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();') + expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(7)).toBe(' // }') + + editor.toggleLineCommentsForBufferRows(4, 5) + expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') + expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();') + console.log(JSON.stringify(editor.lineTextForBufferRow(5))); + return + expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') + expect(editor.lineTextForBufferRow(7)).toBe(' // }') + + editor.setText('\tvar i;') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('\t// var i;') + + editor.setText('var i;') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe('// var i;') + + editor.setText(' var i;') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe(' // var i;') + + editor.setText(' ') + editor.toggleLineCommentsForBufferRows(0, 0) + expect(editor.lineTextForBufferRow(0)).toBe(' // ') + + editor.setText(' a\n \n b') + editor.toggleLineCommentsForBufferRows(0, 2) + expect(editor.lineTextForBufferRow(0)).toBe(' // a') + expect(editor.lineTextForBufferRow(1)).toBe(' // ') + expect(editor.lineTextForBufferRow(2)).toBe(' // b') + + editor.setText(' \n // var i;') + editor.toggleLineCommentsForBufferRows(0, 1) + expect(editor.lineTextForBufferRow(0)).toBe(' ') + expect(editor.lineTextForBufferRow(1)).toBe(' var i;') + }) + }) + }) + describe('.isFoldableAtRow(row)', () => { beforeEach(() => { buffer = atom.project.bufferForPathSync('sample.js') diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 5ded33bb1..d85e36535 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -3,7 +3,6 @@ path = require 'path' fs = require 'fs-plus' Grim = require 'grim' {CompositeDisposable, Disposable, Emitter} = require 'event-kit' -{OnigRegExp} = require 'oniguruma' {Point, Range} = TextBuffer = require 'text-buffer' DecorationManager = require './decoration-manager' TokenizedBuffer = require './tokenized-buffer' @@ -3887,74 +3886,7 @@ class TextEditor extends Model toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row) - toggleLineCommentsForBufferRows: (start, end) -> - scope = @scopeDescriptorForBufferPosition([start, 0]) - commentStrings = @getCommentStrings(scope) - return unless commentStrings?.commentStartString - {commentStartString, commentEndString} = commentStrings - - buffer = @buffer - commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?') - commentStartRegex = new OnigRegExp("^(\\s*)(#{commentStartRegexString})") - - if commentEndString - shouldUncomment = commentStartRegex.testSync(buffer.lineForRow(start)) - if shouldUncomment - commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?') - commentEndRegex = new OnigRegExp("(#{commentEndRegexString})(\\s*)$") - startMatch = commentStartRegex.searchSync(buffer.lineForRow(start)) - endMatch = commentEndRegex.searchSync(buffer.lineForRow(end)) - if startMatch and endMatch - buffer.transact -> - columnStart = startMatch[1].length - columnEnd = columnStart + startMatch[2].length - buffer.setTextInRange([[start, columnStart], [start, columnEnd]], "") - - endLength = buffer.lineLengthForRow(end) - endMatch[2].length - endColumn = endLength - endMatch[1].length - buffer.setTextInRange([[end, endColumn], [end, endLength]], "") - else - buffer.transact -> - indentLength = buffer.lineForRow(start).match(/^\s*/)?[0].length ? 0 - buffer.insert([start, indentLength], commentStartString) - buffer.insert([end, buffer.lineLengthForRow(end)], commentEndString) - else - allBlank = true - allBlankOrCommented = true - - for row in [start..end] by 1 - line = buffer.lineForRow(row) - blank = line?.match(/^\s*$/) - - allBlank = false unless blank - allBlankOrCommented = false unless blank or commentStartRegex.testSync(line) - - shouldUncomment = allBlankOrCommented and not allBlank - - if shouldUncomment - for row in [start..end] by 1 - if match = commentStartRegex.searchSync(buffer.lineForRow(row)) - columnStart = match[1].length - columnEnd = columnStart + match[2].length - buffer.setTextInRange([[row, columnStart], [row, columnEnd]], "") - else - indents = [] - for row in [start..end] by 1 - unless @isBufferRowBlank(row) - indents.push(@indentationForBufferRow(start)) - indents.push(0) if indents.length is 0 - indent = Math.min(indents...) - - indentString = @buildIndentString(indent) - tabLength = @getTabLength() - indentRegex = new RegExp("(\t|[ ]{#{tabLength}}){#{Math.floor(indent)}}") - for row in [start..end] by 1 - line = buffer.lineForRow(row) - if indentLength = line.match(indentRegex)?[0].length - buffer.insert([row, indentLength], commentStartString) - else - buffer.setTextInRange([[row, 0], [row, indentString.length]], indentString + commentStartString) - return + toggleLineCommentsForBufferRows: (start, end) -> @tokenizedBuffer.toggleLineCommentsForBufferRows(start, end) rowRangeForParagraphAtBufferRow: (bufferRow) -> return unless NON_WHITESPACE_REGEXP.test(@lineTextForBufferRow(bufferRow)) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index 546678f57..bdfdc254b 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -158,6 +158,94 @@ class TokenizedBuffer { return Math.max(desiredIndentLevel, 0) } + /* + Section - Comments + */ + + toggleLineCommentsForBufferRows (start, end) { + const scope = this.scopeDescriptorForPosition([start, 0]) + const commentStrings = this.commentStringsForScopeDescriptor(scope) + if (!commentStrings) return + const {commentStartString, commentEndString} = commentStrings + if (!commentStartString) return + + const commentStartRegexString = _.escapeRegExp(commentStartString).replace(/(\s+)$/, '(?:$1)?') + const commentStartRegex = new OnigRegExp(`^(\\s*)(${commentStartRegexString})`) + + if (commentEndString) { + const shouldUncomment = commentStartRegex.testSync(this.buffer.lineForRow(start)) + if (shouldUncomment) { + const commentEndRegexString = _.escapeRegExp(commentEndString).replace(/^(\s+)/, '(?:$1)?') + const commentEndRegex = new OnigRegExp(`(${commentEndRegexString})(\\s*)$`) + const startMatch = commentStartRegex.searchSync(this.buffer.lineForRow(start)) + const endMatch = commentEndRegex.searchSync(this.buffer.lineForRow(end)) + if (startMatch && endMatch) { + this.buffer.transact(() => { + const columnStart = startMatch[1].length + const columnEnd = columnStart + startMatch[2].length + this.buffer.setTextInRange([[start, columnStart], [start, columnEnd]], '') + + const endLength = this.buffer.lineLengthForRow(end) - endMatch[2].length + const endColumn = endLength - endMatch[1].length + return this.buffer.setTextInRange([[end, endColumn], [end, endLength]], '') + }) + } + } else { + this.buffer.transact(() => { + const indentLength = this.buffer.lineForRow(start).match(/^\s*/)[0].length + this.buffer.insert([start, indentLength], commentStartString) + this.buffer.insert([end, this.buffer.lineLengthForRow(end)], commentEndString) + }) + } + } else { + let allBlank = true + let allBlankOrCommented = true + + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + const blank = line.match(/^\s*$/) + if (!blank) allBlank = false + if (!blank && !commentStartRegex.testSync(line)) allBlankOrCommented = false + } + + const shouldUncomment = allBlankOrCommented && !allBlank + + if (shouldUncomment) { + for (let row = start; row <= end; row++) { + const match = commentStartRegex.searchSync(this.buffer.lineForRow(row)) + if (match) { + const columnStart = match[1].length + const columnEnd = columnStart + match[2].length + this.buffer.setTextInRange([[row, columnStart], [row, columnEnd]], '') + } + } + } else { + const indents = [] + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + if (NON_WHITESPACE_REGEX.test(line)) { + indents.push(this.indentLevelForLine(line)) + } + } + if (indents.length === 0) indents.push(0) + const indent = Math.min(...indents) + + const tabLength = this.getTabLength() + const indentString = ' '.repeat(tabLength * indent) + const indentRegex = new RegExp(`(\t|[ ]{${tabLength}}){${Math.floor(indent)}}`) + for (let row = start; row <= end; row++) { + const line = this.buffer.lineForRow(row) + const indentMatch = line.match(indentRegex) + if (indentMatch) { + this.buffer.insert([row, indentMatch[0].length], commentStartString) + } else { + this.buffer.insert([row, 0], indentString + commentStartString) + } + } + } + } + } + buildIterator () { return new TokenizedBufferIterator(this) } @@ -720,6 +808,12 @@ class TokenizedBuffer { } } + commentStringsForScopeDescriptor (scopes) { + if (this.scopedSettingsDelegate) { + return this.scopedSettingsDelegate.getCommentStrings(scopes) + } + } + regexForPattern (pattern) { if (pattern) { if (!this.regexesByPattern[pattern]) { From 88a32589abdbe89b2c269fc6a0fab108ed33aa4a Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 25 Sep 2017 09:56:40 -0400 Subject: [PATCH 32/81] Restore a missing "typeof" --- src/package.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/package.coffee b/src/package.coffee index fdd89bc74..e0db21ccc 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -517,7 +517,7 @@ class Package console.error "Error deactivating package '#{@name}'", e.stack # We support then-able async promises as well as sync ones from deactivate - if deactivationResult?.then is 'function' + if typeof deactivationResult?.then is 'function' deactivationResult.then => @afterDeactivation() else @afterDeactivation() From 345e236d86bbf3ce4e4447b4d3ca2c835b2ae7c0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 25 Sep 2017 09:52:15 -0700 Subject: [PATCH 33/81] Fix toggleLineCommentsForBufferRows --- spec/tokenized-buffer-spec.js | 202 +++++++++++++++++++--------------- src/text-editor-registry.js | 2 + src/tokenized-buffer.js | 41 +++++-- 3 files changed, 147 insertions(+), 98 deletions(-) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index ccd605800..b2324d392 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -5,6 +5,7 @@ const {Point, Range} = TextBuffer const _ = require('underscore-plus') const dedent = require('dedent') const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') +const {ScopedSettingsDelegate} = require('../src/text-editor-registry') describe('TokenizedBuffer', () => { let tokenizedBuffer, buffer @@ -16,7 +17,10 @@ describe('TokenizedBuffer', () => { await atom.packages.activatePackage('language-javascript') }) - afterEach(() => tokenizedBuffer && tokenizedBuffer.destroy()) + afterEach(() => { + buffer && buffer.destroy() + tokenizedBuffer && tokenizedBuffer.destroy() + }) function startTokenizing (tokenizedBuffer) { tokenizedBuffer.setVisible(true) @@ -640,159 +644,181 @@ describe('TokenizedBuffer', () => { }) describe('.toggleLineCommentsForBufferRows', () => { - let editor - describe('xml', () => { beforeEach(async () => { - editor = await atom.workspace.open('sample.xml', {autoIndent: false}) - editor.setText('') await atom.packages.activatePackage('language-xml') + buffer = new TextBuffer('') + tokenizedBuffer = new TokenizedBuffer({ + buffer, + grammar: atom.grammars.grammarForScopeName('text.xml'), + scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config) + }) }) it('removes the leading whitespace from the comment end pattern match when uncommenting lines', () => { - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('test') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) + expect(buffer.lineForRow(0)).toBe('test') }) }) describe('less', () => { beforeEach(async () => { - editor = await atom.workspace.open('sample.less', {autoIndent: false}) await atom.packages.activatePackage('language-less') await atom.packages.activatePackage('language-css') + buffer = await TextBuffer.load(require.resolve('./fixtures/sample.less')) + tokenizedBuffer = new TokenizedBuffer({ + buffer, + grammar: atom.grammars.grammarForScopeName('source.css.less'), + scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config) + }) }) it('only uses the `commentEnd` pattern if it comes from the same grammar as the `commentStart` when commenting lines', () => { - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('// @color: #4D926F;') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) + expect(buffer.lineForRow(0)).toBe('// @color: #4D926F;') }) }) describe('css', () => { beforeEach(async () => { - editor = await atom.workspace.open('css.css', {autoIndent: false}) await atom.packages.activatePackage('language-css') + buffer = await TextBuffer.load(require.resolve('./fixtures/css.css')) + tokenizedBuffer = new TokenizedBuffer({ + buffer, + grammar: atom.grammars.grammarForScopeName('source.css'), + scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config) + }) }) it('comments/uncomments lines in the given range', () => { - editor.toggleLineCommentsForBufferRows(0, 1) - expect(editor.lineTextForBufferRow(0)).toBe('/*body {') - expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;*/') - expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;') - expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1) + expect(buffer.lineForRow(0)).toBe('/*body {') + expect(buffer.lineForRow(1)).toBe(' font-size: 1234px;*/') + expect(buffer.lineForRow(2)).toBe(' width: 110%;') + expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;') - editor.toggleLineCommentsForBufferRows(2, 2) - expect(editor.lineTextForBufferRow(0)).toBe('/*body {') - expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;*/') - expect(editor.lineTextForBufferRow(2)).toBe(' /*width: 110%;*/') - expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2) + expect(buffer.lineForRow(0)).toBe('/*body {') + expect(buffer.lineForRow(1)).toBe(' font-size: 1234px;*/') + expect(buffer.lineForRow(2)).toBe(' /*width: 110%;*/') + expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;') - editor.toggleLineCommentsForBufferRows(0, 1) - expect(editor.lineTextForBufferRow(0)).toBe('body {') - expect(editor.lineTextForBufferRow(1)).toBe(' font-size: 1234px;') - expect(editor.lineTextForBufferRow(2)).toBe(' /*width: 110%;*/') - expect(editor.lineTextForBufferRow(3)).toBe(' font-weight: bold !important;') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1) + expect(buffer.lineForRow(0)).toBe('body {') + expect(buffer.lineForRow(1)).toBe(' font-size: 1234px;') + expect(buffer.lineForRow(2)).toBe(' /*width: 110%;*/') + expect(buffer.lineForRow(3)).toBe(' font-weight: bold !important;') }) it('uncomments lines with leading whitespace', () => { - editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/') - editor.toggleLineCommentsForBufferRows(2, 2) - expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%;') + buffer.setTextInRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/') + tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2) + expect(buffer.lineForRow(2)).toBe(' width: 110%;') }) it('uncomments lines with trailing whitespace', () => { - editor.setTextInBufferRange([[2, 0], [2, Infinity]], '/*width: 110%;*/ ') - editor.toggleLineCommentsForBufferRows(2, 2) - expect(editor.lineTextForBufferRow(2)).toBe('width: 110%; ') + buffer.setTextInRange([[2, 0], [2, Infinity]], '/*width: 110%;*/ ') + tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2) + expect(buffer.lineForRow(2)).toBe('width: 110%; ') }) it('uncomments lines with leading and trailing whitespace', () => { - editor.setTextInBufferRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/ ') - editor.toggleLineCommentsForBufferRows(2, 2) - expect(editor.lineTextForBufferRow(2)).toBe(' width: 110%; ') + buffer.setTextInRange([[2, 0], [2, Infinity]], ' /*width: 110%;*/ ') + tokenizedBuffer.toggleLineCommentsForBufferRows(2, 2) + expect(buffer.lineForRow(2)).toBe(' width: 110%; ') }) }) describe('coffeescript', () => { beforeEach(async () => { - editor = await atom.workspace.open('coffee.coffee', {autoIndent: false}) await atom.packages.activatePackage('language-coffee-script') + buffer = await TextBuffer.load(require.resolve('./fixtures/coffee.coffee')) + tokenizedBuffer = new TokenizedBuffer({ + buffer, + tabLength: 2, + grammar: atom.grammars.grammarForScopeName('source.coffee'), + scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config) + }) }) it('comments/uncomments lines in the given range', () => { - editor.toggleLineCommentsForBufferRows(4, 6) - expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') - expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') - expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + tokenizedBuffer.toggleLineCommentsForBufferRows(4, 6) + expect(buffer.lineForRow(4)).toBe(' # pivot = items.shift()') + expect(buffer.lineForRow(5)).toBe(' # left = []') + expect(buffer.lineForRow(6)).toBe(' # right = []') - editor.toggleLineCommentsForBufferRows(4, 5) - expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') - expect(editor.lineTextForBufferRow(5)).toBe(' left = []') - expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') + tokenizedBuffer.toggleLineCommentsForBufferRows(4, 5) + expect(buffer.lineForRow(4)).toBe(' pivot = items.shift()') + expect(buffer.lineForRow(5)).toBe(' left = []') + expect(buffer.lineForRow(6)).toBe(' # right = []') }) it('comments/uncomments empty lines', () => { - editor.toggleLineCommentsForBufferRows(4, 7) - expect(editor.lineTextForBufferRow(4)).toBe(' # pivot = items.shift()') - expect(editor.lineTextForBufferRow(5)).toBe(' # left = []') - expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') - expect(editor.lineTextForBufferRow(7)).toBe(' # ') + tokenizedBuffer.toggleLineCommentsForBufferRows(4, 7) + expect(buffer.lineForRow(4)).toBe(' # pivot = items.shift()') + expect(buffer.lineForRow(5)).toBe(' # left = []') + expect(buffer.lineForRow(6)).toBe(' # right = []') + expect(buffer.lineForRow(7)).toBe(' # ') - editor.toggleLineCommentsForBufferRows(4, 5) - expect(editor.lineTextForBufferRow(4)).toBe(' pivot = items.shift()') - expect(editor.lineTextForBufferRow(5)).toBe(' left = []') - expect(editor.lineTextForBufferRow(6)).toBe(' # right = []') - expect(editor.lineTextForBufferRow(7)).toBe(' # ') + tokenizedBuffer.toggleLineCommentsForBufferRows(4, 5) + expect(buffer.lineForRow(4)).toBe(' pivot = items.shift()') + expect(buffer.lineForRow(5)).toBe(' left = []') + expect(buffer.lineForRow(6)).toBe(' # right = []') + expect(buffer.lineForRow(7)).toBe(' # ') }) }) describe('javascript', () => { beforeEach(async () => { - editor = await atom.workspace.open('sample.js', {autoIndent: false}) await atom.packages.activatePackage('language-javascript') + buffer = await TextBuffer.load(require.resolve('./fixtures/sample.js')) + tokenizedBuffer = new TokenizedBuffer({ + buffer, + tabLength: 2, + grammar: atom.grammars.grammarForScopeName('source.js'), + scopedSettingsDelegate: new ScopedSettingsDelegate(atom.config) + }) }) it('comments/uncomments lines in the given range', () => { - editor.toggleLineCommentsForBufferRows(4, 7) - expect(editor.lineTextForBufferRow(4)).toBe(' // while(items.length > 0) {') - expect(editor.lineTextForBufferRow(5)).toBe(' // current = items.shift();') - expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') - expect(editor.lineTextForBufferRow(7)).toBe(' // }') + tokenizedBuffer.toggleLineCommentsForBufferRows(4, 7) + expect(buffer.lineForRow(4)).toBe(' // while(items.length > 0) {') + expect(buffer.lineForRow(5)).toBe(' // current = items.shift();') + expect(buffer.lineForRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') + expect(buffer.lineForRow(7)).toBe(' // }') - editor.toggleLineCommentsForBufferRows(4, 5) - expect(editor.lineTextForBufferRow(4)).toBe(' while(items.length > 0) {') - expect(editor.lineTextForBufferRow(5)).toBe(' current = items.shift();') - console.log(JSON.stringify(editor.lineTextForBufferRow(5))); - return - expect(editor.lineTextForBufferRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') - expect(editor.lineTextForBufferRow(7)).toBe(' // }') + tokenizedBuffer.toggleLineCommentsForBufferRows(4, 5) + expect(buffer.lineForRow(4)).toBe(' while(items.length > 0) {') + expect(buffer.lineForRow(5)).toBe(' current = items.shift();') + expect(buffer.lineForRow(6)).toBe(' // current < pivot ? left.push(current) : right.push(current);') + expect(buffer.lineForRow(7)).toBe(' // }') - editor.setText('\tvar i;') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('\t// var i;') + buffer.setText('\tvar i;') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) + expect(buffer.lineForRow(0)).toBe('\t// var i;') - editor.setText('var i;') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe('// var i;') + buffer.setText('var i;') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) + expect(buffer.lineForRow(0)).toBe('// var i;') - editor.setText(' var i;') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe(' // var i;') + buffer.setText(' var i;') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) + expect(buffer.lineForRow(0)).toBe(' // var i;') - editor.setText(' ') - editor.toggleLineCommentsForBufferRows(0, 0) - expect(editor.lineTextForBufferRow(0)).toBe(' // ') + buffer.setText(' ') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) + expect(buffer.lineForRow(0)).toBe('// ') - editor.setText(' a\n \n b') - editor.toggleLineCommentsForBufferRows(0, 2) - expect(editor.lineTextForBufferRow(0)).toBe(' // a') - expect(editor.lineTextForBufferRow(1)).toBe(' // ') - expect(editor.lineTextForBufferRow(2)).toBe(' // b') + buffer.setText(' a\n \n b') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 2) + expect(buffer.lineForRow(0)).toBe(' // a') + expect(buffer.lineForRow(1)).toBe(' // ') + expect(buffer.lineForRow(2)).toBe(' // b') - editor.setText(' \n // var i;') - editor.toggleLineCommentsForBufferRows(0, 1) - expect(editor.lineTextForBufferRow(0)).toBe(' ') - expect(editor.lineTextForBufferRow(1)).toBe(' var i;') + buffer.setText(' \n // var i;') + tokenizedBuffer.toggleLineCommentsForBufferRows(0, 1) + expect(buffer.lineForRow(0)).toBe(' ') + expect(buffer.lineForRow(1)).toBe(' var i;') }) }) }) diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js index 35be27fd1..2cbf3093c 100644 --- a/src/text-editor-registry.js +++ b/src/text-editor-registry.js @@ -429,3 +429,5 @@ class ScopedSettingsDelegate { } } } + +TextEditorRegistry.ScopedSettingsDelegate = ScopedSettingsDelegate diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index bdfdc254b..1d52411ae 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -38,6 +38,7 @@ class TokenizedBuffer { this.tabLength = params.tabLength this.largeFileMode = params.largeFileMode this.assert = params.assert + this.scopedSettingsDelegate = params.scopedSettingsDelegate this.setGrammar(params.grammar || NullGrammar) this.disposables.add(this.buffer.registerTextDecorationLayer(this)) @@ -220,26 +221,28 @@ class TokenizedBuffer { } } } else { - const indents = [] + let minIndentLevel = null for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row) if (NON_WHITESPACE_REGEX.test(line)) { - indents.push(this.indentLevelForLine(line)) + const indentLevel = this.indentLevelForLine(line) + if (minIndentLevel == null || indentLevel < minIndentLevel) minIndentLevel = indentLevel } } - if (indents.length === 0) indents.push(0) - const indent = Math.min(...indents) + if (minIndentLevel == null) minIndentLevel = 0 const tabLength = this.getTabLength() - const indentString = ' '.repeat(tabLength * indent) - const indentRegex = new RegExp(`(\t|[ ]{${tabLength}}){${Math.floor(indent)}}`) + const indentString = ' '.repeat(tabLength * minIndentLevel) for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row) - const indentMatch = line.match(indentRegex) - if (indentMatch) { - this.buffer.insert([row, indentMatch[0].length], commentStartString) + if (NON_WHITESPACE_REGEX.test(line)) { + const indentColumn = this.columnForIndentLevel(line, minIndentLevel) + this.buffer.insert(Point(row, indentColumn), commentStartString) } else { - this.buffer.insert([row, 0], indentString + commentStartString) + this.buffer.setTextInRange( + new Range(new Point(row, 0), new Point(row, Infinity)), + indentString + commentStartString + ) } } } @@ -581,6 +584,24 @@ class TokenizedBuffer { return scopes } + columnForIndentLevel (line, indentLevel, tabLength = this.tabLength) { + let column = 0 + let indentLength = 0 + const goalIndentLength = indentLevel * tabLength + while (indentLength < goalIndentLength) { + const char = line[column] + if (char === '\t') { + indentLength += tabLength - (indentLength % tabLength) + } else if (char === ' ') { + indentLength++ + } else { + break + } + column++ + } + return column + } + indentLevelForLine (line, tabLength = this.tabLength) { let indentLength = 0 for (let i = 0, {length} = line; i < length; i++) { From 610fe4eb9f4bf5afa0f6f36977afb6ad780a1c20 Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 25 Sep 2017 13:50:34 -0400 Subject: [PATCH 34/81] :arrow_up: autosave --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b01789c4..e17ba0c4f 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "autocomplete-plus": "2.35.10", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", - "autosave": "0.24.4", + "autosave": "0.24.5", "background-tips": "0.27.1", "bookmarks": "0.44.4", "bracket-matcher": "0.88.0", From c363c950f0882475b3e4fa3421baa6759a218c6d Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 25 Sep 2017 12:28:53 -0700 Subject: [PATCH 35/81] :arrow_up: autocomplete-plus --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e17ba0c4f..b0c5d1577 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", - "autocomplete-plus": "2.35.10", + "autocomplete-plus": "2.35.11", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.5", From 1880e1401d7745a2a644c4f7e72cbc625d06023d Mon Sep 17 00:00:00 2001 From: Ash Wilson Date: Mon, 25 Sep 2017 16:16:34 -0400 Subject: [PATCH 36/81] :arrow_up: apm --- apm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apm/package.json b/apm/package.json index e8d4321b1..5391c9972 100644 --- a/apm/package.json +++ b/apm/package.json @@ -6,6 +6,6 @@ "url": "https://github.com/atom/atom.git" }, "dependencies": { - "atom-package-manager": "1.18.7" + "atom-package-manager": "1.18.8" } } From f690b0d8c45c99a91c774dbf87ba3d0cbed53db1 Mon Sep 17 00:00:00 2001 From: Wliu <50Wliu@users.noreply.github.com> Date: Mon, 25 Sep 2017 23:11:35 +0200 Subject: [PATCH 37/81] :arrow_up: first-mate@7.0.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 575877dcb..5b9d8e89b 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "etch": "^0.12.6", "event-kit": "^2.4.0", "find-parent-dir": "^0.3.0", - "first-mate": "7.0.8", + "first-mate": "7.0.9", "focus-trap": "^2.3.0", "fs-admin": "^0.1.6", "fs-plus": "^3.0.1", From 83c90e341a3f1fdf91737cca915e475a1591b028 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Sep 2017 17:29:09 -0700 Subject: [PATCH 38/81] Convert GitRepository to JS --- src/git-repository.coffee | 496 -------------------------------- src/git-repository.js | 591 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 591 insertions(+), 496 deletions(-) delete mode 100644 src/git-repository.coffee create mode 100644 src/git-repository.js diff --git a/src/git-repository.coffee b/src/git-repository.coffee deleted file mode 100644 index c7105baef..000000000 --- a/src/git-repository.coffee +++ /dev/null @@ -1,496 +0,0 @@ -{join} = require 'path' - -_ = require 'underscore-plus' -{Emitter, Disposable, CompositeDisposable} = require 'event-kit' -fs = require 'fs-plus' -path = require 'path' -GitUtils = require 'git-utils' - -Task = require './task' - -# Extended: Represents the underlying git operations performed by Atom. -# -# This class shouldn't be instantiated directly but instead by accessing the -# `atom.project` global and calling `getRepositories()`. Note that this will -# only be available when the project is backed by a Git repository. -# -# This class handles submodules automatically by taking a `path` argument to many -# of the methods. This `path` argument will determine which underlying -# repository is used. -# -# For a repository with submodules this would have the following outcome: -# -# ```coffee -# repo = atom.project.getRepositories()[0] -# repo.getShortHead() # 'master' -# repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234' -# ``` -# -# ## Examples -# -# ### Logging the URL of the origin remote -# -# ```coffee -# git = atom.project.getRepositories()[0] -# console.log git.getOriginURL() -# ``` -# -# ### Requiring in packages -# -# ```coffee -# {GitRepository} = require 'atom' -# ``` -module.exports = -class GitRepository - @exists: (path) -> - if git = @open(path) - git.destroy() - true - else - false - - ### - Section: Construction and Destruction - ### - - # Public: Creates a new GitRepository instance. - # - # * `path` The {String} path to the Git repository to open. - # * `options` An optional {Object} with the following keys: - # * `refreshOnWindowFocus` A {Boolean}, `true` to refresh the index and - # statuses when the window is focused. - # - # Returns a {GitRepository} instance or `null` if the repository could not be opened. - @open: (path, options) -> - return null unless path - try - new GitRepository(path, options) - catch - null - - constructor: (path, options={}) -> - @emitter = new Emitter - @subscriptions = new CompositeDisposable - - @repo = GitUtils.open(path) - unless @repo? - throw new Error("No Git repository found searching path: #{path}") - - @statuses = {} - @upstream = {ahead: 0, behind: 0} - for submodulePath, submoduleRepo of @repo.submodules - submoduleRepo.upstream = {ahead: 0, behind: 0} - - {@project, @config, refreshOnWindowFocus} = options - - refreshOnWindowFocus ?= true - if refreshOnWindowFocus - onWindowFocus = => - @refreshIndex() - @refreshStatus() - - window.addEventListener 'focus', onWindowFocus - @subscriptions.add new Disposable(-> window.removeEventListener 'focus', onWindowFocus) - - if @project? - @project.getBuffers().forEach (buffer) => @subscribeToBuffer(buffer) - @subscriptions.add @project.onDidAddBuffer (buffer) => @subscribeToBuffer(buffer) - - # Public: Destroy this {GitRepository} object. - # - # This destroys any tasks and subscriptions and releases the underlying - # libgit2 repository handle. This method is idempotent. - destroy: -> - if @emitter? - @emitter.emit 'did-destroy' - @emitter.dispose() - @emitter = null - - if @statusTask? - @statusTask.terminate() - @statusTask = null - - if @repo? - @repo.release() - @repo = null - - if @subscriptions? - @subscriptions.dispose() - @subscriptions = null - - # Public: Returns a {Boolean} indicating if this repository has been destroyed. - isDestroyed: -> - not @repo? - - # Public: Invoke the given callback when this GitRepository's destroy() method - # is invoked. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @emitter.once 'did-destroy', callback - - ### - Section: Event Subscription - ### - - # Public: Invoke the given callback when a specific file's status has - # changed. When a file is updated, reloaded, etc, and the status changes, this - # will be fired. - # - # * `callback` {Function} - # * `event` {Object} - # * `path` {String} the old parameters the decoration used to have - # * `pathStatus` {Number} representing the status. This value can be passed to - # {::isStatusModified} or {::isStatusNew} to get more information. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeStatus: (callback) -> - @emitter.on 'did-change-status', callback - - # Public: Invoke the given callback when a multiple files' statuses have - # changed. For example, on window focus, the status of all the paths in the - # repo is checked. If any of them have changed, this will be fired. Call - # {::getPathStatus(path)} to get the status for your path of choice. - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeStatuses: (callback) -> - @emitter.on 'did-change-statuses', callback - - ### - Section: Repository Details - ### - - # Public: A {String} indicating the type of version control system used by - # this repository. - # - # Returns `"git"`. - getType: -> 'git' - - # Public: Returns the {String} path of the repository. - getPath: -> - @path ?= fs.absolute(@getRepo().getPath()) - - # Public: Returns the {String} working directory path of the repository. - getWorkingDirectory: -> @getRepo().getWorkingDirectory() - - # Public: Returns true if at the root, false if in a subfolder of the - # repository. - isProjectAtRoot: -> - @projectAtRoot ?= @project?.relativize(@getWorkingDirectory()) is '' - - # Public: Makes a path relative to the repository's working directory. - relativize: (path) -> @getRepo().relativize(path) - - # Public: Returns true if the given branch exists. - hasBranch: (branch) -> @getReferenceTarget("refs/heads/#{branch}")? - - # Public: Retrieves a shortened version of the HEAD reference value. - # - # This removes the leading segments of `refs/heads`, `refs/tags`, or - # `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7 - # characters. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository contains submodules. - # - # Returns a {String}. - getShortHead: (path) -> @getRepo(path).getShortHead() - - # Public: Is the given path a submodule in the repository? - # - # * `path` The {String} path to check. - # - # Returns a {Boolean}. - isSubmodule: (path) -> - return false unless path - - repo = @getRepo(path) - if repo.isSubmodule(repo.relativize(path)) - true - else - # Check if the path is a working directory in a repo that isn't the root. - repo isnt @getRepo() and repo.relativize(join(path, 'dir')) is 'dir' - - # Public: Returns the number of commits behind the current branch is from the - # its upstream remote branch. - # - # * `reference` The {String} branch reference name. - # * `path` The {String} path in the repository to get this information for, - # only needed if the repository contains submodules. - getAheadBehindCount: (reference, path) -> - @getRepo(path).getAheadBehindCount(reference) - - # Public: Get the cached ahead/behind commit counts for the current branch's - # upstream branch. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. - # - # Returns an {Object} with the following keys: - # * `ahead` The {Number} of commits ahead. - # * `behind` The {Number} of commits behind. - getCachedUpstreamAheadBehindCount: (path) -> - @getRepo(path).upstream ? @upstream - - # Public: Returns the git configuration value specified by the key. - # - # * `key` The {String} key for the configuration to lookup. - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. - getConfigValue: (key, path) -> @getRepo(path).getConfigValue(key) - - # Public: Returns the origin url of the repository. - # - # * `path` (optional) {String} path in the repository to get this information - # for, only needed if the repository has submodules. - getOriginURL: (path) -> @getConfigValue('remote.origin.url', path) - - # Public: Returns the upstream branch for the current HEAD, or null if there - # is no upstream branch for the current HEAD. - # - # * `path` An optional {String} path in the repo to get this information for, - # only needed if the repository contains submodules. - # - # Returns a {String} branch name such as `refs/remotes/origin/master`. - getUpstreamBranch: (path) -> @getRepo(path).getUpstreamBranch() - - # Public: Gets all the local and remote references. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. - # - # Returns an {Object} with the following keys: - # * `heads` An {Array} of head reference names. - # * `remotes` An {Array} of remote reference names. - # * `tags` An {Array} of tag reference names. - getReferences: (path) -> @getRepo(path).getReferences() - - # Public: Returns the current {String} SHA for the given reference. - # - # * `reference` The {String} reference to get the target of. - # * `path` An optional {String} path in the repo to get the reference target - # for. Only needed if the repository contains submodules. - getReferenceTarget: (reference, path) -> - @getRepo(path).getReferenceTarget(reference) - - ### - Section: Reading Status - ### - - # Public: Returns true if the given path is modified. - # - # * `path` The {String} path to check. - # - # Returns a {Boolean} that's true if the `path` is modified. - isPathModified: (path) -> @isStatusModified(@getPathStatus(path)) - - # Public: Returns true if the given path is new. - # - # * `path` The {String} path to check. - # - # Returns a {Boolean} that's true if the `path` is new. - isPathNew: (path) -> @isStatusNew(@getPathStatus(path)) - - # Public: Is the given path ignored? - # - # * `path` The {String} path to check. - # - # Returns a {Boolean} that's true if the `path` is ignored. - isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path)) - - # Public: Get the status of a directory in the repository's working directory. - # - # * `path` The {String} path to check. - # - # Returns a {Number} representing the status. This value can be passed to - # {::isStatusModified} or {::isStatusNew} to get more information. - getDirectoryStatus: (directoryPath) -> - directoryPath = "#{@relativize(directoryPath)}/" - directoryStatus = 0 - for statusPath, status of @statuses - directoryStatus |= status if statusPath.indexOf(directoryPath) is 0 - directoryStatus - - # Public: Get the status of a single path in the repository. - # - # * `path` A {String} repository-relative path. - # - # Returns a {Number} representing the status. This value can be passed to - # {::isStatusModified} or {::isStatusNew} to get more information. - getPathStatus: (path) -> - repo = @getRepo(path) - relativePath = @relativize(path) - currentPathStatus = @statuses[relativePath] ? 0 - pathStatus = repo.getStatus(repo.relativize(path)) ? 0 - pathStatus = 0 if repo.isStatusIgnored(pathStatus) - if pathStatus > 0 - @statuses[relativePath] = pathStatus - else - delete @statuses[relativePath] - if currentPathStatus isnt pathStatus - @emitter.emit 'did-change-status', {path, pathStatus} - - pathStatus - - # Public: Get the cached status for the given path. - # - # * `path` A {String} path in the repository, relative or absolute. - # - # Returns a status {Number} or null if the path is not in the cache. - getCachedPathStatus: (path) -> - @statuses[@relativize(path)] - - # Public: Returns true if the given status indicates modification. - # - # * `status` A {Number} representing the status. - # - # Returns a {Boolean} that's true if the `status` indicates modification. - isStatusModified: (status) -> @getRepo().isStatusModified(status) - - # Public: Returns true if the given status indicates a new path. - # - # * `status` A {Number} representing the status. - # - # Returns a {Boolean} that's true if the `status` indicates a new path. - isStatusNew: (status) -> @getRepo().isStatusNew(status) - - ### - Section: Retrieving Diffs - ### - - # Public: Retrieves the number of lines added and removed to a path. - # - # This compares the working directory contents of the path to the `HEAD` - # version. - # - # * `path` The {String} path to check. - # - # Returns an {Object} with the following keys: - # * `added` The {Number} of added lines. - # * `deleted` The {Number} of deleted lines. - getDiffStats: (path) -> - repo = @getRepo(path) - repo.getDiffStats(repo.relativize(path)) - - # Public: Retrieves the line diffs comparing the `HEAD` version of the given - # path and the given text. - # - # * `path` The {String} path relative to the repository. - # * `text` The {String} to compare against the `HEAD` contents - # - # Returns an {Array} of hunk {Object}s with the following keys: - # * `oldStart` The line {Number} of the old hunk. - # * `newStart` The line {Number} of the new hunk. - # * `oldLines` The {Number} of lines in the old hunk. - # * `newLines` The {Number} of lines in the new hunk - getLineDiffs: (path, text) -> - # Ignore eol of line differences on windows so that files checked in as - # LF don't report every line modified when the text contains CRLF endings. - options = ignoreEolWhitespace: process.platform is 'win32' - repo = @getRepo(path) - repo.getLineDiffs(repo.relativize(path), text, options) - - ### - Section: Checking Out - ### - - # Public: Restore the contents of a path in the working directory and index - # to the version at `HEAD`. - # - # This is essentially the same as running: - # - # ```sh - # git reset HEAD -- - # git checkout HEAD -- - # ``` - # - # * `path` The {String} path to checkout. - # - # Returns a {Boolean} that's true if the method was successful. - checkoutHead: (path) -> - repo = @getRepo(path) - headCheckedOut = repo.checkoutHead(repo.relativize(path)) - @getPathStatus(path) if headCheckedOut - headCheckedOut - - # Public: Checks out a branch in your repository. - # - # * `reference` The {String} reference to checkout. - # * `create` A {Boolean} value which, if true creates the new reference if - # it doesn't exist. - # - # Returns a Boolean that's true if the method was successful. - checkoutReference: (reference, create) -> - @getRepo().checkoutReference(reference, create) - - ### - Section: Private - ### - - # Subscribes to buffer events. - subscribeToBuffer: (buffer) -> - getBufferPathStatus = => - if bufferPath = buffer.getPath() - @getPathStatus(bufferPath) - - getBufferPathStatus() - bufferSubscriptions = new CompositeDisposable - bufferSubscriptions.add buffer.onDidSave(getBufferPathStatus) - bufferSubscriptions.add buffer.onDidReload(getBufferPathStatus) - bufferSubscriptions.add buffer.onDidChangePath(getBufferPathStatus) - bufferSubscriptions.add buffer.onDidDestroy => - bufferSubscriptions.dispose() - @subscriptions.remove(bufferSubscriptions) - @subscriptions.add(bufferSubscriptions) - return - - # Subscribes to editor view event. - checkoutHeadForEditor: (editor) -> - buffer = editor.getBuffer() - if filePath = buffer.getPath() - @checkoutHead(filePath) - buffer.reload() - - # Returns the corresponding {Repository} - getRepo: (path) -> - if @repo? - @repo.submoduleForPath(path) ? @repo - else - throw new Error("Repository has been destroyed") - - # Reread the index to update any values that have changed since the - # last time the index was read. - refreshIndex: -> @getRepo().refreshIndex() - - # Refreshes the current git status in an outside process and asynchronously - # updates the relevant properties. - refreshStatus: -> - @handlerPath ?= require.resolve('./repository-status-handler') - - relativeProjectPaths = @project?.getPaths() - .map (projectPath) => @relativize(projectPath) - .filter (projectPath) -> projectPath.length > 0 and not path.isAbsolute(projectPath) - - @statusTask?.terminate() - new Promise (resolve) => - @statusTask = Task.once @handlerPath, @getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) => - statusesUnchanged = _.isEqual(statuses, @statuses) and - _.isEqual(upstream, @upstream) and - _.isEqual(branch, @branch) and - _.isEqual(submodules, @submodules) - - @statuses = statuses - @upstream = upstream - @branch = branch - @submodules = submodules - - for submodulePath, submoduleRepo of @getRepo().submodules - submoduleRepo.upstream = submodules[submodulePath]?.upstream ? {ahead: 0, behind: 0} - - unless statusesUnchanged - @emitter.emit 'did-change-statuses' - resolve() diff --git a/src/git-repository.js b/src/git-repository.js new file mode 100644 index 000000000..503e5fa0d --- /dev/null +++ b/src/git-repository.js @@ -0,0 +1,591 @@ +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const {join} = require('path') +const _ = require('underscore-plus') +const {Emitter, Disposable, CompositeDisposable} = require('event-kit') +const fs = require('fs-plus') +const path = require('path') +const GitUtils = require('git-utils') +const Task = require('./task') + +// Extended: Represents the underlying git operations performed by Atom. +// +// This class shouldn't be instantiated directly but instead by accessing the +// `atom.project` global and calling `getRepositories()`. Note that this will +// only be available when the project is backed by a Git repository. +// +// This class handles submodules automatically by taking a `path` argument to many +// of the methods. This `path` argument will determine which underlying +// repository is used. +// +// For a repository with submodules this would have the following outcome: +// +// ```coffee +// repo = atom.project.getRepositories()[0] +// repo.getShortHead() # 'master' +// repo.getShortHead('vendor/path/to/a/submodule') # 'dead1234' +// ``` +// +// ## Examples +// +// ### Logging the URL of the origin remote +// +// ```coffee +// git = atom.project.getRepositories()[0] +// console.log git.getOriginURL() +// ``` +// +// ### Requiring in packages +// +// ```coffee +// {GitRepository} = require 'atom' +// ``` +module.exports = +class GitRepository { + static exists (path) { + const git = this.open(path) + if (git) { + git.destroy() + return true + } else { + return false + } + } + + /* + Section: Construction and Destruction + */ + + // Public: Creates a new GitRepository instance. + // + // * `path` The {String} path to the Git repository to open. + // * `options` An optional {Object} with the following keys: + // * `refreshOnWindowFocus` A {Boolean}, `true` to refresh the index and + // statuses when the window is focused. + // + // Returns a {GitRepository} instance or `null` if the repository could not be opened. + static open (path, options) { + if (!path) { return null } + try { + return new GitRepository(path, options) + } catch (error) { + return null + } + } + + constructor (path, options = {}) { + this.emitter = new Emitter() + this.subscriptions = new CompositeDisposable() + this.repo = GitUtils.open(path) + if (this.repo == null) { + throw new Error(`No Git repository found searching path: ${path}`) + } + + this.statuses = {} + this.upstream = {ahead: 0, behind: 0} + for (let submodulePath in this.repo.submodules) { + const submoduleRepo = this.repo.submodules[submodulePath] + submoduleRepo.upstream = {ahead: 0, behind: 0} + } + + this.project = options.project + this.config = options.config + + if (options.refreshOnWindowFocus || options.refreshOnWindowFocus == null) { + const onWindowFocus = () => { + this.refreshIndex() + return this.refreshStatus() + } + + window.addEventListener('focus', onWindowFocus) + this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', onWindowFocus))) + } + + if (this.project != null) { + this.project.getBuffers().forEach(buffer => this.subscribeToBuffer(buffer)) + this.subscriptions.add(this.project.onDidAddBuffer(buffer => this.subscribeToBuffer(buffer))) + } + } + + // Public: Destroy this {GitRepository} object. + // + // This destroys any tasks and subscriptions and releases the underlying + // libgit2 repository handle. This method is idempotent. + destroy () { + if (this.emitter != null) { + this.emitter.emit('did-destroy') + this.emitter.dispose() + this.emitter = null + } + + if (this.statusTask != null) { + this.statusTask.terminate() + this.statusTask = null + } + + if (this.repo != null) { + this.repo.release() + this.repo = null + } + + if (this.subscriptions != null) { + this.subscriptions.dispose() + this.subscriptions = null + } + } + + // Public: Returns a {Boolean} indicating if this repository has been destroyed. + isDestroyed () { + return this.repo == null + } + + // Public: Invoke the given callback when this GitRepository's destroy() method + // is invoked. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy (callback) { + return this.emitter.once('did-destroy', callback) + } + + /* + Section: Event Subscription + */ + + // Public: Invoke the given callback when a specific file's status has + // changed. When a file is updated, reloaded, etc, and the status changes, this + // will be fired. + // + // * `callback` {Function} + // * `event` {Object} + // * `path` {String} the old parameters the decoration used to have + // * `pathStatus` {Number} representing the status. This value can be passed to + // {::isStatusModified} or {::isStatusNew} to get more information. + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeStatus (callback) { + return this.emitter.on('did-change-status', callback) + } + + // Public: Invoke the given callback when a multiple files' statuses have + // changed. For example, on window focus, the status of all the paths in the + // repo is checked. If any of them have changed, this will be fired. Call + // {::getPathStatus(path)} to get the status for your path of choice. + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangeStatuses (callback) { + return this.emitter.on('did-change-statuses', callback) + } + + /* + Section: Repository Details + */ + + // Public: A {String} indicating the type of version control system used by + // this repository. + // + // Returns `"git"`. + getType () { return 'git' } + + // Public: Returns the {String} path of the repository. + getPath () { + if (this.path == null) { + this.path = fs.absolute(this.getRepo().getPath()) + } + return this.path + } + + // Public: Returns the {String} working directory path of the repository. + getWorkingDirectory () { + return this.getRepo().getWorkingDirectory() + } + + // Public: Returns true if at the root, false if in a subfolder of the + // repository. + isProjectAtRoot () { + if (this.projectAtRoot == null) { + this.projectAtRoot = this.project && this.project.relativize(this.getWorkingDirectory()) + } + return this.projectAtRoot + } + + // Public: Makes a path relative to the repository's working directory. + relativize (path) { + return this.getRepo().relativize(path) + } + + // Public: Returns true if the given branch exists. + hasBranch (branch) { + return this.getReferenceTarget(`refs/heads/${branch}`) != null + } + + // Public: Retrieves a shortened version of the HEAD reference value. + // + // This removes the leading segments of `refs/heads`, `refs/tags`, or + // `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7 + // characters. + // + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository contains submodules. + // + // Returns a {String}. + getShortHead (path) { + return this.getRepo(path).getShortHead() + } + + // Public: Is the given path a submodule in the repository? + // + // * `path` The {String} path to check. + // + // Returns a {Boolean}. + isSubmodule (path) { + if (!path) return false + + const repo = this.getRepo(path) + if (repo.isSubmodule(repo.relativize(path))) { + return true + } else { + // Check if the path is a working directory in a repo that isn't the root. + return repo !== this.getRepo() && repo.relativize(join(path, 'dir')) === 'dir' + } + } + + // Public: Returns the number of commits behind the current branch is from the + // its upstream remote branch. + // + // * `reference` The {String} branch reference name. + // * `path` The {String} path in the repository to get this information for, + // only needed if the repository contains submodules. + getAheadBehindCount (reference, path) { + return this.getRepo(path).getAheadBehindCount(reference) + } + + // Public: Get the cached ahead/behind commit counts for the current branch's + // upstream branch. + // + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository has submodules. + // + // Returns an {Object} with the following keys: + // * `ahead` The {Number} of commits ahead. + // * `behind` The {Number} of commits behind. + getCachedUpstreamAheadBehindCount (path) { + return this.getRepo(path).upstream || this.upstream + } + + // Public: Returns the git configuration value specified by the key. + // + // * `key` The {String} key for the configuration to lookup. + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository has submodules. + getConfigValue (key, path) { + return this.getRepo(path).getConfigValue(key) + } + + // Public: Returns the origin url of the repository. + // + // * `path` (optional) {String} path in the repository to get this information + // for, only needed if the repository has submodules. + getOriginURL (path) { + return this.getConfigValue('remote.origin.url', path) + } + + // Public: Returns the upstream branch for the current HEAD, or null if there + // is no upstream branch for the current HEAD. + // + // * `path` An optional {String} path in the repo to get this information for, + // only needed if the repository contains submodules. + // + // Returns a {String} branch name such as `refs/remotes/origin/master`. + getUpstreamBranch (path) { + return this.getRepo(path).getUpstreamBranch() + } + + // Public: Gets all the local and remote references. + // + // * `path` An optional {String} path in the repository to get this information + // for, only needed if the repository has submodules. + // + // Returns an {Object} with the following keys: + // * `heads` An {Array} of head reference names. + // * `remotes` An {Array} of remote reference names. + // * `tags` An {Array} of tag reference names. + getReferences (path) { + return this.getRepo(path).getReferences() + } + + // Public: Returns the current {String} SHA for the given reference. + // + // * `reference` The {String} reference to get the target of. + // * `path` An optional {String} path in the repo to get the reference target + // for. Only needed if the repository contains submodules. + getReferenceTarget (reference, path) { + return this.getRepo(path).getReferenceTarget(reference) + } + + /* + Section: Reading Status + */ + + // Public: Returns true if the given path is modified. + // + // * `path` The {String} path to check. + // + // Returns a {Boolean} that's true if the `path` is modified. + isPathModified (path) { + return this.isStatusModified(this.getPathStatus(path)) + } + + // Public: Returns true if the given path is new. + // + // * `path` The {String} path to check. + // + // Returns a {Boolean} that's true if the `path` is new. + isPathNew (path) { + return this.isStatusNew(this.getPathStatus(path)) + } + + // Public: Is the given path ignored? + // + // * `path` The {String} path to check. + // + // Returns a {Boolean} that's true if the `path` is ignored. + isPathIgnored (path) { + return this.getRepo().isIgnored(this.relativize(path)) + } + + // Public: Get the status of a directory in the repository's working directory. + // + // * `path` The {String} path to check. + // + // Returns a {Number} representing the status. This value can be passed to + // {::isStatusModified} or {::isStatusNew} to get more information. + getDirectoryStatus (directoryPath) { + directoryPath = `${this.relativize(directoryPath)}/` + let directoryStatus = 0 + for (let statusPath in this.statuses) { + const status = this.statuses[statusPath] + if (statusPath.startsWith(directoryPath)) directoryStatus |= status + } + return directoryStatus + } + + // Public: Get the status of a single path in the repository. + // + // * `path` A {String} repository-relative path. + // + // Returns a {Number} representing the status. This value can be passed to + // {::isStatusModified} or {::isStatusNew} to get more information. + getPathStatus (path) { + const repo = this.getRepo(path) + const relativePath = this.relativize(path) + const currentPathStatus = this.statuses[relativePath] || 0 + let pathStatus = repo.getStatus(repo.relativize(path)) || 0 + if (repo.isStatusIgnored(pathStatus)) pathStatus = 0 + if (pathStatus > 0) { + this.statuses[relativePath] = pathStatus + } else { + delete this.statuses[relativePath] + } + if (currentPathStatus !== pathStatus) { + this.emitter.emit('did-change-status', {path, pathStatus}) + } + + return pathStatus + } + + // Public: Get the cached status for the given path. + // + // * `path` A {String} path in the repository, relative or absolute. + // + // Returns a status {Number} or null if the path is not in the cache. + getCachedPathStatus (path) { + return this.statuses[this.relativize(path)] + } + + // Public: Returns true if the given status indicates modification. + // + // * `status` A {Number} representing the status. + // + // Returns a {Boolean} that's true if the `status` indicates modification. + isStatusModified (status) { return this.getRepo().isStatusModified(status) } + + // Public: Returns true if the given status indicates a new path. + // + // * `status` A {Number} representing the status. + // + // Returns a {Boolean} that's true if the `status` indicates a new path. + isStatusNew (status) { + return this.getRepo().isStatusNew(status) + } + + /* + Section: Retrieving Diffs + */ + + // Public: Retrieves the number of lines added and removed to a path. + // + // This compares the working directory contents of the path to the `HEAD` + // version. + // + // * `path` The {String} path to check. + // + // Returns an {Object} with the following keys: + // * `added` The {Number} of added lines. + // * `deleted` The {Number} of deleted lines. + getDiffStats (path) { + const repo = this.getRepo(path) + return repo.getDiffStats(repo.relativize(path)) + } + + // Public: Retrieves the line diffs comparing the `HEAD` version of the given + // path and the given text. + // + // * `path` The {String} path relative to the repository. + // * `text` The {String} to compare against the `HEAD` contents + // + // Returns an {Array} of hunk {Object}s with the following keys: + // * `oldStart` The line {Number} of the old hunk. + // * `newStart` The line {Number} of the new hunk. + // * `oldLines` The {Number} of lines in the old hunk. + // * `newLines` The {Number} of lines in the new hunk + getLineDiffs (path, text) { + // Ignore eol of line differences on windows so that files checked in as + // LF don't report every line modified when the text contains CRLF endings. + const options = {ignoreEolWhitespace: process.platform === 'win32'} + const repo = this.getRepo(path) + return repo.getLineDiffs(repo.relativize(path), text, options) + } + + /* + Section: Checking Out + */ + + // Public: Restore the contents of a path in the working directory and index + // to the version at `HEAD`. + // + // This is essentially the same as running: + // + // ```sh + // git reset HEAD -- + // git checkout HEAD -- + // ``` + // + // * `path` The {String} path to checkout. + // + // Returns a {Boolean} that's true if the method was successful. + checkoutHead (path) { + const repo = this.getRepo(path) + const headCheckedOut = repo.checkoutHead(repo.relativize(path)) + if (headCheckedOut) this.getPathStatus(path) + return headCheckedOut + } + + // Public: Checks out a branch in your repository. + // + // * `reference` The {String} reference to checkout. + // * `create` A {Boolean} value which, if true creates the new reference if + // it doesn't exist. + // + // Returns a Boolean that's true if the method was successful. + checkoutReference (reference, create) { + return this.getRepo().checkoutReference(reference, create) + } + + /* + Section: Private + */ + + // Subscribes to buffer events. + subscribeToBuffer (buffer) { + const getBufferPathStatus = () => { + const bufferPath = buffer.getPath() + if (bufferPath) this.getPathStatus(bufferPath) + } + + getBufferPathStatus() + const bufferSubscriptions = new CompositeDisposable() + bufferSubscriptions.add(buffer.onDidSave(getBufferPathStatus)) + bufferSubscriptions.add(buffer.onDidReload(getBufferPathStatus)) + bufferSubscriptions.add(buffer.onDidChangePath(getBufferPathStatus)) + bufferSubscriptions.add(buffer.onDidDestroy(() => { + bufferSubscriptions.dispose() + return this.subscriptions.remove(bufferSubscriptions) + })) + this.subscriptions.add(bufferSubscriptions) + } + + // Subscribes to editor view event. + checkoutHeadForEditor (editor) { + const buffer = editor.getBuffer() + const bufferPath = buffer.getPath() + if (bufferPath) { + this.checkoutHead(bufferPath) + return buffer.reload() + } + } + + // Returns the corresponding {Repository} + getRepo (path) { + if (this.repo) { + return this.repo.submoduleForPath(path) || this.repo + } else { + throw new Error('Repository has been destroyed') + } + } + + // Reread the index to update any values that have changed since the + // last time the index was read. + refreshIndex () { + return this.getRepo().refreshIndex() + } + + // Refreshes the current git status in an outside process and asynchronously + // updates the relevant properties. + refreshStatus () { + if (this.handlerPath == null) this.handlerPath = require.resolve('./repository-status-handler') + + const relativeProjectPaths = this.project && this.project.getPaths() + .map(projectPath => this.relativize(projectPath)) + .filter(projectPath => (projectPath.length > 0) && !path.isAbsolute(projectPath)) + + if (this.statusTask) this.statusTask.terminate() + + return new Promise(resolve => { + this.statusTask = Task.once(this.handlerPath, this.getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) => { + const statusesUnchanged = + _.isEqual(statuses, this.statuses) && + _.isEqual(upstream, this.upstream) && + _.isEqual(branch, this.branch) && + _.isEqual(submodules, this.submodules) + + this.statuses = statuses + this.upstream = upstream + this.branch = branch + this.submodules = submodules + + const submodulesByPath = this.getRepo().submodules + for (let submodulePath in submodulesByPath) { + const submoduleRepo = submodulesByPath[submodulePath] + submoduleRepo.upstream = + (submodules[submodulePath] && submodules[submodulePath].upstream) || + {ahead: 0, behind: 0} + } + + if (!statusesUnchanged) { + this.emitter.emit('did-change-statuses') + } + + resolve() + }) + }) + } +} From 2b8f017bbdceca309abc75a6bee172043a8adaca Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Sep 2017 22:22:40 -0700 Subject: [PATCH 39/81] :arrow_up: git-utils (prerelease) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b0c5d1577..5a3b4e117 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "fs-plus": "^3.0.1", "fstream": "0.1.24", "fuzzaldrin": "^2.1", - "git-utils": "5.0.0", + "git-utils": "5.0.1-0", "glob": "^7.1.1", "grim": "1.5.0", "jasmine-json": "~0.0", From 99f3ada86b6b37778dc5766ccbb094c40b93a4de Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 26 Sep 2017 22:23:07 -0700 Subject: [PATCH 40/81] Refresh git status in process using async APIs --- src/git-repository.js | 86 ++++++++++++++++------------ src/repository-status-handler.coffee | 36 ------------ 2 files changed, 48 insertions(+), 74 deletions(-) delete mode 100644 src/repository-status-handler.coffee diff --git a/src/git-repository.js b/src/git-repository.js index 503e5fa0d..6929cc1dd 100644 --- a/src/git-repository.js +++ b/src/git-repository.js @@ -11,7 +11,6 @@ const {Emitter, Disposable, CompositeDisposable} = require('event-kit') const fs = require('fs-plus') const path = require('path') const GitUtils = require('git-utils') -const Task = require('./task') // Extended: Represents the underlying git operations performed by Atom. // @@ -99,7 +98,7 @@ class GitRepository { if (options.refreshOnWindowFocus || options.refreshOnWindowFocus == null) { const onWindowFocus = () => { this.refreshIndex() - return this.refreshStatus() + this.refreshStatus() } window.addEventListener('focus', onWindowFocus) @@ -117,23 +116,17 @@ class GitRepository { // This destroys any tasks and subscriptions and releases the underlying // libgit2 repository handle. This method is idempotent. destroy () { - if (this.emitter != null) { + if (this.emitter) { this.emitter.emit('did-destroy') this.emitter.dispose() this.emitter = null } - if (this.statusTask != null) { - this.statusTask.terminate() - this.statusTask = null - } - - if (this.repo != null) { - this.repo.release() + if (this.repo) { this.repo = null } - if (this.subscriptions != null) { + if (this.subscriptions) { this.subscriptions.dispose() this.subscriptions = null } @@ -550,42 +543,59 @@ class GitRepository { // Refreshes the current git status in an outside process and asynchronously // updates the relevant properties. - refreshStatus () { - if (this.handlerPath == null) this.handlerPath = require.resolve('./repository-status-handler') + async refreshStatus () { + const repo = this.getRepo() const relativeProjectPaths = this.project && this.project.getPaths() .map(projectPath => this.relativize(projectPath)) .filter(projectPath => (projectPath.length > 0) && !path.isAbsolute(projectPath)) - if (this.statusTask) this.statusTask.terminate() + const branch = await repo.getHeadAsync() + const upstream = await repo.getAheadBehindCountAsync() - return new Promise(resolve => { - this.statusTask = Task.once(this.handlerPath, this.getPath(), relativeProjectPaths, ({statuses, upstream, branch, submodules}) => { - const statusesUnchanged = - _.isEqual(statuses, this.statuses) && - _.isEqual(upstream, this.upstream) && - _.isEqual(branch, this.branch) && - _.isEqual(submodules, this.submodules) + const statuses = {} + const repoStatus = relativeProjectPaths.length > 0 + ? await repo.getStatusAsync(relativeProjectPaths) + : await repo.getStatusAsync() + for (let filePath in repoStatus) { + statuses[filePath] = repoStatus[filePath] + } - this.statuses = statuses - this.upstream = upstream - this.branch = branch - this.submodules = submodules + const submodules = {} + for (let submodulePath in repo.submodules) { + const submoduleRepo = repo.submodules[submodulePath] + submodules[submodulePath] = { + branch: await submoduleRepo.getHeadAsync(), + upstream: await submoduleRepo.getAheadBehindCountAsync() + } - const submodulesByPath = this.getRepo().submodules - for (let submodulePath in submodulesByPath) { - const submoduleRepo = submodulesByPath[submodulePath] - submoduleRepo.upstream = - (submodules[submodulePath] && submodules[submodulePath].upstream) || - {ahead: 0, behind: 0} - } + const workingDirectoryPath = submoduleRepo.getWorkingDirectory() + const submoduleStatus = await submoduleRepo.getStatusAsync() + for (let filePath in submoduleStatus) { + const absolutePath = path.join(workingDirectoryPath, filePath) + const relativizePath = repo.relativize(absolutePath) + statuses[relativizePath] = submoduleStatus[filePath] + } + } - if (!statusesUnchanged) { - this.emitter.emit('did-change-statuses') - } + const statusesUnchanged = + _.isEqual(branch, this.branch) && + _.isEqual(statuses, this.statuses) && + _.isEqual(upstream, this.upstream) && + _.isEqual(submodules, this.submodules) - resolve() - }) - }) + this.branch = branch + this.statuses = statuses + this.upstream = upstream + this.submodules = submodules + + for (let submodulePath in repo.submodules) { + const submoduleRepo = repo.submodules[submodulePath] + submoduleRepo.upstream = submodules[submodulePath].upstream + } + + if (!statusesUnchanged && !this.isDestroyed()) { + this.emitter.emit('did-change-statuses') + } } } diff --git a/src/repository-status-handler.coffee b/src/repository-status-handler.coffee deleted file mode 100644 index 2fda9a335..000000000 --- a/src/repository-status-handler.coffee +++ /dev/null @@ -1,36 +0,0 @@ -Git = require 'git-utils' -path = require 'path' - -module.exports = (repoPath, paths = []) -> - repo = Git.open(repoPath) - - upstream = {} - statuses = {} - submodules = {} - branch = null - - if repo? - # Statuses in main repo - workingDirectoryPath = repo.getWorkingDirectory() - repoStatus = (if paths.length > 0 then repo.getStatusForPaths(paths) else repo.getStatus()) - for filePath, status of repoStatus - statuses[filePath] = status - - # Statuses in submodules - for submodulePath, submoduleRepo of repo.submodules - submodules[submodulePath] = - branch: submoduleRepo.getHead() - upstream: submoduleRepo.getAheadBehindCount() - - workingDirectoryPath = submoduleRepo.getWorkingDirectory() - for filePath, status of submoduleRepo.getStatus() - absolutePath = path.join(workingDirectoryPath, filePath) - # Make path relative to parent repository - relativePath = repo.relativize(absolutePath) - statuses[relativePath] = status - - upstream = repo.getAheadBehindCount() - branch = repo.getHead() - repo.release() - - {statuses, upstream, branch, submodules} From c54e0782dae62a782d53175ed167d729892673c2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 10:18:12 -0700 Subject: [PATCH 41/81] Fix isProjectAtRoot --- src/git-repository.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git-repository.js b/src/git-repository.js index 6929cc1dd..205572005 100644 --- a/src/git-repository.js +++ b/src/git-repository.js @@ -205,7 +205,7 @@ class GitRepository { // repository. isProjectAtRoot () { if (this.projectAtRoot == null) { - this.projectAtRoot = this.project && this.project.relativize(this.getWorkingDirectory()) + this.projectAtRoot = this.project && this.project.relativize(this.getWorkingDirectory()) === '' } return this.projectAtRoot } From c12a5b23b485ad52675a72769d71e338eb8bd748 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 10:53:31 -0700 Subject: [PATCH 42/81] Convert git-repository-provider-spec to JS --- spec/git-repository-provider-spec.coffee | 98 --------------------- spec/git-repository-provider-spec.js | 103 +++++++++++++++++++++++ 2 files changed, 103 insertions(+), 98 deletions(-) delete mode 100644 spec/git-repository-provider-spec.coffee create mode 100644 spec/git-repository-provider-spec.js diff --git a/spec/git-repository-provider-spec.coffee b/spec/git-repository-provider-spec.coffee deleted file mode 100644 index 16ccf8938..000000000 --- a/spec/git-repository-provider-spec.coffee +++ /dev/null @@ -1,98 +0,0 @@ -path = require 'path' -fs = require 'fs-plus' -temp = require('temp').track() -{Directory} = require 'pathwatcher' -GitRepository = require '../src/git-repository' -GitRepositoryProvider = require '../src/git-repository-provider' - -describe "GitRepositoryProvider", -> - provider = null - - beforeEach -> - provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm) - - afterEach -> - try - temp.cleanupSync() - - describe ".repositoryForDirectory(directory)", -> - describe "when specified a Directory with a Git repository", -> - it "returns a Promise that resolves to a GitRepository", -> - waitsForPromise -> - directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git') - provider.repositoryForDirectory(directory).then (result) -> - expect(result).toBeInstanceOf GitRepository - expect(provider.pathToRepository[result.getPath()]).toBeTruthy() - expect(result.statusTask).toBeTruthy() - expect(result.getType()).toBe 'git' - - it "returns the same GitRepository for different Directory objects in the same repo", -> - firstRepo = null - secondRepo = null - - waitsForPromise -> - directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git') - provider.repositoryForDirectory(directory).then (result) -> firstRepo = result - - waitsForPromise -> - directory = new Directory path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects') - provider.repositoryForDirectory(directory).then (result) -> secondRepo = result - - runs -> - expect(firstRepo).toBeInstanceOf GitRepository - expect(firstRepo).toBe secondRepo - - describe "when specified a Directory without a Git repository", -> - it "returns a Promise that resolves to null", -> - waitsForPromise -> - directory = new Directory temp.mkdirSync('dir') - provider.repositoryForDirectory(directory).then (result) -> - expect(result).toBe null - - describe "when specified a Directory with an invalid Git repository", -> - it "returns a Promise that resolves to null", -> - waitsForPromise -> - dirPath = temp.mkdirSync('dir') - fs.writeFileSync(path.join(dirPath, '.git', 'objects'), '') - fs.writeFileSync(path.join(dirPath, '.git', 'HEAD'), '') - fs.writeFileSync(path.join(dirPath, '.git', 'refs'), '') - - directory = new Directory dirPath - provider.repositoryForDirectory(directory).then (result) -> - expect(result).toBe null - - describe "when specified a Directory with a valid gitfile-linked repository", -> - it "returns a Promise that resolves to a GitRepository", -> - waitsForPromise -> - gitDirPath = path.join(__dirname, 'fixtures', 'git', 'master.git') - workDirPath = temp.mkdirSync('git-workdir') - fs.writeFileSync(path.join(workDirPath, '.git'), 'gitdir: ' + gitDirPath+'\n') - - directory = new Directory workDirPath - provider.repositoryForDirectory(directory).then (result) -> - expect(result).toBeInstanceOf GitRepository - expect(provider.pathToRepository[result.getPath()]).toBeTruthy() - expect(result.statusTask).toBeTruthy() - expect(result.getType()).toBe 'git' - - describe "when specified a Directory without existsSync()", -> - directory = null - provider = null - beforeEach -> - # An implementation of Directory that does not implement existsSync(). - subdirectory = {} - directory = - getSubdirectory: -> - isRoot: -> true - spyOn(directory, "getSubdirectory").andReturn(subdirectory) - - it "returns null", -> - repo = provider.repositoryForDirectorySync(directory) - expect(repo).toBe null - expect(directory.getSubdirectory).toHaveBeenCalledWith(".git") - - it "returns a Promise that resolves to null for the async implementation", -> - waitsForPromise -> - provider.repositoryForDirectory(directory).then (repo) -> - expect(repo).toBe null - expect(directory.getSubdirectory).toHaveBeenCalledWith(".git") diff --git a/spec/git-repository-provider-spec.js b/spec/git-repository-provider-spec.js new file mode 100644 index 000000000..e1d0168a9 --- /dev/null +++ b/spec/git-repository-provider-spec.js @@ -0,0 +1,103 @@ +const path = require('path') +const fs = require('fs-plus') +const temp = require('temp').track() +const {Directory} = require('pathwatcher') +const GitRepository = require('../src/git-repository') +const GitRepositoryProvider = require('../src/git-repository-provider') +const {it, fit, ffit, fffit, beforeEach} = require('./async-spec-helpers') + +describe('GitRepositoryProvider', () => { + let provider + + beforeEach(() => { + provider = new GitRepositoryProvider(atom.project, atom.config, atom.confirm) + }) + + describe('.repositoryForDirectory(directory)', () => { + describe('when specified a Directory with a Git repository', () => { + it('resolves with a GitRepository', async () => { + const directory = new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git')) + const result = await provider.repositoryForDirectory(directory) + expect(result).toBeInstanceOf(GitRepository) + expect(provider.pathToRepository[result.getPath()]).toBeTruthy() + expect(result.getType()).toBe('git') + + // Refresh should be started + await new Promise(resolve => result.onDidChangeStatuses(resolve)) + }) + + it('resolves with the same GitRepository for different Directory objects in the same repo', async () => { + const firstRepo = await provider.repositoryForDirectory( + new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git')) + ) + const secondRepo = await provider.repositoryForDirectory( + new Directory(path.join(__dirname, 'fixtures', 'git', 'master.git', 'objects')) + ) + + expect(firstRepo).toBeInstanceOf(GitRepository) + expect(firstRepo).toBe(secondRepo) + }) + }) + + describe('when specified a Directory without a Git repository', () => { + it('resolves with null', async () => { + const directory = new Directory(temp.mkdirSync('dir')) + const repo = await provider.repositoryForDirectory(directory) + expect(repo).toBe(null) + }) + }) + + describe('when specified a Directory with an invalid Git repository', () => { + it('resolves with null', async () => { + const dirPath = temp.mkdirSync('dir') + fs.writeFileSync(path.join(dirPath, '.git', 'objects'), '') + fs.writeFileSync(path.join(dirPath, '.git', 'HEAD'), '') + fs.writeFileSync(path.join(dirPath, '.git', 'refs'), '') + + const directory = new Directory(dirPath) + const repo = await provider.repositoryForDirectory(directory) + expect(repo).toBe(null) + }) + }) + + describe('when specified a Directory with a valid gitfile-linked repository', () => { + it('returns a Promise that resolves to a GitRepository', async () => { + const gitDirPath = path.join(__dirname, 'fixtures', 'git', 'master.git') + const workDirPath = temp.mkdirSync('git-workdir') + fs.writeFileSync(path.join(workDirPath, '.git'), `gitdir: ${gitDirPath}\n`) + + const directory = new Directory(workDirPath) + const result = await provider.repositoryForDirectory(directory) + expect(result).toBeInstanceOf(GitRepository) + expect(provider.pathToRepository[result.getPath()]).toBeTruthy() + expect(result.getType()).toBe('git') + }) + }) + + describe('when specified a Directory without existsSync()', () => { + let directory + + beforeEach(() => { + // An implementation of Directory that does not implement existsSync(). + const subdirectory = {} + directory = { + getSubdirectory () {}, + isRoot () { return true } + } + spyOn(directory, 'getSubdirectory').andReturn(subdirectory) + }) + + it('returns null', () => { + const repo = provider.repositoryForDirectorySync(directory) + expect(repo).toBe(null) + expect(directory.getSubdirectory).toHaveBeenCalledWith('.git') + }) + + it('returns a Promise that resolves to null for the async implementation', async () => { + const repo = await provider.repositoryForDirectory(directory) + expect(repo).toBe(null) + expect(directory.getSubdirectory).toHaveBeenCalledWith('.git') + }) + }) + }) +}) From e3abcebb76001f56fe1d4cf6ee62f586ff790916 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 11:28:32 -0700 Subject: [PATCH 43/81] Restore behavior where only one status refresh happens at a time --- src/git-repository.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/git-repository.js b/src/git-repository.js index 205572005..f831e1709 100644 --- a/src/git-repository.js +++ b/src/git-repository.js @@ -85,6 +85,7 @@ class GitRepository { throw new Error(`No Git repository found searching path: ${path}`) } + this.statusRefreshCount = 0 this.statuses = {} this.upstream = {ahead: 0, behind: 0} for (let submodulePath in this.repo.submodules) { @@ -544,6 +545,7 @@ class GitRepository { // Refreshes the current git status in an outside process and asynchronously // updates the relevant properties. async refreshStatus () { + const statusRefreshCount = ++this.statusRefreshCount const repo = this.getRepo() const relativeProjectPaths = this.project && this.project.getPaths() @@ -578,6 +580,8 @@ class GitRepository { } } + if (this.statusRefreshCount !== statusRefreshCount || this.isDestroyed()) return + const statusesUnchanged = _.isEqual(branch, this.branch) && _.isEqual(statuses, this.statuses) && @@ -590,12 +594,9 @@ class GitRepository { this.submodules = submodules for (let submodulePath in repo.submodules) { - const submoduleRepo = repo.submodules[submodulePath] - submoduleRepo.upstream = submodules[submodulePath].upstream + repo.submodules[submodulePath].upstream = submodules[submodulePath].upstream } - if (!statusesUnchanged && !this.isDestroyed()) { - this.emitter.emit('did-change-statuses') - } + if (!statusesUnchanged) this.emitter.emit('did-change-statuses') } } From 2dd73de418883085671b6d248845998611e72142 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 12:13:26 -0700 Subject: [PATCH 44/81] :arrow_up: fuzzy-finder for test fixes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a3b4e117..0fb37392c 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "encoding-selector": "0.23.6", "exception-reporting": "0.41.4", "find-and-replace": "0.212.3", - "fuzzy-finder": "1.6.0", + "fuzzy-finder": "1.6.1", "github": "0.6.2", "git-diff": "1.3.6", "go-to-line": "0.32.1", From 07614b3b38577e1c85fdf85f6b707a4ef8e12223 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 12:36:51 -0700 Subject: [PATCH 45/81] Update git-utils main path in startup snapshot blacklist --- script/lib/generate-startup-snapshot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/lib/generate-startup-snapshot.js b/script/lib/generate-startup-snapshot.js index 7701b6a34..2905bca1b 100644 --- a/script/lib/generate-startup-snapshot.js +++ b/script/lib/generate-startup-snapshot.js @@ -39,7 +39,7 @@ module.exports = function (packagedAppPath) { relativePath === path.join('..', 'node_modules', 'debug', 'node.js') || relativePath === path.join('..', 'node_modules', 'fs-extra', 'lib', 'index.js') || relativePath === path.join('..', 'node_modules', 'github', 'node_modules', 'fs-extra', 'lib', 'index.js') || - relativePath === path.join('..', 'node_modules', 'git-utils', 'lib', 'git.js') || + relativePath === path.join('..', 'node_modules', 'git-utils', 'src', 'git.js') || relativePath === path.join('..', 'node_modules', 'glob', 'glob.js') || relativePath === path.join('..', 'node_modules', 'graceful-fs', 'graceful-fs.js') || relativePath === path.join('..', 'node_modules', 'htmlparser2', 'lib', 'index.js') || From ce105ab9140302de496130e9079fb3b9e9fc91b5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 14:18:40 -0700 Subject: [PATCH 46/81] Give an id to each GitRepository --- src/git-repository.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/git-repository.js b/src/git-repository.js index f831e1709..057c5fcb7 100644 --- a/src/git-repository.js +++ b/src/git-repository.js @@ -12,6 +12,8 @@ const fs = require('fs-plus') const path = require('path') const GitUtils = require('git-utils') +let nextId = 0 + // Extended: Represents the underlying git operations performed by Atom. // // This class shouldn't be instantiated directly but instead by accessing the @@ -78,6 +80,7 @@ class GitRepository { } constructor (path, options = {}) { + this.id = nextId++ this.emitter = new Emitter() this.subscriptions = new CompositeDisposable() this.repo = GitUtils.open(path) @@ -117,16 +120,14 @@ class GitRepository { // This destroys any tasks and subscriptions and releases the underlying // libgit2 repository handle. This method is idempotent. destroy () { + this.repo = null + if (this.emitter) { this.emitter.emit('did-destroy') this.emitter.dispose() this.emitter = null } - if (this.repo) { - this.repo = null - } - if (this.subscriptions) { this.subscriptions.dispose() this.subscriptions = null From 6a86a1c7bf61e5184282824e77a7416bb26612a9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 14:19:05 -0700 Subject: [PATCH 47/81] Wait for repo to refresh in test --- spec/git-repository-spec.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/git-repository-spec.coffee b/spec/git-repository-spec.coffee index 47ca84580..e4d1e0c7f 100644 --- a/spec/git-repository-spec.coffee +++ b/spec/git-repository-spec.coffee @@ -283,11 +283,15 @@ describe "GitRepository", -> [editor] = [] beforeEach -> + statusRefreshed = false atom.project.setPaths([copyRepository()]) + atom.project.getRepositories()[0].onDidChangeStatuses -> statusRefreshed = true waitsForPromise -> atom.workspace.open('other.txt').then (o) -> editor = o + waitsFor 'repo to refresh', -> statusRefreshed + it "emits a status-changed event when a buffer is saved", -> editor.insertNewline() From 05a81485ca85598e7e22d6902df86923f8c0d8f2 Mon Sep 17 00:00:00 2001 From: Thomas Johansen Date: Wed, 27 Sep 2017 23:51:08 +0200 Subject: [PATCH 48/81] :apple: Fix faded app icons on macOS 10.13 --- resources/app-icons/beta/atom.icns | Bin 1194491 -> 1193792 bytes resources/app-icons/dev/atom.icns | Bin 1145794 -> 1145071 bytes resources/app-icons/stable/atom.icns | Bin 1198254 -> 1197490 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/app-icons/beta/atom.icns b/resources/app-icons/beta/atom.icns index 69abe031ebca8ad211d139ac69bb4006e9d8a5fd..6737fa27f745ef073849ccd1b3abbceec8d2d8e2 100644 GIT binary patch delta 3420 zcmah}c{r5q+kVE38B6wUVu*|+uU*z8WiX6okStjT@fu?pOO(b|c9LOCvZbVrP_`1U zgjdMECE2ni+t`ZC_q@w-e81lx-|;=iah}(8-1l|g*LmLeeLVXNljcf}>8=nWtdr(WY#}&DcioumN0l+dK>lfdD&Q&Y&>srE9)Bs>z zjb?!TuXQCw?(3{t|JMvf9RQ#K24IsI0HIYjx3@P}rWFUUAWDD5Fp%cNVG^ZxjiQOFbL;xZ&kimouNZbM-noF5X$dX1p06{HgF(HenSOCKI zF`Ehb$QA<9?A?w4 zRUl_UJ+nwqnc^&{aaI9Ti);sVw<8n5yNule-E74o#6fGwoDI;{$ZL;L}O{<2czCGOaOWY+f2K1-A1_y~T@F`J%kS<{NUM1l`WEuln`#XUq_8dX zrcYg8c&F1W>vcB+W6^swx9*mG(`}_lr}~GHdXio{6sBmz^ZM(AvNuui9>*j*cRpot zN!7}wj_X@VF=);g>4D`-x4UsTf3mvQeRILfCOa-sk+9c4+Z=eOxdom!8qv9p`3zJ} zvEx_x>=hL}I7DS6QI*e><>Jy@JSh|Jq%= zPu7K*>E0p!{8%r|!0A7ohGtg}tBB)IZzS1QjYMJ*3njGv~N{(D3yKR}9tSG{jlHQ@r-4YCm%+ zk)a`H5;{9ldcWFeaNZH`)*arc8h!s%L1FZqe2p23<-+RY57Qa1(noAQMRsXxQ@gUi zKx-oawFTGK*50tec}2;AZOMCG93)MX@UziGCc$dSh5CqF?JGz9+XR7ydH>$rP$T1w z$UkNja@XhFLwcdvk*%#b8U^iqwGB}%B^dqbCBNecUpf<-&M9cQ8k@Bmo1nTW--M8@ zMLi66H3(f%3iK^*+2wg2!EZ6ZVfau$z3pYCp^BN5UTavCq142mH~BKA5|3}E2F=`C z|7=pone>3v64Fo{$`N$=rgZ!Rt?$+nu12IG;kO|kkv<43iCdNnFdU`Qli!qR-DTz3 z?SMIza&N3#j!-Z?AE5KRdp)}^PY9ymp!NGT+2`oF3S$ogcS3#b-Yw(us#BTzyd*^J$F;-`8{@idq-<@B~icJPg zt4*z1Saq)8+&z!%UKL&Gj$fy8?$=`*B(pI!yNva5c+r8zl2@GqbmCp8W}KUMZsz)J ztisJpb?oJrc-z=eI4eUfwfJ5tpU{`mbl@_pWxBs=7k)PakEB!xW0oRTJz9y6=QAWJ zAB0(cl%;+*KIgr#zjh^L)U0r&JEh3NZd)q}y1L|^;w^pnf#o^L$1`WY)%Jum5}C$o-my)W2Vv^_nNc0#AP zM0DQbOvX!NC`o|wp)F1>x6~!RD_Kdh;B{i>I%lrlp**}w_~u2|^Q3OO!Jb2jA%;>= z;dRhTS7XlgkNtdRUT-V)ag7g<)I~|d*Nwn+LNEF-_AW@clic? zDxLnQ|;(8H?yq3#Qmir7$0uar>8n^@wOk5 zH%7)pyA^i9h$S25^!6HLWCxjJ(6iWflKk|+*IaAL%9={4y{TcrpjqxDQy^F7L#8vc zE)=Kbmh>CJpJTk+!D6%stz2omAS&&@rfsKtOzXM{SM7!?t{$J<=Hn!D(k9`RMXh5x zQZ{X(Az@lX2Y=>;n9Hv0QY*njy8bAaCRw5j(cEQqqIR^pM8yl>_oY;Bg=Bq3}V z7rZHRdY9BAV&}LszpSgzjFC-3KT8R+p0JeCYUCL>Xi*ZsD){5NNGEfWtNUGtAM@n0 zbewQtr3&`Vy~zXP#yEyqRcZQREcIU4cP<_yg# zm$7pEAzKPJ?(C)A@eivYnpgcflCm}L01-*wMG4A=a`t1gnj8Ba(w^uaAHK8~?v8zT z6XR5#e~Nl?B5S+qla`CXwg>BW4;d@+o3Kmx!o#FDV}@8NTQx5`D=b z%F2Tf7ATArepJ*R9p)8G_6;D}qXRsC_acOl9nkK<1Rq~;*cKfU5XcWeZWTR5u<`K= zTtt&XiNpbR4$dRoyq7F&6#cv+$c|wAccmSgKy)XQi@Bm%ig}`;(J)(*yT6yMFCl=` zFDN7|;z;?Y;IC2pe^M{!p8D%A+EeU7B*&H5ni4K|LW-z zL)iug1cXFG*!V=H6;w6#E|^+7dwTo)P9%}9MkQtz6hC>>)cTik&@AMi2#Eh1!2tg| GZTlZBS|WA; delta 4128 zcma)82{@Ep8-5K9Bl|Lh(AcxoMApJgBWt!6qFuJEHL|3<(I6s3GswP$DN7PQTgcKz z_NGGiNoCDaV*VNRW%>O7|DEf4-g)ljoaa67b>>{p({a7U89mU^#uW>2sV!nvU7P@b zjtQU+)$E~#g`sR!S`Gk=Sxc0av6RI~gH68>?b$>nl2;*xk__znZT`hB?Yg^Grcmy7 zug75b%z9#GBko+UcSfwo5whzG$^tQK4KV-!tz0E4q0}8fWB%Sy3{-%&LjaJC=$x79 zjF6?}j#$M#005aF6d6LS=|BRz5d>raSOK6U$_T;)0i9RA6vN7wqWJ-U$vVG-ys$v- z$hTg@{?Y(I^ugTf3(P$b{hR;w3&YQ^sF8s%#@@b<^!=IFY z8-&Q4$rK7%XvM(Pvbjv5w5)6-Y!ey$&4p1{5S-n_@SBkE;0Cf6)>C;f7 z&17(uG>TizH_PH`H)B`1+IOq@X4$(9XK}>?u5oTWtZSr|bego0S&vE8>+wcZz2V2+dzYB@Hc+yAqZGQY)AuWr;M?21N#Ry z((V&#iNQb2x@3^HI&CPdHGqFSgYv&*>XUzy8jXKXr@0sdHtgeDz^~$~OW@ahsww>1 z$-g~N-MiJK8U7hk^<7=sJ@R`vI@9h8*m#MDDH9n6-K_!uCYPd53W~Eb*rN`i0D9|F zCn%EMMYjYsOrX0}0bo*@K(V6RAm5j_rkmWN2G9fN2&u-94QgZ-!Nm%-}}#3dBXlOl?%W*@qJ#p?FkoErJy#jSG`tcLXz zH(>G7gUZs4iM!s1-Rj4m$+Y|6%m{Nqrx{A8)#AwSlFlH~@83$Hr&b}~^^j$^L)o2}e{D^|&_);Bt_y2|pn@9cLiK(epLz1-|VzF9M* zvF$Y|0YU#5hy|3k7d2{*ikmJL+<@Z{d$t|~3iDwBHgyKUP8Z5g?{n|0d>e-y?pyM+ zK7J338Zf!zd7VxCN`SxE5{6NzTQ^2aS9{>uxkqpoGyUb^+}$-kF9RIV+}E?zurgU4 z4|A+fkxRCIFNJ6y(bNnrDSd36R?KNJ^J+dG4D3Ta7*`fVrU^|GmT~Tn8V^e6Ri4-Q z8C2dc0Y~d9HnD%{f7G6kUvNezH;#=j`14r7BP)qoVr7HE7OhtGu@To>vDYqSw^{hf zet0J*x$8@fUY|0Ly#k`OLfH5mk|6l(Ue^xNl=P7b*0RF7n_=kOYxKj~a66X*n;P=& zF%W+GXf@vdD{|u!ZmZNCVEZH};va2= zi(9UgW?9;jNA{mcwp?bIwtpg)ppH5%WFvq$o03MTKo>mjZzeGALvaO-ISyiETFZyt zOIl1fR>csykQg(6wV11ri-f^ei445+2x!RNu=s?&?C_+F;6=$F9}V_!iXAwsBdg5& zSOR`ICQUkdWMD!q%PY~u|G`lY2c5cO@5>PgP7BMVvrVvo6lAkb#$M9$?!=Na({GDB z{1S|17bPx-Lb^WO-(&Qk#llG7 zk&jo{Lj;9IWi$(wqDDM1^=HNJjY9)mF`1m|LMTgqjL(3QK_dv;i^aonJQwvergg8k zbvK-c2DwMv=gf;5OLbLM7#l_81XyxCf3o;-@A=f9(6*fZF59x88CS^g^TS~{weZ)$ zgfdtjA+^ZBEN+@1;JGG)WU%`{-v>p|8dlWoqT?Pm+9a@xCC&DcnW8zmV0OIIPvBGY zYmXr@t;Zs^J2iSPYNZPvlxuIE*0_-)rX^(wW9Q+UFB3+oJT)w-&Hs$4=r7XSn>Q5K z8c|E?L-6}HX5n`dGpsCI?c(ff3!a-!g|-TY2*bn)F6dr$60AOc&YOwXTs|Az!L%KY z#18m8+b`hml+7w49f3W4^VvZDj`&Zy%N+_vA|TzVudkcrzC+DlmZW%(dr6=04*Z-U z8hfHu$SB$d(et2=gVRkmGG6D+sZfb;z6-75iP1Ypq~lfX+9O50>5YAG=#K2~uN3v{ zy|isLZ9ngqHm-A9;ySTs&=4853>KCoJsl!dPQLG+N^l6o3uJNl2sb6!NpXp%8JlUM z7zXV*c%+W^^_?HTsv)^^j_+!-d2IH?6K$Wl9p_c)WDgK=;klE^-Ts!W6+iC6BY}uQ zCBxBE{Z`EC*V1`YCB3Kg`-b+jKqaoc1f&r74;`s4+IGk`qNnN9>y#JQ8^HIb0%|PL z&H>%AnPXV~#nI?kvJZ1+&64sSMXYdrA@^kb!~h8q#w9eLRW6*q1Yxf0@LxbUXo%nr zu%TE&_^H4$0pfg8cYzqQ$XW!ItkarFB07u(Lvj| zruit$_dR8V^5F`y4!ybHD8CV71N%=FNv$(+T&E4@-l))}m^5RzGs`p79LvT!ULAW= zB$qy;eUdNzi$9;S%@49$O7yIr#{<5-s8C&V@sEM^?n6xI+r;mNusMk2?J8VUw@AP? z%`?}?DR)5q8w(?9++QCiTxAfwp(KvxL^Wg|*2yHkLNjEXCf8YtfLSfoD6^iIkBGWF z44xm{lQDLe=$V)a{gci&C0kOxe;nQVp_wz=;bEE@Ug4UCO8vXu^Df3wHi$d!8N8yG z7)>91x@l80nacb+BhP)vCnof;Ufx3Lp+h-;!Ik-jI z+FD~4JUf06UM9SEi46i@j25*S^trZOGLq?<=a9kkm`P>IgiTOnURCJmdAdD%ilIN& z)CO-%>51vYu^K?QeHkoV9DPd`?26wBxpp2Lbspkc`r=}knJ_3kqJGM4@3{n3^OJ%f z?c;Y}JR?x)<+`re&YUq?%4z?vHq+pi(Z6FYwD zL8<443=7rJzA}9HW>$+>*VS@89{&B|({b1MNBR8xJjL2R*$wsf!d}-kIaO@Q^cTMO zx$FtfTdnf@tt-Bx2N?UJ%Si1)s3AZl zU+mo0zss}!QiiFZMGENI34i7Ue|tfiw-Gs0(%26V9x5%C;qiTvf5ha`<(iX*$0%ju zHWomW;Y6PE;Uis)`aagSZGP(fAqh#Zb{s1G>00_=|03uwHz47|7QVGaZL12Bz2?o1 zXD`yb{vzKO-~ess%d4V!8)P;57`34h`>NA!+1F|^7aw|dfij;Bav|J_`;N)KavUZ~ z^M#Z_Q^Hz}s|QPo<-&f8H!Cdio%pla<}?W|J5%M7^E7g8C>F#w+`*LD=5PzK{CDLF zKpf8@qvUN@?}#c#qy4;wbAwKYd|P5UO3Ga}{y4w?zEIJAn|Ddar!6H$c{MDOMVM0l z!n~En1&Xu}mn>k||vQRKAm6sISQrV_;x-LjbTpm%SqSZ>@H>l%_pQS04c8 zx!Eq~{w>v~)`+p^Vfcgpr$)o?2LOQ(GjLEG06$nZy|FP}b}#{e!b`Ri_|UBqcq{;c zzoGL5-@pN&D1|Rj1n6v~fQn9T8XuB74AL+cB<^1{2)+$6)A&%PCfFpfr3vJu@C9=K z0M<ZNVj(-QCM_WTc zKHULfzs~_5|`ln!WJ9@Gr#iQ$X2vo5sbF)pw2{O zVkSd)Tgh%Ro3vqy+hY&&;ufVJ2TNkhmjqH3#5c1lNDmHd){qW7PSmifxoYxc4ST!! zRn+)BFZsv6k^AcWo@^iI!Q+@Zj^3R6Dks0>*mj*wRdTVVF;-MN3nEYkPVSFRrWbwA z=pG(rm#R>r^xPNn3}ecu&gDI$_?KfAk|rfj)-k(aDM!%cD9eYEOVTrW37gku97uSn zjWOrV+ZW4D2SYVH_?ah6>X^=L_?b2Ir*8vO*J|5Z{jF+sT3ufY>%N32|6-kAf!fvi z=}=npp`m;g+M>eTs^t(*%4PRxK~7l)JORJ`tftPv7i+%g+^B~f5O5dwzJvEL_@g|x z!7jmqg}T^S7+nBSIr-yEx;5uV^4k!3I5X^$(`>Pz%jeGX!3O%x)y8(R>+lwYJ;#z> zx-TQB^ZanqOU7_=w?};dN{i!}v#-0<ne&V`?RDj^Lql^9(;kKi_K^sWAO zKO$~`_Cpp+Jj!rvGLbV|wu_0R2sINE9|m26^w+9pjMO&*Trc;p_p)I**O2+?B(9!d)1zgr;fJl zq#Z`vq#Z~oe)I~uIPD}>l`)8~RU1+5(6IX5oCUMVm#rJVAZ(R@`q4nOg+8~KY#f2L z=Fr7Wt!LLCd@%DB)B5wgoKToyzn)rNfM?YQOcaVF{-O1o%&e2>(8Ukbnv9!_&;l(= zpKj^Uf<*ZA!LmiFW7=v^Tsq>AbbE_^0ZsYHi_+XNjm=s|&a>~B3{t>rCsoeT=wy#B zj1F_(9vhC)v603I0mdK7%XQaLhso$K3xTh$s4~WzZPDcP%axMHaM9LOE;Rz~wrH#{ zaqYpGx8aI~qq*(VVLPkRWy}hubC{k1Xsf!q`y74auDudZ{D_gMg;i29aqB4+TC|2{ zS#pdR`}?q-fxdjvZ@qE;m+L3xzpu;+RtZEY8cKL)uiprQZ+FCGJpJAsAYA3`nx!xD7%kB5yt3LBO;FRog^Z1F0&rKnFvzD$aDyW89rNp8Qu8q42 zXOZMip6{0J=Bb;H*!pajI!^tn!|Ab1NHcT`2_a*?iXJv{h#M~s(%4HY=ZFp?BE7tv z(pcIG~HM1g|) zD-AUNOWY1Qw~RI!l$A{PmUQ>Et!YPT`}|3vwItH8Q|Wx(s!0MB8fxdAR|pX`Du0SJ-818ArvAL~NySU!Q*f;|Mp1^7M~o zxEH4MqL&R~UPi>YZ*EdgInVwpaV$h;FL5LL&qJ934&L)C@@val-xlbGAt$b*&-!BZ zrUu`_;Rn_YlpPv4Le;7|3C`u+b(`L-84CF(hTU!xqgT=BLkkU1RZ<%-EwtW`5*>bchAL+t zFZ#Vrq0`Uds9)dM*$aOfT$X1wHm=%l@7cKNS~sX=J*`?e-aMr^j+0lKon3u7q#Y^r zLZ2NysvzG$i*{=9@5rBS{ub!>Wv;AjSKd<@)@xH^Q9Y( zK=LM&?~27j?~2C?#0t6uc?bHr5DCFSeKI>`<<9)A|6DwcBT#?#`jI_PI9m`2;h+QW zu*iF|sM9B$9sV+MPD1>Iv#npGpRd_JetNO;E@8pJ;js{eBxbjko}tOH<4$LNFI>Dt f3L;alCTA7iebmHm`xzVz3;DYO3jfsq7S4YFxPckS delta 4124 zcma)82{@EnAAfDb*vih-H5ihuj3qPKGRR36-cAy06Tw~f3nLv(^tzh zS~_Jd1}SrEiMjQ-Z>`>!vKFT(tSzVjq^&lj0RW_Sg{XzlcK}KIdqZgu7-NS3ppeox zH`kYSZk3>g48oJ)GgzkC_*EpcAO{?q_KqUh5~ zy@eu)zxkiPP~ztmm63fWLRUG&d)BHn8c3t{i2rtIOsv-#cw*Nt!yn@t$u!#dA3qEn zog2$ETIY|A5H^s(UmStH!j^>%48N#_EdY@H-(?yt8-UFIFMSq*+eijiNL=PhzEPG* z-iTe{$O9|+M%lo+qxZuDu5zwFo2#TB>1_XcW-acoUyIkH`gMO6h7PVar5mkA|ICdb zGyf!4m>CE_TtWZLhyWluzm5i1;dxQ|A&6c@tV^RAr;NRM9s3(LGp-MMiQ(VOnq-)< zI%DiuZ2*6JhLwKG^rz?sHJbmP&Tz3uuiM9b^v~ieOAuFmsu|+y$-g`hqxAazInlZP z`g9}4wef5C9${P$(BTSqCXUxoh_VI%n0Z|2f=TnR`skVI0W40g&a`czN0p>yPj0thJb#lB2oQ)ok3K2N_&Or!L8Gw3fW*BgBNK36L zLVqP_Al3m70KzhiU^2Fn{um#`#X%1!JviyXMGtOz@X&*o9-HW~xgv;*Z{C9`lK$QT z87Bky_`v_aF1;4a*g((P9!jutIATo^``+Q=P14@=?xA>~Jua%W51D|$q;bNoUCWbE zf}XRT(6@x&;@!wbiDcH>L;3+W~wI$mM6^MHKOeEL@s)k9XJ*+s}?KL@*tzj2*rxg?_dZFacr8jS#5#QfG4LPHi zXFgWsY6(mDY%>~n;Z*Ek3h%Kl{FP3fYNQUzJpK4c@&ov?0xdW3BTDw=*1|D4&^-k@&3saJ3FmTX3@UWuVWVa+VQ>I zefm^<3i;s40SSAP5B?%2RHy$k{~8+VD_F9G=S)CCgJatS6WW>uZo|5S?NAYj_&HzW z2zKMVkD~-7)F)TI(>>8v>WX?w>ZlP1)#u_TQD|qwJtD5XQN9v$#qT{x+m5Tt;jFiJ z=eDDV&t=DSFG#gO#C0fQ%^>FFYrI?{eu7hkBYmp8I*&h_6KgQzTG_CVRn2J9H?k=) zIvQWvX%H>(Vt=ghMJH{yO?|v&w*QneFv@HhhqHKo#jNSWI&xowg(zE~8d9N`= z9#{*}QUzv*wvzp0c+(|td~>k$*4}$h6l|gwn8o3=c?*Vq#7^zkjjN<8+y5 zG``3^n1Pwc`mi+ls8f7qzJccMhNi5V&(($}76>Tgy368T3Eg-X{TTf=D2rlP&hlm7 zActx0@4?4~4fXeGHs*Zf_GVE*H7*WrEt9d;;-9*G>C(rAZvx|of-J7Omp|sbAKZ8b z^4e{$^CjMyC*>M56fnvc2G>8IepgD`Cu6tgjhE2Ss7}%aMR)x0Y~+$| zae*eexAEllJZrgj{*HXA*O{2pEKW`>q3p<%bmfVm*{96HP2eKVL%2=SH6on3ez#^+ zVm@1|e^N(GVnG#Y+}!qFv0z7FxHC7OVarw! zBV?Epa*rxDhY^%a5yVz)5tYh)wp4qe!e*vzPdN2s&|aLTcepU`fN>meX>MNm8Xukw)SbC&UlrBdiO8KojVX0)b2OxSeZd$`!OLY5-ukz7zQ z#pB&@ah;mvxM#ZIL)(#4M$Bi zgs%Ch_1udV+*vY+69cQWj9jbLySv=l)U9`D_kOJ`Cm#q&2pPF62oEozSQ(hy5H=TH zJmGpc%Gy|Oyz#Mb`?(qOqXGAQ4;tg5sxS|=bX!bb?Bnh05*#wlP}dts?v<;$a{8M! zTJ2$KWSbWW)M+c<)25cn8SB(e4dM}#N~Closov*3@s`parZRBCzuZH9XGpvk)95kR zO806BrB2KjWt3Ev|1v%=G|GE4CeoJFfarX6S++4~JKrdeQIu7j?%Td3FRAG4IgPHz zeqUR^2?rKWUP(je2$kT5m3pb(lW@W9=d&jGiSoFrEKb5H&z_4F;l}Kf`^0c(r0X)$duW0p`&%`H$}7TD|e)jvTQ4jcD2-H z8EN4WLti&7ouoC9zV?4E==H9fpVV||LdlAjgoD!FdDy+a#~tuh2OXuYGg^iCiJb{> zfr>5m+P4ELn_h;E__SM6@+h_@gBZL+%Q|IosrdN*_nn=!)QAGR zI+1|7Ld+cZLiy~ePyu%j1U@_q4mbKTOD3^6jSVg9$x;vRS$_8B?I%O>{D;DcE`gB- zNSWtNl|d^>h@1*W4`Kbryd*_)+N>~n`eloGZgmBXDI@-RnuqVCKBI3mHr!`$2jdp) z>xrZ{kFb75-<68s`xo6DNrwP~AXNcif{W2c{)-um-m-oif>fh7m*GE}%YQLxb;qeR F_#d^x<>>$b diff --git a/resources/app-icons/stable/atom.icns b/resources/app-icons/stable/atom.icns index 2f3246bb82b0a45775f6e424a529761a2ffa2004..73ef963309813e418e4d2ba7d1a687bbcac19195 100644 GIT binary patch delta 3398 zcmaKpc{o&U+{e$1F*6LBgl6m^lx>9KA(SnTC0lkLW6RbM#*$_1OM|hF?2JefDMDpS zBJ{|<6j`%WJhDw$<~`5yUf28Ad#>yI`Q6v=cg}r(zxS!l(0@wSM|-*u0su~(r=BkI z7XW~7H5Vnyfon1m<#@^m$6W#dlMfkaT>XuF6S%wr0PI@lA~nFdkp_pzZq}v=Gvxm^ zNH|3RVDh_Srgt?OKqyx&(dkQ7%CP_xQF#>0fF4yMq5ue@H<1DBMF2oaGJ~lEkUL5S zCvrJt1|(+!#Kbm;?SC)^uEr>229zQNzQlAS1!N~PVA%iwZ%t%CTH#Ru6rTOh#@X;# z03k&|j%{y`QKX`QrY23!6(|zSqzEuUm2!!SCvb@`6iDt&JIZdpu#nKi45pojW8L2vQYOym-|$fBmg2fLSaBg z1QP(rrL_zOWbI-c0O4QFVn9~;V*m(pFoyvdL`DNZN-hIP0k4B^HWF2NMMVG2lrt(cs1RfM@q0Ko^)r(9a?UbSAq9dR&wN-C}w`-#wTl@Mp#B zfN6GO1QB2~40Q{PxkbfL02Ev=r+sRBd#XK051t4#wO{1iVnMjb;{cWq!MQcpV7SDs zYcNpEpx~eoppc+gK(T^Cfnsagy2h^i0Wy`VjOD+xLRD`6SO%Kwz;*hQJ@L649g4{Y+(fa$=D~Ayo9T}ds~Ky zthsKG$hBr4p>SR{x3_p+HxpJ9j2Zlw+H#Y1LZik-Rl4}p7j+Kr*c8S}+h|eQoe}oq zw~F=Hz9ilRNSB!SZ6mEGKj%)hWs)xMN7s~%o*eXV*Ly8c7HOey9C#4SAj*7rObd$N zwLA6p5)yqzmQAF;H;?D4ZfC;7(xmfdYZ_R6(n@XN&8o}u>iaM4-#^tDm4;r{9>{^a zPc5(GTGSVYo_E{i(HnY$0{i@VomkeJ$X7S z(N9gJj6Q2zlg(j%{E#K|r#eJm+FG;H`4DQPfL1f&F<*6SBcGj&*hM~gz;1qPdH7r_ zo^*&;u|0fUz-rUj_Fc#ccY~L9rhsneUO6u+Xe5k5(r(s}k?NtWnC=gAP%HGcfx2iN zU^u%jzS^QK6vxFPy;8;zG$S3euKz#@ea@hVLq4QBuCy_C(b@6Gk>Pjo0Xg4cuW3gP ziLlwO-T8i)NC2VsuGkZ?HnyCM^RBJZq~o!vlvbbf>FtbX%b zO|>JUpKakwhtV4mc}1f?;!cDOc8F?G?s_;DDx{vmOYpN#l?6}XPh!Le@(WkKlekJ= z(3IX^?XS8W;qXMHzbtxhg6Kk6$Ta}4h!)K`|&cFpmOZ{)*w`NC>FzLdtEw2M# z#-|cX-f9vsN@s7R9Lv6nB(^u(DP$$;oX$@8Jwx)Y3&G|xkL5tu!kW?QyWK^(cP`Dp z1=<Y}V=u4QhhW3g<7zB2R+)4Jv%%ZHAIq^~bi1j7KGXr!e$ z)x&7dJ_2<;cRo-ND z?$q26tL)sp&%KyB)C{C=Ls+|2V+lNJZCO1<;V6y>F?kr1!S zHIsnE^^e6@&PBjY9i9!;wMR=_2h_L*@2 zY$lw6p6<{=c5;kvhWwBp|H{o7XO{3zD>n7lMPu`*hJvoWUrM8GOcHS4(p1?yUQ~Mz zaasA^9hKsYd^tmfP(HqDA)T++Sl@z6RoqJ}VEL-A)@a@z$x|_P zmZib{mtW~3+i+Z;0xrIe?r2vS(0Ot3*?{{5Zdk;o7$Q+F8r@Uu=IfM^L048y7kliq zK4E(tjfa!gQ6-aCv+{dQi};xb>o@MAClD`9?D*x7dQOgkg$9+$1=^;pv~(Vy76*RL zz2EPXN#eTSr3kKWA4mdIcw4=NH$CaOJ#}*Yqr$3uCCY{1J3DUKBmc8h^|3?f@=ey$srMbq`sW)+S>?>(Z?2x@74FyxtHlerbLB4P z7?8Txv8j_&&88^v*U6!=pR&r+X({lm3J@}E=F^|;w$#GPxTXy%W7@< zYrIIICIO3KGt=>wk$Tj1g6h*bKr{1N!*>XFH)+sCU;J!TtjG{D=+oEs9?vB7=gV*Q z_Fq{4lExtxE*h3kttu*gC;2(RN!T-bsaE{SAxSgO46*y}cIHs05Kxe3x8LE`=`VEHxv=Og=*7Iab}QynaN@UO%`DStSC*xt;)bU| zLZrf-tje-td|3fhR!@31_=5VR*>VNZ`ykllQT%z=zlu|5tipyX5f9+Qhr6#P*!W&H z@1&1sHSu*Hv}yc25?$Gq6i)zr^mUB2Yp*)RcoN)w0RVz>_Ve)!2*lw=Ay6h5GaSonEf(bF zAK>Xruom-m_HuIx46qTy`@48}f^7@2Kwm#j0FqEzfk2+*Jat2iK=kn;jj*ty*f`Ku z#^$o#ZlM9TVEyk?OEDK8d_X`Mdla<{9 delta 4156 zcmaJ@2{@GN`~EBqLnOl3$ylOe3}O%=+aM$kDP&7iNR|-ICvhlCF&N6els06ojxE$= zWDpWXF@#8sZK&qVZ?u`p|NmasJ>T=(_x-%fH}A~#R3@OyQqX)DPyZl*-@FXtsj3A4 zwiCD&F;xKo8eo1f2mrVR*1!T>$%a+Qi)Y(HR8au1&=hCsvWWo*0~~dhE}Sf^5Tq&q?M}qI9p_xH5RwA`AvqSb9sFZl(s51(pbT&^ zz|FvB26!0YWnc>fe0533x0-je^x~iE@T-D94!|@Cz~IovhW1X-B**Z8bVot@@Q3#Z z8MurXI*MI@)%bKpY=&L_5ssJ#-6+93C1X+&>B;v_5ZVoB=X(l;a&D0Aj|P|SjnOID zWR`8Bmd9wm%ZBv%b>D&Ys@y$A14H=$V5$U0Bq;0SNn>c=8the;Z`BXNeT5)6|uZD1@k`b*2hDH6M6of@jjm} z0V@Z3Kg=P0CxKDn`>p~&$)kuKMIhL7vaCBwV?a_b6wE96H=Rd+QL*)4-Tl)ux^oaJ zRTBXHv5l{+oImK)y38tF-+D&L2*M*AWASETrTc6VPQ6w$-`y(_gT`Z7#;{M~|Dwo9 zdk23$pBwwSY~)#)!iqX1zp+F=Q>;__@pzVUf=Yj?$B@Hr`~SkNI<6Mz?Q@2Tf6$Lk z*t@Tczz#Z!?|hgYy-ATvNY?)mr777o__6NNgVvBZnXR@&Rnq2hW8neE=QB!MT|+t~ ziC9hVte{@wt4B?zbFX@3Pv%A4r1bW{!X+XQ^)V-82P%TcxVHU6%v|N73L*%fh-OF` zrx;6G=c2fi%Uxc137Z7ocV4USPCl!Ad-2ZxDit6IqKXkhYCSd4zer!|&gw@;GI=E}(EG=54j2ePWWjX5lCi8=P2u zsj@8}t(a-q7R)v>UhVT%zjss+-eZ48mnZH~ahvfDGll!uR3{xt_?>OHt+t=ho2r8B zcU)4O2U%BNL^QIzr)a`l`7N4j<4ie6ZtY$i8N-@dPhK##owRl;iF}E9hc0To@h~rX z=t!ysu3N)l_P`aLf-b|IB5_?GgGffi%B3M4yKE`N<`K)!-9}^~8>K@(&gs+v2T;LEu`OqtgQ(NNMpo?KhWZUEP(gQT z?f6xbh5b2YZ&5e%$_bwXb`71Q;ayE$CaDM~-s_SdE%EEDEV_N_LP5u^2F!swL9aZm z#BhAJyoM8m^WIdCqyD=BY1(hJyH4@!w1Y{Dr5w}Ush0Y_!!-P~NXrPH;LI@0nh+o- zIvNgUKQ5Va$YB#@1&c#lWqdB)C0$kR6!{cC5e?621CNv7yIq$!cS|CyL-Ty@pN)6G z&Fuv}IK2lS9ZLuqLkKUU@71WSQqR%%?-rd(X6F5VqQuHR0!{TLF)V&Vmv zm0vpNJ#%ha{p0K8&>CNnE^b_I0;qSRthh+rW@!MgXGjx>!M1m&`E6J0dz6W2OC1|T zy#!;C>b8#eS%oGNKtKmk4(AYG1Opd5mpPxn@50uBH7}XC3S>q39 zAh=&=*9wvA4K=FkFUSx%In``zt=puZSyRl9qi*bH@i{i7VCL2~oqM#f2Ba8J4nO7% zE12G;u_a_IVZt)xtebjK@qKQLD88YPoJ6WBeX_&k2`=`aRhy4+WI`0z$2h9n=&-7J zYuPClnOZ$TG5lFlV75tWjJ!a+PW9cbx)%i4`F07E-U^EvsUyE=maA4X-eTiwj$xb3 z^g1I&wT)ZenPKhWJ-z#|$Q)?oRlH?bmMv_w#n5<+EzO%XP~q$|^pm;E4{z(`+h~!l zY0I{!6<5O3uk5y?HS$?h%~M8znJZAZq*iTHoI}Y1QvY(%f_!W4_yMtoS#p7f+`sIO z9t~k7GUh>Im2oWx=N#+yDL`i_yX}`&h(a1^@Ap1*XuTwBZUjwx9-*qK^~m|_qBv)l$*$1y_r!N z`7)rsd_vPDwX_i5evdr+>3#V{KcUMnZ}RF%8PtUnc9YH`6_JK1*dJbu6z zO?(9d!b^{pBJMhT0oiTh1H(xg;dyKVXtvaUulz$8N!|gU(SJ}|kspmvbnmn-7irpO zI%9WpIKU{O?0J8vckpY7fqVy*7Fp|el_T6D9mk_2(m=T<-;yo5H%G)0@z=t#@>Ty$ ziYFrdsfsn%8vF^L=d&?VEYjSuJbRR6aCEqUR;vtm=-zRx_JoUy? zv^5gda`o(sxwxG zuGIM);iN!0g4QzD4|S`Ktdp#uFUs!(0I+keZS~vWJXGE{wX!nRSAK|z{h>GUb_uLjPp zjb%E$>+3-hH;}>aTq0uzv84?R->D@r0LcBnWjZ|mPWfCZVjg^dhM`ureMvOzf5v;oOPHKj8p-$Rg@N;hN9{*28alt0OJ zh5`XH*0VpO3;=*F{zijL`67&Q38I;Z-@@p%TgE~7js2MtIM#FkM*hkC5|GzsUmGgS z2Jokc{MR>`@d?{t4Z@G|H5W(pZ|7Ky{wuuI{|78ze=w-h@s*YFQq(t<;~MSTW@*>f7UOvGbL;dc3?gqmrPK2O#CP-e62x!c zefq#R0sK)l@Z074{W31}|8RbL_Fwunzoh)@g33S|vmp%tAPwt8!`g`R(vszofi&;{O3Rb`}T# From c82b6dae0b8e05744f9155b3a3ca7b0a78ee0e32 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 14:53:11 -0700 Subject: [PATCH 49/81] :arrow_up: autosave, text-buffer --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b0c5d1577..e3bcd004c 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.3.4", + "text-buffer": "13.3.5-0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", @@ -97,7 +97,7 @@ "autocomplete-plus": "2.35.11", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", - "autosave": "0.24.5", + "autosave": "0.24.6", "background-tips": "0.27.1", "bookmarks": "0.44.4", "bracket-matcher": "0.88.0", From 91559e885778516815289cac2225489a36ce5ccb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 15:05:12 -0700 Subject: [PATCH 50/81] :arrow_up: git-utils --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0fb37392c..e294f081c 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "fs-plus": "^3.0.1", "fstream": "0.1.24", "fuzzaldrin": "^2.1", - "git-utils": "5.0.1-0", + "git-utils": "5.1.0", "glob": "^7.1.1", "grim": "1.5.0", "jasmine-json": "~0.0", From c7258c7c616faa2d97fdc180bd69a1b5c860e56b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 27 Sep 2017 15:37:12 -0700 Subject: [PATCH 51/81] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e3bcd004c..d5afd5ac1 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.3.5-0", + "text-buffer": "13.3.5", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 5ae9f094011c02281c36218aaa246a3dfdce8c8f Mon Sep 17 00:00:00 2001 From: Thomas Johansen Date: Thu, 28 Sep 2017 13:39:22 +0200 Subject: [PATCH 52/81] :arrow_up: python@2.7.14 --- .python-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.python-version b/.python-version index ecc17b8e9..9bbf49249 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -2.7.13 +2.7.14 From 596fe5fae3adc369eaad145ad274371a3d04e087 Mon Sep 17 00:00:00 2001 From: Thomas Johansen Date: Thu, 28 Sep 2017 13:53:56 +0200 Subject: [PATCH 53/81] Revert ":arrow_up: python@2.7.14" This reverts commit 5ae9f094011c02281c36218aaa246a3dfdce8c8f. --- .python-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.python-version b/.python-version index 9bbf49249..ecc17b8e9 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -2.7.14 +2.7.13 From 84a10bb014bd6ee8a5d5904895dea22f55219682 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2017 11:56:00 -0700 Subject: [PATCH 54/81] :arrow_up: symbols-view --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc8791493..2df7ee676 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "spell-check": "0.72.2", "status-bar": "1.8.13", "styleguide": "0.49.7", - "symbols-view": "0.118.0", + "symbols-view": "0.118.1", "tabs": "0.107.4", "timecop": "0.36.0", "tree-view": "0.218.0", From 596b6ed9ff0dea43441609fbf5c1999c9c732100 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2017 12:01:30 -0700 Subject: [PATCH 55/81] :arrow_up: markdown-preview --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2df7ee676..0a115377a 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,7 @@ "keybinding-resolver": "0.38.0", "line-ending-selector": "0.7.4", "link": "0.31.3", - "markdown-preview": "0.159.13", + "markdown-preview": "0.159.14", "metrics": "1.2.6", "notifications": "0.69.2", "open-on-github": "1.2.1", From 7124b4d949d86f82f83c1a752b00e4ef755d2606 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2017 12:28:42 -0700 Subject: [PATCH 56/81] Don't pass addCursor options through to markBufferPosition Fixes #15646 --- src/text-editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index d85e36535..c9813e445 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -2208,7 +2208,7 @@ class TextEditor extends Model # # Returns a {Cursor}. addCursorAtBufferPosition: (bufferPosition, options) -> - @selectionsMarkerLayer.markBufferPosition(bufferPosition, Object.assign({invalidate: 'never'}, options)) + @selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'}) @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false @getLastSelection().cursor From 1798abf4c61089dad75648f68ca33e3c35f2d7c4 Mon Sep 17 00:00:00 2001 From: Linus Eriksson Date: Thu, 28 Sep 2017 22:01:25 +0200 Subject: [PATCH 57/81] :arrow_up: atom-keymap@8.2.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a115377a..2324f476f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "@atom/source-map-support": "^0.3.4", "async": "0.2.6", - "atom-keymap": "8.2.5", + "atom-keymap": "8.2.6", "atom-select-list": "^0.1.0", "atom-ui": "0.4.1", "babel-core": "5.8.38", From f9110f7708d019c666a327e6d5885d09ff159385 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2017 13:55:57 -0700 Subject: [PATCH 58/81] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0a115377a..303a134b5 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.3.5", + "text-buffer": "13.4.0", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 5eecd8d7483673da0d0464a2a869c0b13888983f Mon Sep 17 00:00:00 2001 From: Hubot Date: Thu, 28 Sep 2017 16:32:44 -0500 Subject: [PATCH 59/81] 1.23.0-dev --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 303a134b5..3da649c93 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "1.22.0-dev", + "version": "1.23.0-dev", "description": "A hackable text editor for the 21st Century.", "main": "./src/main-process/main.js", "repository": { From a7db6ce7b1cfcfdaff5418f6b15458f842d16376 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2017 16:18:56 -0700 Subject: [PATCH 60/81] Convert package-manager-spec to JS --- spec/package-manager-spec.coffee | 1410 --------------------------- spec/package-manager-spec.js | 1547 ++++++++++++++++++++++++++++++ 2 files changed, 1547 insertions(+), 1410 deletions(-) delete mode 100644 spec/package-manager-spec.coffee create mode 100644 spec/package-manager-spec.js diff --git a/spec/package-manager-spec.coffee b/spec/package-manager-spec.coffee deleted file mode 100644 index 9b7d46340..000000000 --- a/spec/package-manager-spec.coffee +++ /dev/null @@ -1,1410 +0,0 @@ -path = require 'path' -Package = require '../src/package' -PackageManager = require '../src/package-manager' -temp = require('temp').track() -fs = require 'fs-plus' -{Disposable} = require 'atom' -{buildKeydownEvent} = require '../src/keymap-extensions' -{mockLocalStorage} = require './spec-helper' -ModuleCache = require '../src/module-cache' - -describe "PackageManager", -> - createTestElement = (className) -> - element = document.createElement('div') - element.className = className - element - - beforeEach -> - spyOn(ModuleCache, 'add') - - afterEach -> - try - temp.cleanupSync() - - describe "initialize", -> - it "adds regular package path", -> - packageManger = new PackageManager({}) - configDirPath = path.join('~', 'someConfig') - packageManger.initialize({configDirPath}) - expect(packageManger.packageDirPaths.length).toBe 1 - expect(packageManger.packageDirPaths[0]).toBe path.join(configDirPath, 'packages') - - it "adds regular package path and dev package path in dev mode", -> - packageManger = new PackageManager({}) - configDirPath = path.join('~', 'someConfig') - packageManger.initialize({configDirPath, devMode: true}) - expect(packageManger.packageDirPaths.length).toBe 2 - expect(packageManger.packageDirPaths).toContain path.join(configDirPath, 'packages') - expect(packageManger.packageDirPaths).toContain path.join(configDirPath, 'dev', 'packages') - - describe "::getApmPath()", -> - it "returns the path to the apm command", -> - apmPath = path.join(process.resourcesPath, "app", "apm", "bin", "apm") - if process.platform is 'win32' - apmPath += ".cmd" - expect(atom.packages.getApmPath()).toBe apmPath - - describe "when the core.apmPath setting is set", -> - beforeEach -> - atom.config.set("core.apmPath", "/path/to/apm") - - it "returns the value of the core.apmPath config setting", -> - expect(atom.packages.getApmPath()).toBe "/path/to/apm" - - describe "::loadPackages()", -> - beforeEach -> - spyOn(atom.packages, 'loadAvailablePackage') - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - - it "sets hasLoadedInitialPackages", -> - expect(atom.packages.hasLoadedInitialPackages()).toBe false - atom.packages.loadPackages() - expect(atom.packages.hasLoadedInitialPackages()).toBe true - - describe "::loadPackage(name)", -> - beforeEach -> - atom.config.set("core.disabledPackages", []) - - it "returns the package", -> - pack = atom.packages.loadPackage("package-with-index") - expect(pack instanceof Package).toBe true - expect(pack.metadata.name).toBe "package-with-index" - - it "returns the package if it has an invalid keymap", -> - spyOn(atom, 'inSpecMode').andReturn(false) - pack = atom.packages.loadPackage("package-with-broken-keymap") - expect(pack instanceof Package).toBe true - expect(pack.metadata.name).toBe "package-with-broken-keymap" - - it "returns the package if it has an invalid stylesheet", -> - spyOn(atom, 'inSpecMode').andReturn(false) - pack = atom.packages.loadPackage("package-with-invalid-styles") - expect(pack instanceof Package).toBe true - expect(pack.metadata.name).toBe "package-with-invalid-styles" - expect(pack.stylesheets.length).toBe 0 - - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - expect(-> pack.reloadStylesheets()).not.toThrow() - expect(addErrorHandler.callCount).toBe 2 - expect(addErrorHandler.argsForCall[1][0].message).toContain("Failed to reload the package-with-invalid-styles package stylesheets") - expect(addErrorHandler.argsForCall[1][0].options.packageName).toEqual "package-with-invalid-styles" - - it "returns null if the package has an invalid package.json", -> - spyOn(atom, 'inSpecMode').andReturn(false) - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - expect(atom.packages.loadPackage("package-with-broken-package-json")).toBeNull() - expect(addErrorHandler.callCount).toBe 1 - expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-with-broken-package-json package") - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-broken-package-json" - - it "returns null if the package name or path starts with a dot", -> - expect(atom.packages.loadPackage("/Users/user/.atom/packages/.git")).toBeNull() - - it "normalizes short repository urls in package.json", -> - {metadata} = atom.packages.loadPackage("package-with-short-url-package-json") - expect(metadata.repository.type).toBe "git" - expect(metadata.repository.url).toBe "https://github.com/example/repo" - - {metadata} = atom.packages.loadPackage("package-with-invalid-url-package-json") - expect(metadata.repository.type).toBe "git" - expect(metadata.repository.url).toBe "foo" - - it "trims git+ from the beginning and .git from the end of repository URLs, even if npm already normalized them ", -> - {metadata} = atom.packages.loadPackage("package-with-prefixed-and-suffixed-repo-url") - expect(metadata.repository.type).toBe "git" - expect(metadata.repository.url).toBe "https://github.com/example/repo" - - it "returns null if the package is not found in any package directory", -> - spyOn(console, 'warn') - expect(atom.packages.loadPackage("this-package-cannot-be-found")).toBeNull() - expect(console.warn.callCount).toBe(1) - expect(console.warn.argsForCall[0][0]).toContain("Could not resolve") - - describe "when the package is deprecated", -> - it "returns null", -> - spyOn(console, 'warn') - expect(atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'packages', 'wordcount'))).toBeNull() - expect(atom.packages.isDeprecatedPackage('wordcount', '2.1.9')).toBe true - expect(atom.packages.isDeprecatedPackage('wordcount', '2.2.0')).toBe true - expect(atom.packages.isDeprecatedPackage('wordcount', '2.2.1')).toBe false - expect(atom.packages.getDeprecatedPackageMetadata('wordcount').version).toBe '<=2.2.0' - - it "invokes ::onDidLoadPackage listeners with the loaded package", -> - loadedPackage = null - atom.packages.onDidLoadPackage (pack) -> loadedPackage = pack - - atom.packages.loadPackage("package-with-main") - - expect(loadedPackage.name).toBe "package-with-main" - - it "registers any deserializers specified in the package's package.json", -> - pack = atom.packages.loadPackage("package-with-deserializers") - - state1 = {deserializer: 'Deserializer1', a: 'b'} - expect(atom.deserializers.deserialize(state1)).toEqual { - wasDeserializedBy: 'deserializeMethod1' - state: state1 - } - - state2 = {deserializer: 'Deserializer2', c: 'd'} - expect(atom.deserializers.deserialize(state2)).toEqual { - wasDeserializedBy: 'deserializeMethod2' - state: state2 - } - - it "early-activates any atom.directory-provider or atom.repository-provider services that the package provide", -> - jasmine.useRealClock() - - providers = [] - atom.packages.serviceHub.consume 'atom.directory-provider', '^0.1.0', (provider) -> - providers.push(provider) - - atom.packages.loadPackage('package-with-directory-provider') - expect(providers.map((p) -> p.name)).toEqual(['directory provider from package-with-directory-provider']) - - describe "when there are view providers specified in the package's package.json", -> - model1 = {worksWithViewProvider1: true} - model2 = {worksWithViewProvider2: true} - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackage('package-with-view-providers') - runs -> - atom.packages.unloadPackage('package-with-view-providers') - - it "does not load the view providers immediately", -> - pack = atom.packages.loadPackage("package-with-view-providers") - expect(pack.mainModule).toBeNull() - - expect(-> atom.views.getView(model1)).toThrow() - expect(-> atom.views.getView(model2)).toThrow() - - it "registers the view providers when the package is activated", -> - pack = atom.packages.loadPackage("package-with-view-providers") - - waitsForPromise -> - atom.packages.activatePackage("package-with-view-providers").then -> - element1 = atom.views.getView(model1) - expect(element1 instanceof HTMLDivElement).toBe true - expect(element1.dataset.createdBy).toBe 'view-provider-1' - - element2 = atom.views.getView(model2) - expect(element2 instanceof HTMLDivElement).toBe true - expect(element2.dataset.createdBy).toBe 'view-provider-2' - - it "registers the view providers when any of the package's deserializers are used", -> - pack = atom.packages.loadPackage("package-with-view-providers") - - spyOn(atom.views, 'addViewProvider').andCallThrough() - atom.deserializers.deserialize({ - deserializer: 'DeserializerFromPackageWithViewProviders', - a: 'b' - }) - expect(atom.views.addViewProvider.callCount).toBe 2 - - atom.deserializers.deserialize({ - deserializer: 'DeserializerFromPackageWithViewProviders', - a: 'b' - }) - expect(atom.views.addViewProvider.callCount).toBe 2 - - element1 = atom.views.getView(model1) - expect(element1 instanceof HTMLDivElement).toBe true - expect(element1.dataset.createdBy).toBe 'view-provider-1' - - element2 = atom.views.getView(model2) - expect(element2 instanceof HTMLDivElement).toBe true - expect(element2.dataset.createdBy).toBe 'view-provider-2' - - it "registers the config schema in the package's metadata, if present", -> - pack = atom.packages.loadPackage("package-with-json-config-schema") - expect(atom.config.getSchema('package-with-json-config-schema')).toEqual { - type: 'object' - properties: { - a: {type: 'number', default: 5} - b: {type: 'string', default: 'five'} - } - } - - expect(pack.mainModule).toBeNull() - - atom.packages.unloadPackage('package-with-json-config-schema') - atom.config.clear() - - pack = atom.packages.loadPackage("package-with-json-config-schema") - expect(atom.config.getSchema('package-with-json-config-schema')).toEqual { - type: 'object' - properties: { - a: {type: 'number', default: 5} - b: {type: 'string', default: 'five'} - } - } - - describe "when a package does not have deserializers, view providers or a config schema in its package.json", -> - beforeEach -> - mockLocalStorage() - - it "defers loading the package's main module if the package previously used no Atom APIs when its main module was required", -> - pack1 = atom.packages.loadPackage('package-with-main') - expect(pack1.mainModule).toBeDefined() - - atom.packages.unloadPackage('package-with-main') - - pack2 = atom.packages.loadPackage('package-with-main') - expect(pack2.mainModule).toBeNull() - - it "does not defer loading the package's main module if the package previously used Atom APIs when its main module was required", -> - pack1 = atom.packages.loadPackage('package-with-eval-time-api-calls') - expect(pack1.mainModule).toBeDefined() - - atom.packages.unloadPackage('package-with-eval-time-api-calls') - - pack2 = atom.packages.loadPackage('package-with-eval-time-api-calls') - expect(pack2.mainModule).not.toBeNull() - - describe "::loadAvailablePackage(availablePackage)", -> - describe "if the package was preloaded", -> - it "adds the package path to the module cache", -> - availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check' - availablePackage.isBundled = true - expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) - - metadata = atom.packages.loadPackageMetadata(availablePackage) - atom.packages.preloadPackage( - availablePackage.name, - { - rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), - metadata - } - ) - atom.packages.loadAvailablePackage(availablePackage) - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(true) - expect(ModuleCache.add).toHaveBeenCalledWith(availablePackage.path, metadata) - - it "deactivates it if it had been disabled", -> - availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check' - availablePackage.isBundled = true - expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) - - metadata = atom.packages.loadPackageMetadata(availablePackage) - preloadedPackage = atom.packages.preloadPackage( - availablePackage.name, - { - rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), - metadata - } - ) - expect(preloadedPackage.keymapActivated).toBe(true) - expect(preloadedPackage.settingsActivated).toBe(true) - expect(preloadedPackage.menusActivated).toBe(true) - - atom.packages.loadAvailablePackage(availablePackage, new Set([availablePackage.name])) - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) - expect(preloadedPackage.keymapActivated).toBe(false) - expect(preloadedPackage.settingsActivated).toBe(false) - expect(preloadedPackage.menusActivated).toBe(false) - - it "deactivates it and reloads the new one if trying to load the same package outside of the bundle", -> - availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check' - availablePackage.isBundled = true - expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) - - metadata = atom.packages.loadPackageMetadata(availablePackage) - preloadedPackage = atom.packages.preloadPackage( - availablePackage.name, - { - rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), - metadata - } - ) - expect(preloadedPackage.keymapActivated).toBe(true) - expect(preloadedPackage.settingsActivated).toBe(true) - expect(preloadedPackage.menusActivated).toBe(true) - - availablePackage.isBundled = false - atom.packages.loadAvailablePackage(availablePackage) - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(true) - expect(preloadedPackage.keymapActivated).toBe(false) - expect(preloadedPackage.settingsActivated).toBe(false) - expect(preloadedPackage.menusActivated).toBe(false) - - describe "if the package was not preloaded", -> - it "adds the package path to the module cache", -> - availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check' - availablePackage.isBundled = true - metadata = atom.packages.loadPackageMetadata(availablePackage) - atom.packages.loadAvailablePackage(availablePackage) - expect(ModuleCache.add).toHaveBeenCalledWith(availablePackage.path, metadata) - - describe "preloading", -> - it "requires the main module, loads the config schema and activates keymaps, menus and settings without reactivating them during package activation", -> - availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check' - availablePackage.isBundled = true - metadata = atom.packages.loadPackageMetadata(availablePackage) - expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) - - atom.packages.packagesCache = {} - atom.packages.packagesCache[availablePackage.name] = { - main: path.join(availablePackage.path, metadata.main), - grammarPaths: [] - } - preloadedPackage = atom.packages.preloadPackage( - availablePackage.name, - { - rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), - metadata - } - ) - expect(preloadedPackage.keymapActivated).toBe(true) - expect(preloadedPackage.settingsActivated).toBe(true) - expect(preloadedPackage.menusActivated).toBe(true) - expect(preloadedPackage.mainModule).toBeTruthy() - expect(preloadedPackage.configSchemaRegisteredOnLoad).toBeTruthy() - - spyOn(atom.keymaps, 'add') - spyOn(atom.menu, 'add') - spyOn(atom.contextMenu, 'add') - spyOn(atom.config, 'setSchema') - - atom.packages.loadAvailablePackage(availablePackage) - expect(preloadedPackage.getMainModulePath()).toBe(path.join(availablePackage.path, metadata.main)) - - atom.packages.activatePackage(availablePackage.name) - expect(atom.keymaps.add).not.toHaveBeenCalled() - expect(atom.menu.add).not.toHaveBeenCalled() - expect(atom.contextMenu.add).not.toHaveBeenCalled() - expect(atom.config.setSchema).not.toHaveBeenCalled() - expect(preloadedPackage.keymapActivated).toBe(true) - expect(preloadedPackage.settingsActivated).toBe(true) - expect(preloadedPackage.menusActivated).toBe(true) - expect(preloadedPackage.mainModule).toBeTruthy() - expect(preloadedPackage.configSchemaRegisteredOnLoad).toBeTruthy() - - it "deactivates disabled keymaps during package activation", -> - availablePackage = atom.packages.getAvailablePackages().find (p) -> p.name is 'spell-check' - availablePackage.isBundled = true - metadata = atom.packages.loadPackageMetadata(availablePackage) - expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() - expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) - - atom.packages.packagesCache = {} - atom.packages.packagesCache[availablePackage.name] = { - main: path.join(availablePackage.path, metadata.main), - grammarPaths: [] - } - preloadedPackage = atom.packages.preloadPackage( - availablePackage.name, - { - rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), - metadata - } - ) - expect(preloadedPackage.keymapActivated).toBe(true) - expect(preloadedPackage.settingsActivated).toBe(true) - expect(preloadedPackage.menusActivated).toBe(true) - - atom.packages.loadAvailablePackage(availablePackage) - atom.config.set("core.packagesWithKeymapsDisabled", [availablePackage.name]) - atom.packages.activatePackage(availablePackage.name) - - expect(preloadedPackage.keymapActivated).toBe(false) - expect(preloadedPackage.settingsActivated).toBe(true) - expect(preloadedPackage.menusActivated).toBe(true) - - describe "::unloadPackage(name)", -> - describe "when the package is active", -> - it "throws an error", -> - pack = null - waitsForPromise -> - atom.packages.activatePackage('package-with-main').then (p) -> pack = p - - runs -> - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() - expect( -> atom.packages.unloadPackage(pack.name)).toThrow() - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() - - describe "when the package is not loaded", -> - it "throws an error", -> - expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy() - expect( -> atom.packages.unloadPackage('unloaded')).toThrow() - expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy() - - describe "when the package is loaded", -> - it "no longers reports it as being loaded", -> - pack = atom.packages.loadPackage('package-with-main') - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - atom.packages.unloadPackage(pack.name) - expect(atom.packages.isPackageLoaded(pack.name)).toBeFalsy() - - it "invokes ::onDidUnloadPackage listeners with the unloaded package", -> - atom.packages.loadPackage('package-with-main') - unloadedPackage = null - atom.packages.onDidUnloadPackage (pack) -> unloadedPackage = pack - atom.packages.unloadPackage('package-with-main') - expect(unloadedPackage.name).toBe 'package-with-main' - - describe "::activatePackage(id)", -> - describe "when called multiple times", -> - it "it only calls activate on the package once", -> - spyOn(Package.prototype, 'activateNow').andCallThrough() - waitsForPromise -> - atom.packages.activatePackage('package-with-index') - waitsForPromise -> - atom.packages.activatePackage('package-with-index') - waitsForPromise -> - atom.packages.activatePackage('package-with-index') - - runs -> - expect(Package.prototype.activateNow.callCount).toBe 1 - - describe "when the package has a main module", -> - describe "when the metadata specifies a main module path˜", -> - it "requires the module at the specified path", -> - mainModule = require('./fixtures/packages/package-with-main/main-module') - spyOn(mainModule, 'activate') - pack = null - waitsForPromise -> - atom.packages.activatePackage('package-with-main').then (p) -> pack = p - - runs -> - expect(mainModule.activate).toHaveBeenCalled() - expect(pack.mainModule).toBe mainModule - - describe "when the metadata does not specify a main module", -> - it "requires index.coffee", -> - indexModule = require('./fixtures/packages/package-with-index/index') - spyOn(indexModule, 'activate') - pack = null - waitsForPromise -> - atom.packages.activatePackage('package-with-index').then (p) -> pack = p - - runs -> - expect(indexModule.activate).toHaveBeenCalled() - expect(pack.mainModule).toBe indexModule - - it "assigns config schema, including defaults when package contains a schema", -> - expect(atom.config.get('package-with-config-schema.numbers.one')).toBeUndefined() - - waitsForPromise -> - atom.packages.activatePackage('package-with-config-schema') - - runs -> - expect(atom.config.get('package-with-config-schema.numbers.one')).toBe 1 - expect(atom.config.get('package-with-config-schema.numbers.two')).toBe 2 - - expect(atom.config.set('package-with-config-schema.numbers.one', 'nope')).toBe false - expect(atom.config.set('package-with-config-schema.numbers.one', '10')).toBe true - expect(atom.config.get('package-with-config-schema.numbers.one')).toBe 10 - - describe "when the package metadata includes `activationCommands`", -> - [mainModule, promise, workspaceCommandListener, registration] = [] - - beforeEach -> - jasmine.attachToDOM(atom.workspace.getElement()) - mainModule = require './fixtures/packages/package-with-activation-commands/index' - mainModule.activationCommandCallCount = 0 - spyOn(mainModule, 'activate').andCallThrough() - spyOn(Package.prototype, 'requireMainModule').andCallThrough() - - workspaceCommandListener = jasmine.createSpy('workspaceCommandListener') - registration = atom.commands.add '.workspace', 'activation-command', workspaceCommandListener - - promise = atom.packages.activatePackage('package-with-activation-commands') - - afterEach -> - registration?.dispose() - mainModule = null - - it "defers requiring/activating the main module until an activation event bubbles to the root view", -> - expect(Package.prototype.requireMainModule.callCount).toBe 0 - - atom.workspace.getElement().dispatchEvent(new CustomEvent('activation-command', bubbles: true)) - - waitsForPromise -> - promise - - runs -> - expect(Package.prototype.requireMainModule.callCount).toBe 1 - - it "triggers the activation event on all handlers registered during activation", -> - waitsForPromise -> - atom.workspace.open() - - runs -> - editorElement = atom.workspace.getActiveTextEditor().getElement() - editorCommandListener = jasmine.createSpy("editorCommandListener") - atom.commands.add 'atom-text-editor', 'activation-command', editorCommandListener - atom.commands.dispatch(editorElement, 'activation-command') - expect(mainModule.activate.callCount).toBe 1 - expect(mainModule.activationCommandCallCount).toBe 1 - expect(editorCommandListener.callCount).toBe 1 - expect(workspaceCommandListener.callCount).toBe 1 - atom.commands.dispatch(editorElement, 'activation-command') - expect(mainModule.activationCommandCallCount).toBe 2 - expect(editorCommandListener.callCount).toBe 2 - expect(workspaceCommandListener.callCount).toBe 2 - expect(mainModule.activate.callCount).toBe 1 - - it "activates the package immediately when the events are empty", -> - mainModule = require './fixtures/packages/package-with-empty-activation-commands/index' - spyOn(mainModule, 'activate').andCallThrough() - - waitsForPromise -> - atom.packages.activatePackage('package-with-empty-activation-commands') - - runs -> - expect(mainModule.activate.callCount).toBe 1 - - it "adds a notification when the activation commands are invalid", -> - spyOn(atom, 'inSpecMode').andReturn(false) - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - expect(-> atom.packages.activatePackage('package-with-invalid-activation-commands')).not.toThrow() - expect(addErrorHandler.callCount).toBe 1 - expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to activate the package-with-invalid-activation-commands package") - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-activation-commands" - - it "adds a notification when the context menu is invalid", -> - spyOn(atom, 'inSpecMode').andReturn(false) - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - expect(-> atom.packages.activatePackage('package-with-invalid-context-menu')).not.toThrow() - expect(addErrorHandler.callCount).toBe 1 - expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to activate the package-with-invalid-context-menu package") - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-context-menu" - - it "adds a notification when the grammar is invalid", -> - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - - expect(-> atom.packages.activatePackage('package-with-invalid-grammar')).not.toThrow() - - waitsFor -> - addErrorHandler.callCount > 0 - - runs -> - expect(addErrorHandler.callCount).toBe 1 - expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load a package-with-invalid-grammar package grammar") - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-grammar" - - it "adds a notification when the settings are invalid", -> - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - - expect(-> atom.packages.activatePackage('package-with-invalid-settings')).not.toThrow() - - waitsFor -> - addErrorHandler.callCount > 0 - - runs -> - expect(addErrorHandler.callCount).toBe 1 - expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-with-invalid-settings package settings") - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-with-invalid-settings" - - describe "when the package metadata includes `activationHooks`", -> - [mainModule, promise] = [] - - beforeEach -> - mainModule = require './fixtures/packages/package-with-activation-hooks/index' - spyOn(mainModule, 'activate').andCallThrough() - spyOn(Package.prototype, 'requireMainModule').andCallThrough() - - it "defers requiring/activating the main module until an triggering of an activation hook occurs", -> - promise = atom.packages.activatePackage('package-with-activation-hooks') - expect(Package.prototype.requireMainModule.callCount).toBe 0 - atom.packages.triggerActivationHook('language-fictitious:grammar-used') - atom.packages.triggerDeferredActivationHooks() - - waitsForPromise -> - promise - - runs -> - expect(Package.prototype.requireMainModule.callCount).toBe 1 - - it "does not double register activation hooks when deactivating and reactivating", -> - promise = atom.packages.activatePackage('package-with-activation-hooks') - expect(mainModule.activate.callCount).toBe 0 - atom.packages.triggerActivationHook('language-fictitious:grammar-used') - atom.packages.triggerDeferredActivationHooks() - - waitsForPromise -> - promise - - runs -> - expect(mainModule.activate.callCount).toBe 1 - - waitsForPromise -> - atom.packages.deactivatePackage('package-with-activation-hooks') - - runs -> - promise = atom.packages.activatePackage('package-with-activation-hooks') - atom.packages.triggerActivationHook('language-fictitious:grammar-used') - atom.packages.triggerDeferredActivationHooks() - - waitsForPromise -> - promise - - runs -> - expect(mainModule.activate.callCount).toBe 2 - - it "activates the package immediately when activationHooks is empty", -> - mainModule = require './fixtures/packages/package-with-empty-activation-hooks/index' - spyOn(mainModule, 'activate').andCallThrough() - - runs -> - expect(Package.prototype.requireMainModule.callCount).toBe 0 - - waitsForPromise -> - atom.packages.activatePackage('package-with-empty-activation-hooks') - - runs -> - expect(mainModule.activate.callCount).toBe 1 - expect(Package.prototype.requireMainModule.callCount).toBe 1 - - it "activates the package immediately if the activation hook had already been triggered", -> - atom.packages.triggerActivationHook('language-fictitious:grammar-used') - atom.packages.triggerDeferredActivationHooks() - expect(Package.prototype.requireMainModule.callCount).toBe 0 - - waitsForPromise -> - atom.packages.activatePackage('package-with-activation-hooks') - - runs -> - expect(Package.prototype.requireMainModule.callCount).toBe 1 - - describe "when the package has no main module", -> - it "does not throw an exception", -> - spyOn(console, "error") - spyOn(console, "warn").andCallThrough() - expect(-> atom.packages.activatePackage('package-without-module')).not.toThrow() - expect(console.error).not.toHaveBeenCalled() - expect(console.warn).not.toHaveBeenCalled() - - describe "when the package does not export an activate function", -> - it "activates the package and does not throw an exception or log a warning", -> - spyOn(console, "warn") - expect(-> atom.packages.activatePackage('package-with-no-activate')).not.toThrow() - - waitsFor -> - atom.packages.isPackageActive('package-with-no-activate') - - runs -> - expect(console.warn).not.toHaveBeenCalled() - - it "passes the activate method the package's previously serialized state if it exists", -> - pack = null - waitsForPromise -> - atom.packages.activatePackage("package-with-serialization").then (p) -> pack = p - runs -> - expect(pack.mainModule.someNumber).not.toBe 77 - pack.mainModule.someNumber = 77 - atom.packages.serializePackage("package-with-serialization") - waitsForPromise -> - atom.packages.deactivatePackage("package-with-serialization") - runs -> - spyOn(pack.mainModule, 'activate').andCallThrough() - waitsForPromise -> - atom.packages.activatePackage("package-with-serialization") - runs -> - expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77}) - - it "invokes ::onDidActivatePackage listeners with the activated package", -> - activatedPackage = null - atom.packages.onDidActivatePackage (pack) -> - activatedPackage = pack - - atom.packages.activatePackage('package-with-main') - - waitsFor -> activatedPackage? - runs -> expect(activatedPackage.name).toBe 'package-with-main' - - describe "when the package's main module throws an error on load", -> - it "adds a notification instead of throwing an exception", -> - spyOn(atom, 'inSpecMode').andReturn(false) - atom.config.set("core.disabledPackages", []) - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - expect(-> atom.packages.activatePackage("package-that-throws-an-exception")).not.toThrow() - expect(addErrorHandler.callCount).toBe 1 - expect(addErrorHandler.argsForCall[0][0].message).toContain("Failed to load the package-that-throws-an-exception package") - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual "package-that-throws-an-exception" - - it "re-throws the exception in test mode", -> - atom.config.set("core.disabledPackages", []) - addErrorHandler = jasmine.createSpy() - expect(-> atom.packages.activatePackage("package-that-throws-an-exception")).toThrow("This package throws an exception") - - describe "when the package is not found", -> - it "rejects the promise", -> - atom.config.set("core.disabledPackages", []) - - onSuccess = jasmine.createSpy('onSuccess') - onFailure = jasmine.createSpy('onFailure') - spyOn(console, 'warn') - - atom.packages.activatePackage("this-doesnt-exist").then(onSuccess, onFailure) - - waitsFor "promise to be rejected", -> - onFailure.callCount > 0 - - runs -> - expect(console.warn.callCount).toBe 1 - expect(onFailure.mostRecentCall.args[0] instanceof Error).toBe true - expect(onFailure.mostRecentCall.args[0].message).toContain "Failed to load package 'this-doesnt-exist'" - - describe "keymap loading", -> - describe "when the metadata does not contain a 'keymaps' manifest", -> - it "loads all the .cson/.json files in the keymaps directory", -> - element1 = createTestElement('test-1') - element2 = createTestElement('test-2') - element3 = createTestElement('test-3') - - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0 - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element2)).toHaveLength 0 - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element3)).toHaveLength 0 - - waitsForPromise -> - atom.packages.activatePackage("package-with-keymaps") - - runs -> - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)[0].command).toBe "test-1" - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element2)[0].command).toBe "test-2" - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element3)).toHaveLength 0 - - describe "when the metadata contains a 'keymaps' manifest", -> - it "loads only the keymaps specified by the manifest, in the specified order", -> - element1 = createTestElement('test-1') - element3 = createTestElement('test-3') - - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0 - - waitsForPromise -> - atom.packages.activatePackage("package-with-keymaps-manifest") - - runs -> - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)[0].command).toBe 'keymap-1' - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-n', target: element1)[0].command).toBe 'keymap-2' - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-y', target: element3)).toHaveLength 0 - - describe "when the keymap file is empty", -> - it "does not throw an error on activation", -> - waitsForPromise -> - atom.packages.activatePackage("package-with-empty-keymap") - - runs -> - expect(atom.packages.isPackageActive("package-with-empty-keymap")).toBe true - - describe "when the package's keymaps have been disabled", -> - it "does not add the keymaps", -> - element1 = createTestElement('test-1') - - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0 - - atom.config.set("core.packagesWithKeymapsDisabled", ["package-with-keymaps-manifest"]) - - waitsForPromise -> - atom.packages.activatePackage("package-with-keymaps-manifest") - - runs -> - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0 - - describe "when setting core.packagesWithKeymapsDisabled", -> - it "ignores package names in the array that aren't loaded", -> - atom.packages.observePackagesWithKeymapsDisabled() - - expect(-> atom.config.set("core.packagesWithKeymapsDisabled", ["package-does-not-exist"])).not.toThrow() - expect(-> atom.config.set("core.packagesWithKeymapsDisabled", [])).not.toThrow() - - describe "when the package's keymaps are disabled and re-enabled after it is activated", -> - it "removes and re-adds the keymaps", -> - element1 = createTestElement('test-1') - atom.packages.observePackagesWithKeymapsDisabled() - - waitsForPromise -> - atom.packages.activatePackage("package-with-keymaps-manifest") - - runs -> - atom.config.set("core.packagesWithKeymapsDisabled", ['package-with-keymaps-manifest']) - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)).toHaveLength 0 - - atom.config.set("core.packagesWithKeymapsDisabled", []) - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: element1)[0].command).toBe 'keymap-1' - - describe "when the package is de-activated and re-activated", -> - [element, events, userKeymapPath] = [] - - beforeEach -> - userKeymapPath = path.join(temp.mkdirSync(), "user-keymaps.cson") - spyOn(atom.keymaps, "getUserKeymapPath").andReturn(userKeymapPath) - - element = createTestElement('test-1') - jasmine.attachToDOM(element) - - events = [] - element.addEventListener 'user-command', (e) -> events.push(e) - element.addEventListener 'test-1', (e) -> events.push(e) - - afterEach -> - element.remove() - - # Avoid leaking user keymap subscription - atom.keymaps.watchSubscriptions[userKeymapPath].dispose() - delete atom.keymaps.watchSubscriptions[userKeymapPath] - - temp.cleanupSync() - - it "doesn't override user-defined keymaps", -> - fs.writeFileSync userKeymapPath, """ - ".test-1": - "ctrl-z": "user-command" - """ - atom.keymaps.loadUserKeymap() - - waitsForPromise -> - atom.packages.activatePackage("package-with-keymaps") - - runs -> - atom.keymaps.handleKeyboardEvent(buildKeydownEvent("z", ctrl: true, target: element)) - - expect(events.length).toBe(1) - expect(events[0].type).toBe("user-command") - - waitsForPromise -> - atom.packages.deactivatePackage("package-with-keymaps") - - waitsForPromise -> - atom.packages.activatePackage("package-with-keymaps") - - runs -> - atom.keymaps.handleKeyboardEvent(buildKeydownEvent("z", ctrl: true, target: element)) - - expect(events.length).toBe(2) - expect(events[1].type).toBe("user-command") - - describe "menu loading", -> - beforeEach -> - atom.contextMenu.definitions = [] - atom.menu.template = [] - - describe "when the metadata does not contain a 'menus' manifest", -> - it "loads all the .cson/.json files in the menus directory", -> - element = createTestElement('test-1') - - expect(atom.contextMenu.templateForElement(element)).toEqual [] - - waitsForPromise -> - atom.packages.activatePackage("package-with-menus") - - runs -> - expect(atom.menu.template.length).toBe 2 - expect(atom.menu.template[0].label).toBe "Second to Last" - expect(atom.menu.template[1].label).toBe "Last" - expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 1" - expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 2" - expect(atom.contextMenu.templateForElement(element)[2].label).toBe "Menu item 3" - - describe "when the metadata contains a 'menus' manifest", -> - it "loads only the menus specified by the manifest, in the specified order", -> - element = createTestElement('test-1') - - expect(atom.contextMenu.templateForElement(element)).toEqual [] - - waitsForPromise -> - atom.packages.activatePackage("package-with-menus-manifest") - - runs -> - expect(atom.menu.template[0].label).toBe "Second to Last" - expect(atom.menu.template[1].label).toBe "Last" - expect(atom.contextMenu.templateForElement(element)[0].label).toBe "Menu item 2" - expect(atom.contextMenu.templateForElement(element)[1].label).toBe "Menu item 1" - expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined() - - describe "when the menu file is empty", -> - it "does not throw an error on activation", -> - waitsForPromise -> - atom.packages.activatePackage("package-with-empty-menu") - - runs -> - expect(atom.packages.isPackageActive("package-with-empty-menu")).toBe true - - describe "stylesheet loading", -> - describe "when the metadata contains a 'styleSheets' manifest", -> - it "loads style sheets from the styles directory as specified by the manifest", -> - one = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/1.css") - two = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/2.less") - three = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/3.css") - - expect(atom.themes.stylesheetElementForId(one)).toBeNull() - expect(atom.themes.stylesheetElementForId(two)).toBeNull() - expect(atom.themes.stylesheetElementForId(three)).toBeNull() - - waitsForPromise -> - atom.packages.activatePackage("package-with-style-sheets-manifest") - - runs -> - expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(three)).toBeNull() - - expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe '1px' - - describe "when the metadata does not contain a 'styleSheets' manifest", -> - it "loads all style sheets from the styles directory", -> - one = require.resolve("./fixtures/packages/package-with-styles/styles/1.css") - two = require.resolve("./fixtures/packages/package-with-styles/styles/2.less") - three = require.resolve("./fixtures/packages/package-with-styles/styles/3.test-context.css") - four = require.resolve("./fixtures/packages/package-with-styles/styles/4.css") - - expect(atom.themes.stylesheetElementForId(one)).toBeNull() - expect(atom.themes.stylesheetElementForId(two)).toBeNull() - expect(atom.themes.stylesheetElementForId(three)).toBeNull() - expect(atom.themes.stylesheetElementForId(four)).toBeNull() - - waitsForPromise -> - atom.packages.activatePackage("package-with-styles") - - runs -> - expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(three)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(four)).not.toBeNull() - expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe '3px' - - it "assigns the stylesheet's context based on the filename", -> - waitsForPromise -> - atom.packages.activatePackage("package-with-styles") - - runs -> - count = 0 - - for styleElement in atom.styles.getStyleElements() - if styleElement.sourcePath.match /1.css/ - expect(styleElement.context).toBe undefined - count++ - - if styleElement.sourcePath.match /2.less/ - expect(styleElement.context).toBe undefined - count++ - - if styleElement.sourcePath.match /3.test-context.css/ - expect(styleElement.context).toBe 'test-context' - count++ - - if styleElement.sourcePath.match /4.css/ - expect(styleElement.context).toBe undefined - count++ - - expect(count).toBe 4 - - describe "grammar loading", -> - it "loads the package's grammars", -> - waitsForPromise -> - atom.packages.activatePackage('package-with-grammars') - - runs -> - expect(atom.grammars.selectGrammar('a.alot').name).toBe 'Alot' - expect(atom.grammars.selectGrammar('a.alittle').name).toBe 'Alittle' - - describe "scoped-property loading", -> - it "loads the scoped properties", -> - waitsForPromise -> - atom.packages.activatePackage("package-with-settings") - - runs -> - expect(atom.config.get 'editor.increaseIndentPattern', scope: ['.source.omg']).toBe '^a' - - describe "service registration", -> - it "registers the package's provided and consumed services", -> - consumerModule = require "./fixtures/packages/package-with-consumed-services" - firstServiceV3Disposed = false - firstServiceV4Disposed = false - secondServiceDisposed = false - spyOn(consumerModule, 'consumeFirstServiceV3').andReturn(new Disposable -> firstServiceV3Disposed = true) - spyOn(consumerModule, 'consumeFirstServiceV4').andReturn(new Disposable -> firstServiceV4Disposed = true) - spyOn(consumerModule, 'consumeSecondService').andReturn(new Disposable -> secondServiceDisposed = true) - - waitsForPromise -> - atom.packages.activatePackage("package-with-consumed-services") - - waitsForPromise -> - atom.packages.activatePackage("package-with-provided-services") - - runs -> - expect(consumerModule.consumeFirstServiceV3.callCount).toBe(1) - expect(consumerModule.consumeFirstServiceV3).toHaveBeenCalledWith('first-service-v3') - expect(consumerModule.consumeFirstServiceV4).toHaveBeenCalledWith('first-service-v4') - expect(consumerModule.consumeSecondService).toHaveBeenCalledWith('second-service') - - consumerModule.consumeFirstServiceV3.reset() - consumerModule.consumeFirstServiceV4.reset() - consumerModule.consumeSecondService.reset() - - waitsForPromise -> - atom.packages.deactivatePackage("package-with-provided-services") - - runs -> - expect(firstServiceV3Disposed).toBe true - expect(firstServiceV4Disposed).toBe true - expect(secondServiceDisposed).toBe true - - waitsForPromise -> - atom.packages.deactivatePackage("package-with-consumed-services") - - waitsForPromise -> - atom.packages.activatePackage("package-with-provided-services") - - runs -> - expect(consumerModule.consumeFirstServiceV3).not.toHaveBeenCalled() - expect(consumerModule.consumeFirstServiceV4).not.toHaveBeenCalled() - expect(consumerModule.consumeSecondService).not.toHaveBeenCalled() - - it "ignores provided and consumed services that do not exist", -> - addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) - - waitsForPromise -> - atom.packages.activatePackage("package-with-missing-consumed-services") - - waitsForPromise -> - atom.packages.activatePackage("package-with-missing-provided-services") - - runs -> - expect(atom.packages.isPackageActive("package-with-missing-consumed-services")).toBe true - expect(atom.packages.isPackageActive("package-with-missing-provided-services")).toBe true - expect(addErrorHandler.callCount).toBe 0 - - describe "::serialize", -> - it "does not serialize packages that threw an error during activation", -> - spyOn(atom, 'inSpecMode').andReturn(false) - spyOn(console, 'warn') - badPack = null - waitsForPromise -> - atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p - - runs -> - spyOn(badPack.mainModule, 'serialize').andCallThrough() - - atom.packages.serialize() - expect(badPack.mainModule.serialize).not.toHaveBeenCalled() - - it "absorbs exceptions that are thrown by the package module's serialize method", -> - spyOn(console, 'error') - - waitsForPromise -> - atom.packages.activatePackage('package-with-serialize-error') - - waitsForPromise -> - atom.packages.activatePackage('package-with-serialization') - - runs -> - atom.packages.serialize() - expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined() - expect(atom.packages.packageStates['package-with-serialization']).toEqual someNumber: 1 - expect(console.error).toHaveBeenCalled() - - describe "::deactivatePackages()", -> - it "deactivates all packages but does not serialize them", -> - [pack1, pack2] = [] - - waitsForPromise -> - atom.packages.activatePackage("package-with-deactivate").then (p) -> pack1 = p - atom.packages.activatePackage("package-with-serialization").then (p) -> pack2 = p - - runs -> - spyOn(pack1.mainModule, 'deactivate') - spyOn(pack2.mainModule, 'serialize') - - waitsForPromise -> - atom.packages.deactivatePackages() - - runs -> - expect(pack1.mainModule.deactivate).toHaveBeenCalled() - expect(pack2.mainModule.serialize).not.toHaveBeenCalled() - - describe "::deactivatePackage(id)", -> - afterEach -> - atom.packages.unloadPackages() - - it "calls `deactivate` on the package's main module if activate was successful", -> - spyOn(atom, 'inSpecMode').andReturn(false) - pack = null - waitsForPromise -> - atom.packages.activatePackage("package-with-deactivate").then (p) -> pack = p - - runs -> - expect(atom.packages.isPackageActive("package-with-deactivate")).toBeTruthy() - spyOn(pack.mainModule, 'deactivate').andCallThrough() - - waitsForPromise -> - atom.packages.deactivatePackage("package-with-deactivate") - - runs -> - expect(pack.mainModule.deactivate).toHaveBeenCalled() - expect(atom.packages.isPackageActive("package-with-module")).toBeFalsy() - - spyOn(console, 'warn') - - badPack = null - waitsForPromise -> - atom.packages.activatePackage("package-that-throws-on-activate").then (p) -> badPack = p - - runs -> - expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeTruthy() - spyOn(badPack.mainModule, 'deactivate').andCallThrough() - - waitsForPromise -> - atom.packages.deactivatePackage("package-that-throws-on-activate") - - runs -> - expect(badPack.mainModule.deactivate).not.toHaveBeenCalled() - expect(atom.packages.isPackageActive("package-that-throws-on-activate")).toBeFalsy() - - it "absorbs exceptions that are thrown by the package module's deactivate method", -> - spyOn(console, 'error') - thrownError = null - - waitsForPromise -> - atom.packages.activatePackage("package-that-throws-on-deactivate") - - waitsForPromise -> - try - atom.packages.deactivatePackage("package-that-throws-on-deactivate") - catch error - thrownError = error - - runs -> - expect(thrownError).toBeNull() - expect(console.error).toHaveBeenCalled() - - it "removes the package's grammars", -> - waitsForPromise -> - atom.packages.activatePackage('package-with-grammars') - - waitsForPromise -> - atom.packages.deactivatePackage('package-with-grammars') - - runs -> - expect(atom.grammars.selectGrammar('a.alot').name).toBe 'Null Grammar' - expect(atom.grammars.selectGrammar('a.alittle').name).toBe 'Null Grammar' - - it "removes the package's keymaps", -> - waitsForPromise -> - atom.packages.activatePackage('package-with-keymaps') - - waitsForPromise -> - atom.packages.deactivatePackage('package-with-keymaps') - - runs -> - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: createTestElement('test-1'))).toHaveLength 0 - expect(atom.keymaps.findKeyBindings(keystrokes: 'ctrl-z', target: createTestElement('test-2'))).toHaveLength 0 - - it "removes the package's stylesheets", -> - waitsForPromise -> - atom.packages.activatePackage('package-with-styles') - - waitsForPromise -> - atom.packages.deactivatePackage('package-with-styles') - - runs -> - one = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/1.css") - two = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/2.less") - three = require.resolve("./fixtures/packages/package-with-style-sheets-manifest/styles/3.css") - expect(atom.themes.stylesheetElementForId(one)).not.toExist() - expect(atom.themes.stylesheetElementForId(two)).not.toExist() - expect(atom.themes.stylesheetElementForId(three)).not.toExist() - - it "removes the package's scoped-properties", -> - waitsForPromise -> - atom.packages.activatePackage("package-with-settings") - - runs -> - expect(atom.config.get 'editor.increaseIndentPattern', scope: ['.source.omg']).toBe '^a' - - waitsForPromise -> - atom.packages.deactivatePackage("package-with-settings") - - runs -> - expect(atom.config.get 'editor.increaseIndentPattern', scope: ['.source.omg']).toBeUndefined() - - it "invokes ::onDidDeactivatePackage listeners with the deactivated package", -> - deactivatedPackage = null - - waitsForPromise -> - atom.packages.activatePackage("package-with-main") - - runs -> - atom.packages.onDidDeactivatePackage (pack) -> deactivatedPackage = pack - - waitsForPromise -> - atom.packages.deactivatePackage("package-with-main") - - runs -> - expect(deactivatedPackage.name).toBe "package-with-main" - - describe "::activate()", -> - beforeEach -> - spyOn(atom, 'inSpecMode').andReturn(false) - jasmine.snapshotDeprecations() - spyOn(console, 'warn') - atom.packages.loadPackages() - - loadedPackages = atom.packages.getLoadedPackages() - expect(loadedPackages.length).toBeGreaterThan 0 - - afterEach -> - waitsForPromise -> - atom.packages.deactivatePackages() - runs -> - atom.packages.unloadPackages() - jasmine.restoreDeprecationsSnapshot() - - it "sets hasActivatedInitialPackages", -> - spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(null) - spyOn(atom.packages, 'activatePackages') - expect(atom.packages.hasActivatedInitialPackages()).toBe false - waitsForPromise -> atom.packages.activate() - runs -> expect(atom.packages.hasActivatedInitialPackages()).toBe true - - it "activates all the packages, and none of the themes", -> - packageActivator = spyOn(atom.packages, 'activatePackages') - themeActivator = spyOn(atom.themes, 'activatePackages') - - atom.packages.activate() - - expect(packageActivator).toHaveBeenCalled() - expect(themeActivator).toHaveBeenCalled() - - packages = packageActivator.mostRecentCall.args[0] - expect(['atom', 'textmate']).toContain(pack.getType()) for pack in packages - - themes = themeActivator.mostRecentCall.args[0] - expect(['theme']).toContain(theme.getType()) for theme in themes - - it "calls callbacks registered with ::onDidActivateInitialPackages", -> - package1 = atom.packages.loadPackage('package-with-main') - package2 = atom.packages.loadPackage('package-with-index') - package3 = atom.packages.loadPackage('package-with-activation-commands') - spyOn(atom.packages, 'getLoadedPackages').andReturn([package1, package2, package3]) - spyOn(atom.themes, 'activatePackages') - activateSpy = jasmine.createSpy('activateSpy') - atom.packages.onDidActivateInitialPackages(activateSpy) - - atom.packages.activate() - waitsFor -> activateSpy.callCount > 0 - runs -> - jasmine.unspy(atom.packages, 'getLoadedPackages') - expect(package1 in atom.packages.getActivePackages()).toBe true - expect(package2 in atom.packages.getActivePackages()).toBe true - expect(package3 in atom.packages.getActivePackages()).toBe false - - describe "::enablePackage(id) and ::disablePackage(id)", -> - describe "with packages", -> - it "enables a disabled package", -> - packageName = 'package-with-main' - atom.config.pushAtKeyPath('core.disabledPackages', packageName) - atom.packages.observeDisabledPackages() - expect(atom.config.get('core.disabledPackages')).toContain packageName - - pack = atom.packages.enablePackage(packageName) - loadedPackages = atom.packages.getLoadedPackages() - activatedPackages = null - waitsFor -> - activatedPackages = atom.packages.getActivePackages() - activatedPackages.length > 0 - - runs -> - expect(loadedPackages).toContain(pack) - expect(activatedPackages).toContain(pack) - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - - it "disables an enabled package", -> - packageName = 'package-with-main' - pack = null - activatedPackages = null - - waitsForPromise -> - atom.packages.activatePackage(packageName) - - runs -> - atom.packages.observeDisabledPackages() - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - - pack = atom.packages.disablePackage(packageName) - - waitsFor -> - activatedPackages = atom.packages.getActivePackages() - activatedPackages.length is 0 - - runs -> - expect(activatedPackages).not.toContain(pack) - expect(atom.config.get('core.disabledPackages')).toContain packageName - - it "returns null if the package cannot be loaded", -> - spyOn(console, 'warn') - expect(atom.packages.enablePackage("this-doesnt-exist")).toBeNull() - expect(console.warn.callCount).toBe 1 - - it "does not disable an already disabled package", -> - packageName = 'package-with-main' - atom.config.pushAtKeyPath('core.disabledPackages', packageName) - atom.packages.observeDisabledPackages() - expect(atom.config.get('core.disabledPackages')).toContain packageName - - atom.packages.disablePackage(packageName) - packagesDisabled = atom.config.get('core.disabledPackages').filter((pack) -> pack is packageName) - expect(packagesDisabled.length).toEqual 1 - - describe "with themes", -> - didChangeActiveThemesHandler = null - - beforeEach -> - waitsForPromise -> - atom.themes.activateThemes() - - afterEach -> - waitsForPromise -> - atom.themes.deactivateThemes() - - it "enables and disables a theme", -> - packageName = 'theme-with-package-file' - - expect(atom.config.get('core.themes')).not.toContain packageName - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - - # enabling of theme - pack = atom.packages.enablePackage(packageName) - - waitsFor 'theme to enable', 500, -> - pack in atom.packages.getActivePackages() - - runs -> - expect(atom.config.get('core.themes')).toContain packageName - expect(atom.config.get('core.disabledPackages')).not.toContain packageName - - didChangeActiveThemesHandler = jasmine.createSpy('didChangeActiveThemesHandler') - didChangeActiveThemesHandler.reset() - atom.themes.onDidChangeActiveThemes didChangeActiveThemesHandler - - pack = atom.packages.disablePackage(packageName) - - waitsFor 'did-change-active-themes event to fire', 500, -> - didChangeActiveThemesHandler.callCount is 1 - - runs -> - expect(atom.packages.getActivePackages()).not.toContain pack - expect(atom.config.get('core.themes')).not.toContain packageName - expect(atom.config.get('core.themes')).not.toContain packageName - expect(atom.config.get('core.disabledPackages')).not.toContain packageName diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js new file mode 100644 index 000000000..3cd25383f --- /dev/null +++ b/spec/package-manager-spec.js @@ -0,0 +1,1547 @@ +const path = require('path') +const Package = require('../src/package') +const PackageManager = require('../src/package-manager') +const temp = require('temp').track() +const fs = require('fs-plus') +const {Disposable} = require('atom') +const {buildKeydownEvent} = require('../src/keymap-extensions') +const {mockLocalStorage} = require('./spec-helper') +const ModuleCache = require('../src/module-cache') + +describe('PackageManager', () => { + function createTestElement (className) { + const element = document.createElement('div') + element.className = className + return element + } + + beforeEach(() => { + spyOn(ModuleCache, 'add') + }) + + describe('initialize', () => { + it('adds regular package path', () => { + const packageManger = new PackageManager({}) + const configDirPath = path.join('~', 'someConfig') + packageManger.initialize({configDirPath}) + expect(packageManger.packageDirPaths.length).toBe(1) + expect(packageManger.packageDirPaths[0]).toBe(path.join(configDirPath, 'packages')) + }) + + it('adds regular package path and dev package path in dev mode', () => { + const packageManger = new PackageManager({}) + const configDirPath = path.join('~', 'someConfig') + packageManger.initialize({configDirPath, devMode: true}) + expect(packageManger.packageDirPaths.length).toBe(2) + expect(packageManger.packageDirPaths).toContain(path.join(configDirPath, 'packages')) + expect(packageManger.packageDirPaths).toContain(path.join(configDirPath, 'dev', 'packages')) + }) + }) + + describe('::getApmPath()', () => { + it('returns the path to the apm command', () => { + let apmPath = path.join(process.resourcesPath, 'app', 'apm', 'bin', 'apm') + if (process.platform === 'win32') { + apmPath += '.cmd' + } + expect(atom.packages.getApmPath()).toBe(apmPath) + }) + + describe('when the core.apmPath setting is set', () => { + beforeEach(() => atom.config.set('core.apmPath', '/path/to/apm')) + + it('returns the value of the core.apmPath config setting', () => expect(atom.packages.getApmPath()).toBe('/path/to/apm')) + }) + }) + + describe('::loadPackages()', () => { + beforeEach(() => spyOn(atom.packages, 'loadAvailablePackage')) + + afterEach(() => { + waitsForPromise(() => atom.packages.deactivatePackages()) + runs(() => atom.packages.unloadPackages()) + }) + + it('sets hasLoadedInitialPackages', () => { + expect(atom.packages.hasLoadedInitialPackages()).toBe(false) + atom.packages.loadPackages() + expect(atom.packages.hasLoadedInitialPackages()).toBe(true) + }) + }) + + describe('::loadPackage(name)', () => { + beforeEach(() => atom.config.set('core.disabledPackages', [])) + + it('returns the package', () => { + const pack = atom.packages.loadPackage('package-with-index') + expect(pack instanceof Package).toBe(true) + expect(pack.metadata.name).toBe('package-with-index') + }) + + it('returns the package if it has an invalid keymap', () => { + spyOn(atom, 'inSpecMode').andReturn(false) + const pack = atom.packages.loadPackage('package-with-broken-keymap') + expect(pack instanceof Package).toBe(true) + expect(pack.metadata.name).toBe('package-with-broken-keymap') + }) + + it('returns the package if it has an invalid stylesheet', () => { + spyOn(atom, 'inSpecMode').andReturn(false) + const pack = atom.packages.loadPackage('package-with-invalid-styles') + expect(pack instanceof Package).toBe(true) + expect(pack.metadata.name).toBe('package-with-invalid-styles') + expect(pack.stylesheets.length).toBe(0) + + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + expect(() => pack.reloadStylesheets()).not.toThrow() + expect(addErrorHandler.callCount).toBe(2) + expect(addErrorHandler.argsForCall[1][0].message).toContain('Failed to reload the package-with-invalid-styles package stylesheets') + expect(addErrorHandler.argsForCall[1][0].options.packageName).toEqual('package-with-invalid-styles') + }) + + it('returns null if the package has an invalid package.json', () => { + spyOn(atom, 'inSpecMode').andReturn(false) + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + expect(atom.packages.loadPackage('package-with-broken-package-json')).toBeNull() + expect(addErrorHandler.callCount).toBe(1) + expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to load the package-with-broken-package-json package') + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-broken-package-json') + }) + + it('returns null if the package name or path starts with a dot', () => expect(atom.packages.loadPackage('/Users/user/.atom/packages/.git')).toBeNull()) + + it('normalizes short repository urls in package.json', () => { + let {metadata} = atom.packages.loadPackage('package-with-short-url-package-json') + expect(metadata.repository.type).toBe('git') + expect(metadata.repository.url).toBe('https://github.com/example/repo'); + + ({metadata} = atom.packages.loadPackage('package-with-invalid-url-package-json')) + expect(metadata.repository.type).toBe('git') + expect(metadata.repository.url).toBe('foo') + }) + + it('trims git+ from the beginning and .git from the end of repository URLs, even if npm already normalized them ', () => { + const {metadata} = atom.packages.loadPackage('package-with-prefixed-and-suffixed-repo-url') + expect(metadata.repository.type).toBe('git') + expect(metadata.repository.url).toBe('https://github.com/example/repo') + }) + + it('returns null if the package is not found in any package directory', () => { + spyOn(console, 'warn') + expect(atom.packages.loadPackage('this-package-cannot-be-found')).toBeNull() + expect(console.warn.callCount).toBe(1) + expect(console.warn.argsForCall[0][0]).toContain('Could not resolve') + }) + + describe('when the package is deprecated', () => { + it('returns null', () => { + spyOn(console, 'warn') + expect(atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'packages', 'wordcount'))).toBeNull() + expect(atom.packages.isDeprecatedPackage('wordcount', '2.1.9')).toBe(true) + expect(atom.packages.isDeprecatedPackage('wordcount', '2.2.0')).toBe(true) + expect(atom.packages.isDeprecatedPackage('wordcount', '2.2.1')).toBe(false) + expect(atom.packages.getDeprecatedPackageMetadata('wordcount').version).toBe('<=2.2.0') + }) + }) + + it('invokes ::onDidLoadPackage listeners with the loaded package', () => { + let loadedPackage = null + + atom.packages.onDidLoadPackage(pack => { + loadedPackage = pack + }) + + atom.packages.loadPackage('package-with-main') + + expect(loadedPackage.name).toBe('package-with-main') + }) + + it("registers any deserializers specified in the package's package.json", () => { + atom.packages.loadPackage('package-with-deserializers') + + const state1 = {deserializer: 'Deserializer1', a: 'b'} + expect(atom.deserializers.deserialize(state1)).toEqual({ + wasDeserializedBy: 'deserializeMethod1', + state: state1 + }) + + const state2 = {deserializer: 'Deserializer2', c: 'd'} + expect(atom.deserializers.deserialize(state2)).toEqual({ + wasDeserializedBy: 'deserializeMethod2', + state: state2 + }) + }) + + it('early-activates any atom.directory-provider or atom.repository-provider services that the package provide', () => { + jasmine.useRealClock() + + const providers = [] + atom.packages.serviceHub.consume('atom.directory-provider', '^0.1.0', provider => providers.push(provider)) + + atom.packages.loadPackage('package-with-directory-provider') + expect(providers.map(p => p.name)).toEqual(['directory provider from package-with-directory-provider']) + }) + + describe("when there are view providers specified in the package's package.json", () => { + const model1 = {worksWithViewProvider1: true} + const model2 = {worksWithViewProvider2: true} + + afterEach(() => { + waitsForPromise(() => atom.packages.deactivatePackage('package-with-view-providers')) + runs(() => atom.packages.unloadPackage('package-with-view-providers')) + }) + + it('does not load the view providers immediately', () => { + const pack = atom.packages.loadPackage('package-with-view-providers') + expect(pack.mainModule).toBeNull() + + expect(() => atom.views.getView(model1)).toThrow() + expect(() => atom.views.getView(model2)).toThrow() + }) + + it('registers the view providers when the package is activated', () => { + atom.packages.loadPackage('package-with-view-providers') + + waitsForPromise(() => + atom.packages.activatePackage('package-with-view-providers').then(() => { + const element1 = atom.views.getView(model1) + expect(element1 instanceof HTMLDivElement).toBe(true) + expect(element1.dataset.createdBy).toBe('view-provider-1') + + const element2 = atom.views.getView(model2) + expect(element2 instanceof HTMLDivElement).toBe(true) + expect(element2.dataset.createdBy).toBe('view-provider-2') + }) + ) + }) + + it("registers the view providers when any of the package's deserializers are used", () => { + atom.packages.loadPackage('package-with-view-providers') + + spyOn(atom.views, 'addViewProvider').andCallThrough() + atom.deserializers.deserialize({ + deserializer: 'DeserializerFromPackageWithViewProviders', + a: 'b' + }) + expect(atom.views.addViewProvider.callCount).toBe(2) + + atom.deserializers.deserialize({ + deserializer: 'DeserializerFromPackageWithViewProviders', + a: 'b' + }) + expect(atom.views.addViewProvider.callCount).toBe(2) + + const element1 = atom.views.getView(model1) + expect(element1 instanceof HTMLDivElement).toBe(true) + expect(element1.dataset.createdBy).toBe('view-provider-1') + + const element2 = atom.views.getView(model2) + expect(element2 instanceof HTMLDivElement).toBe(true) + expect(element2.dataset.createdBy).toBe('view-provider-2') + }) + }) + + it("registers the config schema in the package's metadata, if present", () => { + let pack = atom.packages.loadPackage('package-with-json-config-schema') + expect(atom.config.getSchema('package-with-json-config-schema')).toEqual({ + type: 'object', + properties: { + a: {type: 'number', default: 5}, + b: {type: 'string', default: 'five'} + } + }) + + expect(pack.mainModule).toBeNull() + + atom.packages.unloadPackage('package-with-json-config-schema') + atom.config.clear() + + pack = atom.packages.loadPackage('package-with-json-config-schema') + expect(atom.config.getSchema('package-with-json-config-schema')).toEqual({ + type: 'object', + properties: { + a: {type: 'number', default: 5}, + b: {type: 'string', default: 'five'} + } + }) + }) + + describe('when a package does not have deserializers, view providers or a config schema in its package.json', () => { + beforeEach(() => mockLocalStorage()) + + it("defers loading the package's main module if the package previously used no Atom APIs when its main module was required", () => { + const pack1 = atom.packages.loadPackage('package-with-main') + expect(pack1.mainModule).toBeDefined() + + atom.packages.unloadPackage('package-with-main') + + const pack2 = atom.packages.loadPackage('package-with-main') + expect(pack2.mainModule).toBeNull() + }) + + it("does not defer loading the package's main module if the package previously used Atom APIs when its main module was required", () => { + const pack1 = atom.packages.loadPackage('package-with-eval-time-api-calls') + expect(pack1.mainModule).toBeDefined() + + atom.packages.unloadPackage('package-with-eval-time-api-calls') + + const pack2 = atom.packages.loadPackage('package-with-eval-time-api-calls') + expect(pack2.mainModule).not.toBeNull() + }) + }) + }) + + describe('::loadAvailablePackage(availablePackage)', () => { + describe('if the package was preloaded', () => { + it('adds the package path to the module cache', () => { + const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check') + availablePackage.isBundled = true + expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) + + const metadata = atom.packages.loadPackageMetadata(availablePackage) + atom.packages.preloadPackage( + availablePackage.name, + { + rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), + metadata + } + ) + atom.packages.loadAvailablePackage(availablePackage) + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(true) + expect(ModuleCache.add).toHaveBeenCalledWith(availablePackage.path, metadata) + }) + + it('deactivates it if it had been disabled', () => { + const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check') + availablePackage.isBundled = true + expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) + + const metadata = atom.packages.loadPackageMetadata(availablePackage) + const preloadedPackage = atom.packages.preloadPackage( + availablePackage.name, + { + rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), + metadata + } + ) + expect(preloadedPackage.keymapActivated).toBe(true) + expect(preloadedPackage.settingsActivated).toBe(true) + expect(preloadedPackage.menusActivated).toBe(true) + + atom.packages.loadAvailablePackage(availablePackage, new Set([availablePackage.name])) + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) + expect(preloadedPackage.keymapActivated).toBe(false) + expect(preloadedPackage.settingsActivated).toBe(false) + expect(preloadedPackage.menusActivated).toBe(false) + }) + + it('deactivates it and reloads the new one if trying to load the same package outside of the bundle', () => { + const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check') + availablePackage.isBundled = true + expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) + + const metadata = atom.packages.loadPackageMetadata(availablePackage) + const preloadedPackage = atom.packages.preloadPackage( + availablePackage.name, + { + rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), + metadata + } + ) + expect(preloadedPackage.keymapActivated).toBe(true) + expect(preloadedPackage.settingsActivated).toBe(true) + expect(preloadedPackage.menusActivated).toBe(true) + + availablePackage.isBundled = false + atom.packages.loadAvailablePackage(availablePackage) + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(true) + expect(preloadedPackage.keymapActivated).toBe(false) + expect(preloadedPackage.settingsActivated).toBe(false) + expect(preloadedPackage.menusActivated).toBe(false) + }) + }) + + describe('if the package was not preloaded', () => { + it('adds the package path to the module cache', () => { + const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check') + availablePackage.isBundled = true + const metadata = atom.packages.loadPackageMetadata(availablePackage) + atom.packages.loadAvailablePackage(availablePackage) + expect(ModuleCache.add).toHaveBeenCalledWith(availablePackage.path, metadata) + }) + }) + }) + + describe('preloading', () => { + it('requires the main module, loads the config schema and activates keymaps, menus and settings without reactivating them during package activation', () => { + const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check') + availablePackage.isBundled = true + const metadata = atom.packages.loadPackageMetadata(availablePackage) + expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) + + atom.packages.packagesCache = {} + atom.packages.packagesCache[availablePackage.name] = { + main: path.join(availablePackage.path, metadata.main), + grammarPaths: [] + } + const preloadedPackage = atom.packages.preloadPackage( + availablePackage.name, + { + rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), + metadata + } + ) + expect(preloadedPackage.keymapActivated).toBe(true) + expect(preloadedPackage.settingsActivated).toBe(true) + expect(preloadedPackage.menusActivated).toBe(true) + expect(preloadedPackage.mainModule).toBeTruthy() + expect(preloadedPackage.configSchemaRegisteredOnLoad).toBeTruthy() + + spyOn(atom.keymaps, 'add') + spyOn(atom.menu, 'add') + spyOn(atom.contextMenu, 'add') + spyOn(atom.config, 'setSchema') + + atom.packages.loadAvailablePackage(availablePackage) + expect(preloadedPackage.getMainModulePath()).toBe(path.join(availablePackage.path, metadata.main)) + + atom.packages.activatePackage(availablePackage.name) + expect(atom.keymaps.add).not.toHaveBeenCalled() + expect(atom.menu.add).not.toHaveBeenCalled() + expect(atom.contextMenu.add).not.toHaveBeenCalled() + expect(atom.config.setSchema).not.toHaveBeenCalled() + expect(preloadedPackage.keymapActivated).toBe(true) + expect(preloadedPackage.settingsActivated).toBe(true) + expect(preloadedPackage.menusActivated).toBe(true) + expect(preloadedPackage.mainModule).toBeTruthy() + expect(preloadedPackage.configSchemaRegisteredOnLoad).toBeTruthy() + }) + + it('deactivates disabled keymaps during package activation', () => { + const availablePackage = atom.packages.getAvailablePackages().find(p => p.name === 'spell-check') + availablePackage.isBundled = true + const metadata = atom.packages.loadPackageMetadata(availablePackage) + expect(atom.packages.preloadedPackages[availablePackage.name]).toBeUndefined() + expect(atom.packages.isPackageLoaded(availablePackage.name)).toBe(false) + + atom.packages.packagesCache = {} + atom.packages.packagesCache[availablePackage.name] = { + main: path.join(availablePackage.path, metadata.main), + grammarPaths: [] + } + const preloadedPackage = atom.packages.preloadPackage( + availablePackage.name, + { + rootDirPath: path.relative(atom.packages.resourcePath, availablePackage.path), + metadata + } + ) + expect(preloadedPackage.keymapActivated).toBe(true) + expect(preloadedPackage.settingsActivated).toBe(true) + expect(preloadedPackage.menusActivated).toBe(true) + + atom.packages.loadAvailablePackage(availablePackage) + atom.config.set('core.packagesWithKeymapsDisabled', [availablePackage.name]) + atom.packages.activatePackage(availablePackage.name) + + expect(preloadedPackage.keymapActivated).toBe(false) + expect(preloadedPackage.settingsActivated).toBe(true) + expect(preloadedPackage.menusActivated).toBe(true) + }) + }) + + describe('::unloadPackage(name)', () => { + describe('when the package is active', () => { + it('throws an error', () => { + let pack + + waitsForPromise(() => atom.packages.activatePackage('package-with-main').then(p => { + pack = p + })) + + runs(() => { + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() + expect(() => atom.packages.unloadPackage(pack.name)).toThrow() + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() + }) + }) + }) + + describe('when the package is not loaded', () => { + it('throws an error', () => { + expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy() + expect(() => atom.packages.unloadPackage('unloaded')).toThrow() + expect(atom.packages.isPackageLoaded('unloaded')).toBeFalsy() + }) + }) + + describe('when the package is loaded', () => { + it('no longers reports it as being loaded', () => { + const pack = atom.packages.loadPackage('package-with-main') + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + atom.packages.unloadPackage(pack.name) + expect(atom.packages.isPackageLoaded(pack.name)).toBeFalsy() + }) + }) + + it('invokes ::onDidUnloadPackage listeners with the unloaded package', () => { + atom.packages.loadPackage('package-with-main') + let unloadedPackage + atom.packages.onDidUnloadPackage(pack => { + unloadedPackage = pack + }) + atom.packages.unloadPackage('package-with-main') + expect(unloadedPackage.name).toBe('package-with-main') + }) + }) + + describe('::activatePackage(id)', () => { + describe('when called multiple times', () => { + it('it only calls activate on the package once', () => { + spyOn(Package.prototype, 'activateNow').andCallThrough() + waitsForPromise(() => atom.packages.activatePackage('package-with-index')) + waitsForPromise(() => atom.packages.activatePackage('package-with-index')) + waitsForPromise(() => atom.packages.activatePackage('package-with-index')) + + runs(() => expect(Package.prototype.activateNow.callCount).toBe(1)) + }) + }) + + describe('when the package has a main module', () => { + describe('when the metadata specifies a main module path˜', () => { + it('requires the module at the specified path', () => { + const mainModule = require('./fixtures/packages/package-with-main/main-module') + spyOn(mainModule, 'activate') + + let pack + waitsForPromise(() => atom.packages.activatePackage('package-with-main').then(p => { + pack = p + })) + + runs(() => { + expect(mainModule.activate).toHaveBeenCalled() + expect(pack.mainModule).toBe(mainModule) + }) + }) + }) + + describe('when the metadata does not specify a main module', () => { + it('requires index.coffee', () => { + const indexModule = require('./fixtures/packages/package-with-index/index') + spyOn(indexModule, 'activate') + let pack + waitsForPromise(() => atom.packages.activatePackage('package-with-index').then(p => { + pack = p + })) + + runs(() => { + expect(indexModule.activate).toHaveBeenCalled() + expect(pack.mainModule).toBe(indexModule) + }) + }) + }) + + it('assigns config schema, including defaults when package contains a schema', () => { + expect(atom.config.get('package-with-config-schema.numbers.one')).toBeUndefined() + + waitsForPromise(() => atom.packages.activatePackage('package-with-config-schema')) + + runs(() => { + expect(atom.config.get('package-with-config-schema.numbers.one')).toBe(1) + expect(atom.config.get('package-with-config-schema.numbers.two')).toBe(2) + + expect(atom.config.set('package-with-config-schema.numbers.one', 'nope')).toBe(false) + expect(atom.config.set('package-with-config-schema.numbers.one', '10')).toBe(true) + expect(atom.config.get('package-with-config-schema.numbers.one')).toBe(10) + }) + }) + + describe('when the package metadata includes `activationCommands`', () => { + let mainModule, promise, workspaceCommandListener, registration + + beforeEach(() => { + jasmine.attachToDOM(atom.workspace.getElement()) + mainModule = require('./fixtures/packages/package-with-activation-commands/index') + mainModule.activationCommandCallCount = 0 + spyOn(mainModule, 'activate').andCallThrough() + spyOn(Package.prototype, 'requireMainModule').andCallThrough() + + workspaceCommandListener = jasmine.createSpy('workspaceCommandListener') + registration = atom.commands.add('.workspace', 'activation-command', workspaceCommandListener) + + promise = atom.packages.activatePackage('package-with-activation-commands') + }) + + afterEach(() => { + if (registration) { + registration.dispose() + } + mainModule = null + }) + + it('defers requiring/activating the main module until an activation event bubbles to the root view', () => { + expect(Package.prototype.requireMainModule.callCount).toBe(0) + + atom.workspace.getElement().dispatchEvent(new CustomEvent('activation-command', {bubbles: true})) + + waitsForPromise(() => promise) + + runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(1)) + }) + + it('triggers the activation event on all handlers registered during activation', () => { + waitsForPromise(() => atom.workspace.open()) + + runs(() => { + const editorElement = atom.workspace.getActiveTextEditor().getElement() + const editorCommandListener = jasmine.createSpy('editorCommandListener') + atom.commands.add('atom-text-editor', 'activation-command', editorCommandListener) + atom.commands.dispatch(editorElement, 'activation-command') + expect(mainModule.activate.callCount).toBe(1) + expect(mainModule.activationCommandCallCount).toBe(1) + expect(editorCommandListener.callCount).toBe(1) + expect(workspaceCommandListener.callCount).toBe(1) + atom.commands.dispatch(editorElement, 'activation-command') + expect(mainModule.activationCommandCallCount).toBe(2) + expect(editorCommandListener.callCount).toBe(2) + expect(workspaceCommandListener.callCount).toBe(2) + expect(mainModule.activate.callCount).toBe(1) + }) + }) + + it('activates the package immediately when the events are empty', () => { + mainModule = require('./fixtures/packages/package-with-empty-activation-commands/index') + spyOn(mainModule, 'activate').andCallThrough() + + waitsForPromise(() => atom.packages.activatePackage('package-with-empty-activation-commands')) + + runs(() => expect(mainModule.activate.callCount).toBe(1)) + }) + + it('adds a notification when the activation commands are invalid', () => { + spyOn(atom, 'inSpecMode').andReturn(false) + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + expect(() => atom.packages.activatePackage('package-with-invalid-activation-commands')).not.toThrow() + expect(addErrorHandler.callCount).toBe(1) + expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to activate the package-with-invalid-activation-commands package') + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-activation-commands') + }) + + it('adds a notification when the context menu is invalid', () => { + spyOn(atom, 'inSpecMode').andReturn(false) + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + expect(() => atom.packages.activatePackage('package-with-invalid-context-menu')).not.toThrow() + expect(addErrorHandler.callCount).toBe(1) + expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to activate the package-with-invalid-context-menu package') + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-context-menu') + }) + + it('adds a notification when the grammar is invalid', () => { + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + + expect(() => atom.packages.activatePackage('package-with-invalid-grammar')).not.toThrow() + + waitsFor(() => addErrorHandler.callCount > 0) + + runs(() => { + expect(addErrorHandler.callCount).toBe(1) + expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to load a package-with-invalid-grammar package grammar') + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-grammar') + }) + }) + + it('adds a notification when the settings are invalid', () => { + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + + expect(() => atom.packages.activatePackage('package-with-invalid-settings')).not.toThrow() + + waitsFor(() => addErrorHandler.callCount > 0) + + runs(() => { + expect(addErrorHandler.callCount).toBe(1) + expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to load the package-with-invalid-settings package settings') + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-settings') + }) + }) + }) + }) + + describe('when the package metadata includes `activationHooks`', () => { + let mainModule, promise + + beforeEach(() => { + mainModule = require('./fixtures/packages/package-with-activation-hooks/index') + spyOn(mainModule, 'activate').andCallThrough() + spyOn(Package.prototype, 'requireMainModule').andCallThrough() + }) + + it('defers requiring/activating the main module until an triggering of an activation hook occurs', () => { + promise = atom.packages.activatePackage('package-with-activation-hooks') + expect(Package.prototype.requireMainModule.callCount).toBe(0) + atom.packages.triggerActivationHook('language-fictitious:grammar-used') + atom.packages.triggerDeferredActivationHooks() + + waitsForPromise(() => promise) + + runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(1)) + }) + + it('does not double register activation hooks when deactivating and reactivating', () => { + promise = atom.packages.activatePackage('package-with-activation-hooks') + expect(mainModule.activate.callCount).toBe(0) + atom.packages.triggerActivationHook('language-fictitious:grammar-used') + atom.packages.triggerDeferredActivationHooks() + + waitsForPromise(() => promise) + + runs(() => expect(mainModule.activate.callCount).toBe(1)) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-activation-hooks')) + + runs(() => { + promise = atom.packages.activatePackage('package-with-activation-hooks') + atom.packages.triggerActivationHook('language-fictitious:grammar-used') + atom.packages.triggerDeferredActivationHooks() + }) + + waitsForPromise(() => promise) + + runs(() => expect(mainModule.activate.callCount).toBe(2)) + }) + + it('activates the package immediately when activationHooks is empty', () => { + mainModule = require('./fixtures/packages/package-with-empty-activation-hooks/index') + spyOn(mainModule, 'activate').andCallThrough() + + runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(0)) + + waitsForPromise(() => atom.packages.activatePackage('package-with-empty-activation-hooks')) + + runs(() => { + expect(mainModule.activate.callCount).toBe(1) + expect(Package.prototype.requireMainModule.callCount).toBe(1) + }) + }) + + it('activates the package immediately if the activation hook had already been triggered', () => { + atom.packages.triggerActivationHook('language-fictitious:grammar-used') + atom.packages.triggerDeferredActivationHooks() + expect(Package.prototype.requireMainModule.callCount).toBe(0) + + waitsForPromise(() => atom.packages.activatePackage('package-with-activation-hooks')) + + runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(1)) + }) + }) + + describe('when the package has no main module', () => { + it('does not throw an exception', () => { + spyOn(console, 'error') + spyOn(console, 'warn').andCallThrough() + expect(() => atom.packages.activatePackage('package-without-module')).not.toThrow() + expect(console.error).not.toHaveBeenCalled() + expect(console.warn).not.toHaveBeenCalled() + }) + }) + + describe('when the package does not export an activate function', () => { + it('activates the package and does not throw an exception or log a warning', () => { + spyOn(console, 'warn') + expect(() => atom.packages.activatePackage('package-with-no-activate')).not.toThrow() + + waitsFor(() => atom.packages.isPackageActive('package-with-no-activate')) + + runs(() => expect(console.warn).not.toHaveBeenCalled()) + }) + }) + + it("passes the activate method the package's previously serialized state if it exists", () => { + let pack + waitsForPromise(() => atom.packages.activatePackage('package-with-serialization').then(p => { + pack = p + })) + runs(() => { + expect(pack.mainModule.someNumber).not.toBe(77) + pack.mainModule.someNumber = 77 + atom.packages.serializePackage('package-with-serialization') + }) + waitsForPromise(() => atom.packages.deactivatePackage('package-with-serialization')) + runs(() => spyOn(pack.mainModule, 'activate').andCallThrough()) + waitsForPromise(() => atom.packages.activatePackage('package-with-serialization')) + runs(() => expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77})) + }) + + it('invokes ::onDidActivatePackage listeners with the activated package', () => { + let activatedPackage + atom.packages.onDidActivatePackage(pack => { + activatedPackage = pack + }) + + atom.packages.activatePackage('package-with-main') + + waitsFor(() => activatedPackage) + runs(() => expect(activatedPackage.name).toBe('package-with-main')) + }) + + describe("when the package's main module throws an error on load", () => { + it('adds a notification instead of throwing an exception', () => { + spyOn(atom, 'inSpecMode').andReturn(false) + atom.config.set('core.disabledPackages', []) + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + expect(() => atom.packages.activatePackage('package-that-throws-an-exception')).not.toThrow() + expect(addErrorHandler.callCount).toBe(1) + expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to load the package-that-throws-an-exception package') + expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-that-throws-an-exception') + }) + + it('re-throws the exception in test mode', () => { + atom.config.set('core.disabledPackages', []) + expect(() => atom.packages.activatePackage('package-that-throws-an-exception')).toThrow('This package throws an exception') + }) + }) + + describe('when the package is not found', () => { + it('rejects the promise', () => { + atom.config.set('core.disabledPackages', []) + + const onSuccess = jasmine.createSpy('onSuccess') + const onFailure = jasmine.createSpy('onFailure') + spyOn(console, 'warn') + + atom.packages.activatePackage('this-doesnt-exist').then(onSuccess, onFailure) + + waitsFor('promise to be rejected', () => onFailure.callCount > 0) + + runs(() => { + expect(console.warn.callCount).toBe(1) + expect(onFailure.mostRecentCall.args[0] instanceof Error).toBe(true) + expect(onFailure.mostRecentCall.args[0].message).toContain("Failed to load package 'this-doesnt-exist'") + }) + }) + }) + + describe('keymap loading', () => { + describe("when the metadata does not contain a 'keymaps' manifest", () => { + it('loads all the .cson/.json files in the keymaps directory', () => { + const element1 = createTestElement('test-1') + const element2 = createTestElement('test-2') + const element3 = createTestElement('test-3') + + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element2})).toHaveLength(0) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element3})).toHaveLength(0) + + waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) + + runs(() => { + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('test-1') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element2})[0].command).toBe('test-2') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element3})).toHaveLength(0) + }) + }) + }) + + describe("when the metadata contains a 'keymaps' manifest", () => { + it('loads only the keymaps specified by the manifest, in the specified order', () => { + const element1 = createTestElement('test-1') + const element3 = createTestElement('test-3') + + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) + + waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps-manifest')) + + runs(() => { + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('keymap-1') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-n', target: element1})[0].command).toBe('keymap-2') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-y', target: element3})).toHaveLength(0) + }) + }) + }) + + describe('when the keymap file is empty', () => { + it('does not throw an error on activation', () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-empty-keymap')) + + runs(() => expect(atom.packages.isPackageActive('package-with-empty-keymap')).toBe(true)) + }) + }) + + describe("when the package's keymaps have been disabled", () => { + it('does not add the keymaps', () => { + const element1 = createTestElement('test-1') + + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) + + atom.config.set('core.packagesWithKeymapsDisabled', ['package-with-keymaps-manifest']) + + waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps-manifest')) + + runs(() => expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0)) + }) + }) + + describe('when setting core.packagesWithKeymapsDisabled', () => { + it("ignores package names in the array that aren't loaded", () => { + atom.packages.observePackagesWithKeymapsDisabled() + + expect(() => atom.config.set('core.packagesWithKeymapsDisabled', ['package-does-not-exist'])).not.toThrow() + expect(() => atom.config.set('core.packagesWithKeymapsDisabled', [])).not.toThrow() + }) + }) + + describe("when the package's keymaps are disabled and re-enabled after it is activated", () => { + it('removes and re-adds the keymaps', () => { + const element1 = createTestElement('test-1') + atom.packages.observePackagesWithKeymapsDisabled() + + waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps-manifest')) + + runs(() => { + atom.config.set('core.packagesWithKeymapsDisabled', ['package-with-keymaps-manifest']) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) + + atom.config.set('core.packagesWithKeymapsDisabled', []) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('keymap-1') + }) + }) + }) + + describe('when the package is de-activated and re-activated', () => { + let element, events, userKeymapPath + + beforeEach(() => { + userKeymapPath = path.join(temp.mkdirSync(), 'user-keymaps.cson') + spyOn(atom.keymaps, 'getUserKeymapPath').andReturn(userKeymapPath) + + element = createTestElement('test-1') + jasmine.attachToDOM(element) + + events = [] + element.addEventListener('user-command', e => events.push(e)) + element.addEventListener('test-1', e => events.push(e)) + }) + + afterEach(() => { + element.remove() + + // Avoid leaking user keymap subscription + atom.keymaps.watchSubscriptions[userKeymapPath].dispose() + delete atom.keymaps.watchSubscriptions[userKeymapPath] + + temp.cleanupSync() + }) + + it("doesn't override user-defined keymaps", () => { + fs.writeFileSync(userKeymapPath, `".test-1": {"ctrl-z": "user-command"}`) + atom.keymaps.loadUserKeymap() + + waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) + + runs(() => { + atom.keymaps.handleKeyboardEvent(buildKeydownEvent('z', {ctrl: true, target: element})) + + expect(events.length).toBe(1) + expect(events[0].type).toBe('user-command') + }) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-keymaps')) + + waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) + + runs(() => { + atom.keymaps.handleKeyboardEvent(buildKeydownEvent('z', {ctrl: true, target: element})) + expect(events.length).toBe(2) + expect(events[1].type).toBe('user-command') + }) + }) + }) + }) + + describe('menu loading', () => { + beforeEach(() => { + atom.contextMenu.definitions = [] + atom.menu.template = [] + }) + + describe("when the metadata does not contain a 'menus' manifest", () => { + it('loads all the .cson/.json files in the menus directory', () => { + const element = createTestElement('test-1') + + expect(atom.contextMenu.templateForElement(element)).toEqual([]) + + waitsForPromise(() => atom.packages.activatePackage('package-with-menus')) + + runs(() => { + expect(atom.menu.template.length).toBe(2) + expect(atom.menu.template[0].label).toBe('Second to Last') + expect(atom.menu.template[1].label).toBe('Last') + expect(atom.contextMenu.templateForElement(element)[0].label).toBe('Menu item 1') + expect(atom.contextMenu.templateForElement(element)[1].label).toBe('Menu item 2') + expect(atom.contextMenu.templateForElement(element)[2].label).toBe('Menu item 3') + }) + }) + }) + + describe("when the metadata contains a 'menus' manifest", () => { + it('loads only the menus specified by the manifest, in the specified order', () => { + const element = createTestElement('test-1') + + expect(atom.contextMenu.templateForElement(element)).toEqual([]) + + waitsForPromise(() => atom.packages.activatePackage('package-with-menus-manifest')) + + runs(() => { + expect(atom.menu.template[0].label).toBe('Second to Last') + expect(atom.menu.template[1].label).toBe('Last') + expect(atom.contextMenu.templateForElement(element)[0].label).toBe('Menu item 2') + expect(atom.contextMenu.templateForElement(element)[1].label).toBe('Menu item 1') + expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined() + }) + }) + }) + + describe('when the menu file is empty', () => { + it('does not throw an error on activation', () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-empty-menu')) + runs(() => expect(atom.packages.isPackageActive('package-with-empty-menu')).toBe(true)) + }) + }) + }) + + describe('stylesheet loading', () => { + describe("when the metadata contains a 'styleSheets' manifest", () => { + it('loads style sheets from the styles directory as specified by the manifest', () => { + const one = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/1.css') + const two = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/2.less') + const three = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/3.css') + + expect(atom.themes.stylesheetElementForId(one)).toBeNull() + expect(atom.themes.stylesheetElementForId(two)).toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() + + waitsForPromise(() => atom.packages.activatePackage('package-with-style-sheets-manifest')) + + runs(() => { + expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() + + expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe('1px') + }) + }) + }) + + describe("when the metadata does not contain a 'styleSheets' manifest", () => { + it('loads all style sheets from the styles directory', () => { + const one = require.resolve('./fixtures/packages/package-with-styles/styles/1.css') + const two = require.resolve('./fixtures/packages/package-with-styles/styles/2.less') + const three = require.resolve('./fixtures/packages/package-with-styles/styles/3.test-context.css') + const four = require.resolve('./fixtures/packages/package-with-styles/styles/4.css') + + expect(atom.themes.stylesheetElementForId(one)).toBeNull() + expect(atom.themes.stylesheetElementForId(two)).toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() + expect(atom.themes.stylesheetElementForId(four)).toBeNull() + + waitsForPromise(() => atom.packages.activatePackage('package-with-styles')) + + runs(() => { + expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(three)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(four)).not.toBeNull() + expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe('3px') + }) + }) + }) + + it("assigns the stylesheet's context based on the filename", () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-styles')) + + runs(() => { + let count = 0 + + for (let styleElement of atom.styles.getStyleElements()) { + if (styleElement.sourcePath.match(/1.css/)) { + expect(styleElement.context).toBe(undefined) + count++ + } + + if (styleElement.sourcePath.match(/2.less/)) { + expect(styleElement.context).toBe(undefined) + count++ + } + + if (styleElement.sourcePath.match(/3.test-context.css/)) { + expect(styleElement.context).toBe('test-context') + count++ + } + + if (styleElement.sourcePath.match(/4.css/)) { + expect(styleElement.context).toBe(undefined) + count++ + } + } + + expect(count).toBe(4) + }) + }) + }) + + describe('grammar loading', () => { + it("loads the package's grammars", () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-grammars')) + + runs(() => { + expect(atom.grammars.selectGrammar('a.alot').name).toBe('Alot') + expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Alittle') + }) + }) + }) + + describe('scoped-property loading', () => { + it('loads the scoped properties', () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-settings')) + + runs(() => expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a')) + }) + }) + + describe('service registration', () => { + it("registers the package's provided and consumed services", () => { + const consumerModule = require('./fixtures/packages/package-with-consumed-services') + let firstServiceV3Disposed = false + let firstServiceV4Disposed = false + let secondServiceDisposed = false + spyOn(consumerModule, 'consumeFirstServiceV3').andReturn(new Disposable(() => { firstServiceV3Disposed = true })) + spyOn(consumerModule, 'consumeFirstServiceV4').andReturn(new Disposable(() => { firstServiceV4Disposed = true })) + spyOn(consumerModule, 'consumeSecondService').andReturn(new Disposable(() => { secondServiceDisposed = true })) + + waitsForPromise(() => atom.packages.activatePackage('package-with-consumed-services')) + + waitsForPromise(() => atom.packages.activatePackage('package-with-provided-services')) + + runs(() => { + expect(consumerModule.consumeFirstServiceV3.callCount).toBe(1) + expect(consumerModule.consumeFirstServiceV3).toHaveBeenCalledWith('first-service-v3') + expect(consumerModule.consumeFirstServiceV4).toHaveBeenCalledWith('first-service-v4') + expect(consumerModule.consumeSecondService).toHaveBeenCalledWith('second-service') + + consumerModule.consumeFirstServiceV3.reset() + consumerModule.consumeFirstServiceV4.reset() + consumerModule.consumeSecondService.reset() + }) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-provided-services')) + + runs(() => { + expect(firstServiceV3Disposed).toBe(true) + expect(firstServiceV4Disposed).toBe(true) + expect(secondServiceDisposed).toBe(true) + }) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-consumed-services')) + + waitsForPromise(() => atom.packages.activatePackage('package-with-provided-services')) + + runs(() => { + expect(consumerModule.consumeFirstServiceV3).not.toHaveBeenCalled() + expect(consumerModule.consumeFirstServiceV4).not.toHaveBeenCalled() + expect(consumerModule.consumeSecondService).not.toHaveBeenCalled() + }) + }) + + it('ignores provided and consumed services that do not exist', () => { + const addErrorHandler = jasmine.createSpy() + atom.notifications.onDidAddNotification(addErrorHandler) + + waitsForPromise(() => atom.packages.activatePackage('package-with-missing-consumed-services')) + + waitsForPromise(() => atom.packages.activatePackage('package-with-missing-provided-services')) + + runs(() => { + expect(atom.packages.isPackageActive('package-with-missing-consumed-services')).toBe(true) + expect(atom.packages.isPackageActive('package-with-missing-provided-services')).toBe(true) + expect(addErrorHandler.callCount).toBe(0) + }) + }) + }) + }) + + describe('::serialize', () => { + it('does not serialize packages that threw an error during activation', () => { + spyOn(atom, 'inSpecMode').andReturn(false) + spyOn(console, 'warn') + + let badPack + waitsForPromise(() => atom.packages.activatePackage('package-that-throws-on-activate').then(p => { + badPack = p + })) + + runs(() => { + spyOn(badPack.mainModule, 'serialize').andCallThrough() + + atom.packages.serialize() + expect(badPack.mainModule.serialize).not.toHaveBeenCalled() + }) + }) + + it("absorbs exceptions that are thrown by the package module's serialize method", () => { + spyOn(console, 'error') + + waitsForPromise(() => atom.packages.activatePackage('package-with-serialize-error')) + + waitsForPromise(() => atom.packages.activatePackage('package-with-serialization')) + + runs(() => { + atom.packages.serialize() + expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined() + expect(atom.packages.packageStates['package-with-serialization']).toEqual({someNumber: 1}) + expect(console.error).toHaveBeenCalled() + }) + }) + }) + + describe('::deactivatePackages()', () => { + it('deactivates all packages but does not serialize them', () => { + let pack1, pack2 + + waitsForPromise(() => { + atom.packages.activatePackage('package-with-deactivate').then(p => { + pack1 = p + }) + return atom.packages.activatePackage('package-with-serialization').then(p => { + pack2 = p + }) + }) + + runs(() => { + spyOn(pack1.mainModule, 'deactivate') + spyOn(pack2.mainModule, 'serialize') + }) + + waitsForPromise(() => atom.packages.deactivatePackages()) + + runs(() => { + expect(pack1.mainModule.deactivate).toHaveBeenCalled() + expect(pack2.mainModule.serialize).not.toHaveBeenCalled() + }) + }) + }) + + describe('::deactivatePackage(id)', () => { + afterEach(() => atom.packages.unloadPackages()) + + it("calls `deactivate` on the package's main module if activate was successful", () => { + spyOn(atom, 'inSpecMode').andReturn(false) + + let pack + waitsForPromise(() => atom.packages.activatePackage('package-with-deactivate').then(p => { + pack = p + })) + + runs(() => { + expect(atom.packages.isPackageActive('package-with-deactivate')).toBeTruthy() + spyOn(pack.mainModule, 'deactivate').andCallThrough() + }) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-deactivate')) + + runs(() => { + expect(pack.mainModule.deactivate).toHaveBeenCalled() + expect(atom.packages.isPackageActive('package-with-module')).toBeFalsy() + + spyOn(console, 'warn') + }) + + let badPack = null + waitsForPromise(() => atom.packages.activatePackage('package-that-throws-on-activate').then(p => { + badPack = p + })) + + runs(() => { + expect(atom.packages.isPackageActive('package-that-throws-on-activate')).toBeTruthy() + spyOn(badPack.mainModule, 'deactivate').andCallThrough() + }) + + waitsForPromise(() => atom.packages.deactivatePackage('package-that-throws-on-activate')) + + runs(() => { + expect(badPack.mainModule.deactivate).not.toHaveBeenCalled() + expect(atom.packages.isPackageActive('package-that-throws-on-activate')).toBeFalsy() + }) + }) + + it("absorbs exceptions that are thrown by the package module's deactivate method", () => { + spyOn(console, 'error') + let thrownError = null + + waitsForPromise(() => atom.packages.activatePackage('package-that-throws-on-deactivate')) + + waitsForPromise(() => { + try { + return atom.packages.deactivatePackage('package-that-throws-on-deactivate') + } catch (error) { + thrownError = error + } + }) + + runs(() => { + expect(thrownError).toBeNull() + expect(console.error).toHaveBeenCalled() + }) + }) + + it("removes the package's grammars", () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-grammars')) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-grammars')) + + runs(() => { + expect(atom.grammars.selectGrammar('a.alot').name).toBe('Null Grammar') + expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Null Grammar') + }) + }) + + it("removes the package's keymaps", () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-keymaps')) + + runs(() => { + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: createTestElement('test-1')})).toHaveLength(0) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: createTestElement('test-2')})).toHaveLength(0) + }) + }) + + it("removes the package's stylesheets", () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-styles')) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-styles')) + + runs(() => { + const one = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/1.css') + const two = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/2.less') + const three = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/3.css') + expect(atom.themes.stylesheetElementForId(one)).not.toExist() + expect(atom.themes.stylesheetElementForId(two)).not.toExist() + expect(atom.themes.stylesheetElementForId(three)).not.toExist() + }) + }) + + it("removes the package's scoped-properties", () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-settings')) + + runs(() => expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a')) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-settings')) + + runs(() => expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBeUndefined()) + }) + + it('invokes ::onDidDeactivatePackage listeners with the deactivated package', () => { + waitsForPromise(() => atom.packages.activatePackage('package-with-main')) + + let deactivatedPackage + runs(() => { + atom.packages.onDidDeactivatePackage(pack => { + deactivatedPackage = pack + }) + }) + + waitsForPromise(() => atom.packages.deactivatePackage('package-with-main')) + + runs(() => expect(deactivatedPackage.name).toBe('package-with-main')) + }) + }) + + describe('::activate()', () => { + beforeEach(() => { + spyOn(atom, 'inSpecMode').andReturn(false) + jasmine.snapshotDeprecations() + spyOn(console, 'warn') + atom.packages.loadPackages() + + const loadedPackages = atom.packages.getLoadedPackages() + expect(loadedPackages.length).toBeGreaterThan(0) + }) + + afterEach(() => { + waitsForPromise(() => atom.packages.deactivatePackages()) + runs(() => { + atom.packages.unloadPackages() + jasmine.restoreDeprecationsSnapshot() + }) + }) + + it('sets hasActivatedInitialPackages', () => { + spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(null) + spyOn(atom.packages, 'activatePackages') + expect(atom.packages.hasActivatedInitialPackages()).toBe(false) + waitsForPromise(() => atom.packages.activate()) + runs(() => expect(atom.packages.hasActivatedInitialPackages()).toBe(true)) + }) + + it('activates all the packages, and none of the themes', () => { + const packageActivator = spyOn(atom.packages, 'activatePackages') + const themeActivator = spyOn(atom.themes, 'activatePackages') + + atom.packages.activate() + + expect(packageActivator).toHaveBeenCalled() + expect(themeActivator).toHaveBeenCalled() + + const packages = packageActivator.mostRecentCall.args[0] + for (let pack of packages) { expect(['atom', 'textmate']).toContain(pack.getType()) } + + const themes = themeActivator.mostRecentCall.args[0] + themes.map((theme) => expect(['theme']).toContain(theme.getType())) + }) + + it('calls callbacks registered with ::onDidActivateInitialPackages', () => { + const package1 = atom.packages.loadPackage('package-with-main') + const package2 = atom.packages.loadPackage('package-with-index') + const package3 = atom.packages.loadPackage('package-with-activation-commands') + spyOn(atom.packages, 'getLoadedPackages').andReturn([package1, package2, package3]) + spyOn(atom.themes, 'activatePackages') + const activateSpy = jasmine.createSpy('activateSpy') + atom.packages.onDidActivateInitialPackages(activateSpy) + + atom.packages.activate() + waitsFor(() => activateSpy.callCount > 0) + runs(() => { + let needle, needle1, needle2 + jasmine.unspy(atom.packages, 'getLoadedPackages') + expect(atom.packages.getActivePackages().includes(package1)).toBe(true) + expect(atom.packages.getActivePackages().includes(package2)).toBe(true) + expect(atom.packages.getActivePackages().includes(package3)).toBe(false) + }) + }) + }) + + describe('::enablePackage(id) and ::disablePackage(id)', () => { + describe('with packages', () => { + it('enables a disabled package', () => { + const packageName = 'package-with-main' + atom.config.pushAtKeyPath('core.disabledPackages', packageName) + atom.packages.observeDisabledPackages() + expect(atom.config.get('core.disabledPackages')).toContain(packageName) + + const pack = atom.packages.enablePackage(packageName) + const loadedPackages = atom.packages.getLoadedPackages() + let activatedPackages = null + waitsFor(() => { + activatedPackages = atom.packages.getActivePackages() + return activatedPackages.length > 0 + }) + + runs(() => { + expect(loadedPackages).toContain(pack) + expect(activatedPackages).toContain(pack) + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) + }) + }) + + it('disables an enabled package', () => { + const packageName = 'package-with-main' + let pack = null + let activatedPackages = null + + waitsForPromise(() => atom.packages.activatePackage(packageName)) + + runs(() => { + atom.packages.observeDisabledPackages() + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) + + pack = atom.packages.disablePackage(packageName) + }) + + waitsFor(() => { + activatedPackages = atom.packages.getActivePackages() + return activatedPackages.length === 0 + }) + + runs(() => { + expect(activatedPackages).not.toContain(pack) + expect(atom.config.get('core.disabledPackages')).toContain(packageName) + }) + }) + + it('returns null if the package cannot be loaded', () => { + spyOn(console, 'warn') + expect(atom.packages.enablePackage('this-doesnt-exist')).toBeNull() + expect(console.warn.callCount).toBe(1) + }) + + it('does not disable an already disabled package', () => { + const packageName = 'package-with-main' + atom.config.pushAtKeyPath('core.disabledPackages', packageName) + atom.packages.observeDisabledPackages() + expect(atom.config.get('core.disabledPackages')).toContain(packageName) + + atom.packages.disablePackage(packageName) + const packagesDisabled = atom.config.get('core.disabledPackages').filter(pack => pack === packageName) + expect(packagesDisabled.length).toEqual(1) + }) + }) + + describe('with themes', () => { + let didChangeActiveThemesHandler = null + + beforeEach(() => { + waitsForPromise(() => atom.themes.activateThemes()) + }) + + afterEach(() => { + waitsForPromise(() => atom.themes.deactivateThemes()) + }) + + it('enables and disables a theme', () => { + const packageName = 'theme-with-package-file' + + expect(atom.config.get('core.themes')).not.toContain(packageName) + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) + + // enabling of theme + let pack = atom.packages.enablePackage(packageName) + + waitsFor('theme to enable', 500, () => { + return atom.packages.getActivePackages().includes(pack) + }) + + runs(() => { + expect(atom.config.get('core.themes')).toContain(packageName) + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) + + didChangeActiveThemesHandler = jasmine.createSpy('didChangeActiveThemesHandler') + didChangeActiveThemesHandler.reset() + atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler) + + pack = atom.packages.disablePackage(packageName) + }) + + waitsFor('did-change-active-themes event to fire', 500, () => didChangeActiveThemesHandler.callCount === 1) + + runs(() => { + expect(atom.packages.getActivePackages()).not.toContain(pack) + expect(atom.config.get('core.themes')).not.toContain(packageName) + expect(atom.config.get('core.themes')).not.toContain(packageName) + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) + }) + }) + }) + }) +}) From 1d0dfe2213185891795f3f1a7db76f009153292c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2017 17:04:46 -0700 Subject: [PATCH 61/81] Use async/await in package-manager-spec --- spec/package-manager-spec.js | 944 ++++++++++++++--------------------- 1 file changed, 366 insertions(+), 578 deletions(-) diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js index 3cd25383f..9b28ab437 100644 --- a/spec/package-manager-spec.js +++ b/spec/package-manager-spec.js @@ -7,6 +7,7 @@ const {Disposable} = require('atom') const {buildKeydownEvent} = require('../src/keymap-extensions') const {mockLocalStorage} = require('./spec-helper') const ModuleCache = require('../src/module-cache') +const {it, fit, ffit, beforeEach, afterEach} = require('./async-spec-helpers') describe('PackageManager', () => { function createTestElement (className) { @@ -57,9 +58,9 @@ describe('PackageManager', () => { describe('::loadPackages()', () => { beforeEach(() => spyOn(atom.packages, 'loadAvailablePackage')) - afterEach(() => { - waitsForPromise(() => atom.packages.deactivatePackages()) - runs(() => atom.packages.unloadPackages()) + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() }) it('sets hasLoadedInitialPackages', () => { @@ -188,9 +189,9 @@ describe('PackageManager', () => { const model1 = {worksWithViewProvider1: true} const model2 = {worksWithViewProvider2: true} - afterEach(() => { - waitsForPromise(() => atom.packages.deactivatePackage('package-with-view-providers')) - runs(() => atom.packages.unloadPackage('package-with-view-providers')) + afterEach(async () => { + await atom.packages.deactivatePackage('package-with-view-providers') + atom.packages.unloadPackage('package-with-view-providers') }) it('does not load the view providers immediately', () => { @@ -201,20 +202,18 @@ describe('PackageManager', () => { expect(() => atom.views.getView(model2)).toThrow() }) - it('registers the view providers when the package is activated', () => { + it('registers the view providers when the package is activated', async () => { atom.packages.loadPackage('package-with-view-providers') - waitsForPromise(() => - atom.packages.activatePackage('package-with-view-providers').then(() => { - const element1 = atom.views.getView(model1) - expect(element1 instanceof HTMLDivElement).toBe(true) - expect(element1.dataset.createdBy).toBe('view-provider-1') + await atom.packages.activatePackage('package-with-view-providers') - const element2 = atom.views.getView(model2) - expect(element2 instanceof HTMLDivElement).toBe(true) - expect(element2.dataset.createdBy).toBe('view-provider-2') - }) - ) + const element1 = atom.views.getView(model1) + expect(element1 instanceof HTMLDivElement).toBe(true) + expect(element1.dataset.createdBy).toBe('view-provider-1') + + const element2 = atom.views.getView(model2) + expect(element2 instanceof HTMLDivElement).toBe(true) + expect(element2.dataset.createdBy).toBe('view-provider-2') }) it("registers the view providers when any of the package's deserializers are used", () => { @@ -458,20 +457,14 @@ describe('PackageManager', () => { describe('::unloadPackage(name)', () => { describe('when the package is active', () => { - it('throws an error', () => { - let pack + it('throws an error', async () => { + const pack = await atom.packages.activatePackage('package-with-main') + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() - waitsForPromise(() => atom.packages.activatePackage('package-with-main').then(p => { - pack = p - })) - - runs(() => { - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() - expect(() => atom.packages.unloadPackage(pack.name)).toThrow() - expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() - expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() - }) + expect(() => atom.packages.unloadPackage(pack.name)).toThrow() + expect(atom.packages.isPackageLoaded(pack.name)).toBeTruthy() + expect(atom.packages.isPackageActive(pack.name)).toBeTruthy() }) }) @@ -505,63 +498,48 @@ describe('PackageManager', () => { describe('::activatePackage(id)', () => { describe('when called multiple times', () => { - it('it only calls activate on the package once', () => { + it('it only calls activate on the package once', async () => { spyOn(Package.prototype, 'activateNow').andCallThrough() - waitsForPromise(() => atom.packages.activatePackage('package-with-index')) - waitsForPromise(() => atom.packages.activatePackage('package-with-index')) - waitsForPromise(() => atom.packages.activatePackage('package-with-index')) + await atom.packages.activatePackage('package-with-index') + await atom.packages.activatePackage('package-with-index') + await atom.packages.activatePackage('package-with-index') - runs(() => expect(Package.prototype.activateNow.callCount).toBe(1)) + expect(Package.prototype.activateNow.callCount).toBe(1) }) }) describe('when the package has a main module', () => { describe('when the metadata specifies a main module path˜', () => { - it('requires the module at the specified path', () => { + it('requires the module at the specified path', async () => { const mainModule = require('./fixtures/packages/package-with-main/main-module') spyOn(mainModule, 'activate') - let pack - waitsForPromise(() => atom.packages.activatePackage('package-with-main').then(p => { - pack = p - })) - - runs(() => { - expect(mainModule.activate).toHaveBeenCalled() - expect(pack.mainModule).toBe(mainModule) - }) + const pack = await atom.packages.activatePackage('package-with-main') + expect(mainModule.activate).toHaveBeenCalled() + expect(pack.mainModule).toBe(mainModule) }) }) describe('when the metadata does not specify a main module', () => { - it('requires index.coffee', () => { + it('requires index.coffee', async () => { const indexModule = require('./fixtures/packages/package-with-index/index') spyOn(indexModule, 'activate') - let pack - waitsForPromise(() => atom.packages.activatePackage('package-with-index').then(p => { - pack = p - })) - runs(() => { - expect(indexModule.activate).toHaveBeenCalled() - expect(pack.mainModule).toBe(indexModule) - }) + const pack = await atom.packages.activatePackage('package-with-index') + expect(indexModule.activate).toHaveBeenCalled() + expect(pack.mainModule).toBe(indexModule) }) }) - it('assigns config schema, including defaults when package contains a schema', () => { + it('assigns config schema, including defaults when package contains a schema', async () => { expect(atom.config.get('package-with-config-schema.numbers.one')).toBeUndefined() - waitsForPromise(() => atom.packages.activatePackage('package-with-config-schema')) - - runs(() => { - expect(atom.config.get('package-with-config-schema.numbers.one')).toBe(1) - expect(atom.config.get('package-with-config-schema.numbers.two')).toBe(2) - - expect(atom.config.set('package-with-config-schema.numbers.one', 'nope')).toBe(false) - expect(atom.config.set('package-with-config-schema.numbers.one', '10')).toBe(true) - expect(atom.config.get('package-with-config-schema.numbers.one')).toBe(10) - }) + await atom.packages.activatePackage('package-with-config-schema') + expect(atom.config.get('package-with-config-schema.numbers.one')).toBe(1) + expect(atom.config.get('package-with-config-schema.numbers.two')).toBe(2) + expect(atom.config.set('package-with-config-schema.numbers.one', 'nope')).toBe(false) + expect(atom.config.set('package-with-config-schema.numbers.one', '10')).toBe(true) + expect(atom.config.get('package-with-config-schema.numbers.one')).toBe(10) }) describe('when the package metadata includes `activationCommands`', () => { @@ -587,43 +565,42 @@ describe('PackageManager', () => { mainModule = null }) - it('defers requiring/activating the main module until an activation event bubbles to the root view', () => { + it('defers requiring/activating the main module until an activation event bubbles to the root view', async () => { expect(Package.prototype.requireMainModule.callCount).toBe(0) atom.workspace.getElement().dispatchEvent(new CustomEvent('activation-command', {bubbles: true})) - waitsForPromise(() => promise) - - runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(1)) + await promise + expect(Package.prototype.requireMainModule.callCount).toBe(1) }) - it('triggers the activation event on all handlers registered during activation', () => { - waitsForPromise(() => atom.workspace.open()) + it('triggers the activation event on all handlers registered during activation', async () => { + await atom.workspace.open() - runs(() => { - const editorElement = atom.workspace.getActiveTextEditor().getElement() - const editorCommandListener = jasmine.createSpy('editorCommandListener') - atom.commands.add('atom-text-editor', 'activation-command', editorCommandListener) - atom.commands.dispatch(editorElement, 'activation-command') - expect(mainModule.activate.callCount).toBe(1) - expect(mainModule.activationCommandCallCount).toBe(1) - expect(editorCommandListener.callCount).toBe(1) - expect(workspaceCommandListener.callCount).toBe(1) - atom.commands.dispatch(editorElement, 'activation-command') - expect(mainModule.activationCommandCallCount).toBe(2) - expect(editorCommandListener.callCount).toBe(2) - expect(workspaceCommandListener.callCount).toBe(2) - expect(mainModule.activate.callCount).toBe(1) - }) + const editorElement = atom.workspace.getActiveTextEditor().getElement() + const editorCommandListener = jasmine.createSpy('editorCommandListener') + atom.commands.add('atom-text-editor', 'activation-command', editorCommandListener) + + atom.commands.dispatch(editorElement, 'activation-command') + expect(mainModule.activate.callCount).toBe(1) + expect(mainModule.activationCommandCallCount).toBe(1) + expect(editorCommandListener.callCount).toBe(1) + expect(workspaceCommandListener.callCount).toBe(1) + + atom.commands.dispatch(editorElement, 'activation-command') + expect(mainModule.activationCommandCallCount).toBe(2) + expect(editorCommandListener.callCount).toBe(2) + expect(workspaceCommandListener.callCount).toBe(2) + expect(mainModule.activate.callCount).toBe(1) }) - it('activates the package immediately when the events are empty', () => { + it('activates the package immediately when the events are empty', async () => { mainModule = require('./fixtures/packages/package-with-empty-activation-commands/index') spyOn(mainModule, 'activate').andCallThrough() - waitsForPromise(() => atom.packages.activatePackage('package-with-empty-activation-commands')) + atom.packages.activatePackage('package-with-empty-activation-commands') - runs(() => expect(mainModule.activate.callCount).toBe(1)) + expect(mainModule.activate.callCount).toBe(1) }) it('adds a notification when the activation commands are invalid', () => { @@ -646,34 +623,38 @@ describe('PackageManager', () => { expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-context-menu') }) - it('adds a notification when the grammar is invalid', () => { - const addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) + it('adds a notification when the grammar is invalid', async () => { + let notificationEvent - expect(() => atom.packages.activatePackage('package-with-invalid-grammar')).not.toThrow() + await new Promise(resolve => { + const subscription = atom.notifications.onDidAddNotification(event => { + notificationEvent = event + subscription.dispose() + resolve() + }) - waitsFor(() => addErrorHandler.callCount > 0) - - runs(() => { - expect(addErrorHandler.callCount).toBe(1) - expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to load a package-with-invalid-grammar package grammar') - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-grammar') + atom.packages.activatePackage('package-with-invalid-grammar') }) + + expect(notificationEvent.message).toContain('Failed to load a package-with-invalid-grammar package grammar') + expect(notificationEvent.options.packageName).toEqual('package-with-invalid-grammar') }) - it('adds a notification when the settings are invalid', () => { - const addErrorHandler = jasmine.createSpy() - atom.notifications.onDidAddNotification(addErrorHandler) + it('adds a notification when the settings are invalid', async () => { + let notificationEvent - expect(() => atom.packages.activatePackage('package-with-invalid-settings')).not.toThrow() + await new Promise(resolve => { + const subscription = atom.notifications.onDidAddNotification(event => { + notificationEvent = event + subscription.dispose() + resolve() + }) - waitsFor(() => addErrorHandler.callCount > 0) - - runs(() => { - expect(addErrorHandler.callCount).toBe(1) - expect(addErrorHandler.argsForCall[0][0].message).toContain('Failed to load the package-with-invalid-settings package settings') - expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-invalid-settings') + atom.packages.activatePackage('package-with-invalid-settings') }) + + expect(notificationEvent.message).toContain('Failed to load the package-with-invalid-settings package settings') + expect(notificationEvent.options.packageName).toEqual('package-with-invalid-settings') }) }) }) @@ -687,62 +668,53 @@ describe('PackageManager', () => { spyOn(Package.prototype, 'requireMainModule').andCallThrough() }) - it('defers requiring/activating the main module until an triggering of an activation hook occurs', () => { + it('defers requiring/activating the main module until an triggering of an activation hook occurs', async () => { promise = atom.packages.activatePackage('package-with-activation-hooks') expect(Package.prototype.requireMainModule.callCount).toBe(0) atom.packages.triggerActivationHook('language-fictitious:grammar-used') atom.packages.triggerDeferredActivationHooks() - waitsForPromise(() => promise) - - runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(1)) + await promise + expect(Package.prototype.requireMainModule.callCount).toBe(1) }) - it('does not double register activation hooks when deactivating and reactivating', () => { + it('does not double register activation hooks when deactivating and reactivating', async () => { promise = atom.packages.activatePackage('package-with-activation-hooks') expect(mainModule.activate.callCount).toBe(0) atom.packages.triggerActivationHook('language-fictitious:grammar-used') atom.packages.triggerDeferredActivationHooks() - waitsForPromise(() => promise) + await promise + expect(mainModule.activate.callCount).toBe(1) - runs(() => expect(mainModule.activate.callCount).toBe(1)) + await atom.packages.deactivatePackage('package-with-activation-hooks') - waitsForPromise(() => atom.packages.deactivatePackage('package-with-activation-hooks')) + promise = atom.packages.activatePackage('package-with-activation-hooks') + atom.packages.triggerActivationHook('language-fictitious:grammar-used') + atom.packages.triggerDeferredActivationHooks() - runs(() => { - promise = atom.packages.activatePackage('package-with-activation-hooks') - atom.packages.triggerActivationHook('language-fictitious:grammar-used') - atom.packages.triggerDeferredActivationHooks() - }) - - waitsForPromise(() => promise) - - runs(() => expect(mainModule.activate.callCount).toBe(2)) + await promise + expect(mainModule.activate.callCount).toBe(2) }) - it('activates the package immediately when activationHooks is empty', () => { + it('activates the package immediately when activationHooks is empty', async () => { mainModule = require('./fixtures/packages/package-with-empty-activation-hooks/index') spyOn(mainModule, 'activate').andCallThrough() - runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(0)) + expect(Package.prototype.requireMainModule.callCount).toBe(0) - waitsForPromise(() => atom.packages.activatePackage('package-with-empty-activation-hooks')) - - runs(() => { - expect(mainModule.activate.callCount).toBe(1) - expect(Package.prototype.requireMainModule.callCount).toBe(1) - }) + await atom.packages.activatePackage('package-with-empty-activation-hooks') + expect(mainModule.activate.callCount).toBe(1) + expect(Package.prototype.requireMainModule.callCount).toBe(1) }) - it('activates the package immediately if the activation hook had already been triggered', () => { + it('activates the package immediately if the activation hook had already been triggered', async () => { atom.packages.triggerActivationHook('language-fictitious:grammar-used') atom.packages.triggerDeferredActivationHooks() expect(Package.prototype.requireMainModule.callCount).toBe(0) - waitsForPromise(() => atom.packages.activatePackage('package-with-activation-hooks')) - - runs(() => expect(Package.prototype.requireMainModule.callCount).toBe(1)) + await atom.packages.activatePackage('package-with-activation-hooks') + expect(Package.prototype.requireMainModule.callCount).toBe(1) }) }) @@ -757,42 +729,33 @@ describe('PackageManager', () => { }) describe('when the package does not export an activate function', () => { - it('activates the package and does not throw an exception or log a warning', () => { + it('activates the package and does not throw an exception or log a warning', async () => { spyOn(console, 'warn') - expect(() => atom.packages.activatePackage('package-with-no-activate')).not.toThrow() - - waitsFor(() => atom.packages.isPackageActive('package-with-no-activate')) - - runs(() => expect(console.warn).not.toHaveBeenCalled()) + await atom.packages.activatePackage('package-with-no-activate') + expect(console.warn).not.toHaveBeenCalled() }) }) - it("passes the activate method the package's previously serialized state if it exists", () => { - let pack - waitsForPromise(() => atom.packages.activatePackage('package-with-serialization').then(p => { - pack = p - })) - runs(() => { - expect(pack.mainModule.someNumber).not.toBe(77) - pack.mainModule.someNumber = 77 - atom.packages.serializePackage('package-with-serialization') - }) - waitsForPromise(() => atom.packages.deactivatePackage('package-with-serialization')) - runs(() => spyOn(pack.mainModule, 'activate').andCallThrough()) - waitsForPromise(() => atom.packages.activatePackage('package-with-serialization')) - runs(() => expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77})) + it("passes the activate method the package's previously serialized state if it exists", async () => { + const pack = await atom.packages.activatePackage('package-with-serialization') + expect(pack.mainModule.someNumber).not.toBe(77) + pack.mainModule.someNumber = 77 + atom.packages.serializePackage('package-with-serialization') + await atom.packages.deactivatePackage('package-with-serialization') + + spyOn(pack.mainModule, 'activate').andCallThrough() + await atom.packages.activatePackage('package-with-serialization') + expect(pack.mainModule.activate).toHaveBeenCalledWith({someNumber: 77}) }) - it('invokes ::onDidActivatePackage listeners with the activated package', () => { + it('invokes ::onDidActivatePackage listeners with the activated package', async () => { let activatedPackage atom.packages.onDidActivatePackage(pack => { activatedPackage = pack }) - atom.packages.activatePackage('package-with-main') - - waitsFor(() => activatedPackage) - runs(() => expect(activatedPackage.name).toBe('package-with-main')) + await atom.packages.activatePackage('package-with-main') + expect(activatedPackage.name).toBe('package-with-main') }) describe("when the package's main module throws an error on load", () => { @@ -814,82 +777,65 @@ describe('PackageManager', () => { }) describe('when the package is not found', () => { - it('rejects the promise', () => { + it('rejects the promise', async () => { + spyOn(console, 'warn') atom.config.set('core.disabledPackages', []) - const onSuccess = jasmine.createSpy('onSuccess') - const onFailure = jasmine.createSpy('onFailure') - spyOn(console, 'warn') - - atom.packages.activatePackage('this-doesnt-exist').then(onSuccess, onFailure) - - waitsFor('promise to be rejected', () => onFailure.callCount > 0) - - runs(() => { + try { + await atom.packages.activatePackage('this-doesnt-exist') + expect('Error to be thrown').toBe('') + } catch (error) { expect(console.warn.callCount).toBe(1) - expect(onFailure.mostRecentCall.args[0] instanceof Error).toBe(true) - expect(onFailure.mostRecentCall.args[0].message).toContain("Failed to load package 'this-doesnt-exist'") - }) + expect(error.message).toContain("Failed to load package 'this-doesnt-exist'") + } }) }) describe('keymap loading', () => { describe("when the metadata does not contain a 'keymaps' manifest", () => { - it('loads all the .cson/.json files in the keymaps directory', () => { + it('loads all the .cson/.json files in the keymaps directory', async () => { const element1 = createTestElement('test-1') const element2 = createTestElement('test-2') const element3 = createTestElement('test-3') - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element2})).toHaveLength(0) expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element3})).toHaveLength(0) - waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) - - runs(() => { - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('test-1') - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element2})[0].command).toBe('test-2') - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element3})).toHaveLength(0) - }) + await atom.packages.activatePackage('package-with-keymaps') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('test-1') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element2})[0].command).toBe('test-2') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element3})).toHaveLength(0) }) }) describe("when the metadata contains a 'keymaps' manifest", () => { - it('loads only the keymaps specified by the manifest, in the specified order', () => { + it('loads only the keymaps specified by the manifest, in the specified order', async () => { const element1 = createTestElement('test-1') const element3 = createTestElement('test-3') - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) - waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps-manifest')) - - runs(() => { - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('keymap-1') - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-n', target: element1})[0].command).toBe('keymap-2') - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-y', target: element3})).toHaveLength(0) - }) + await atom.packages.activatePackage('package-with-keymaps-manifest') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('keymap-1') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-n', target: element1})[0].command).toBe('keymap-2') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-y', target: element3})).toHaveLength(0) }) }) describe('when the keymap file is empty', () => { - it('does not throw an error on activation', () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-empty-keymap')) - - runs(() => expect(atom.packages.isPackageActive('package-with-empty-keymap')).toBe(true)) + it('does not throw an error on activation', async () => { + await atom.packages.activatePackage('package-with-empty-keymap') + expect(atom.packages.isPackageActive('package-with-empty-keymap')).toBe(true) }) }) describe("when the package's keymaps have been disabled", () => { - it('does not add the keymaps', () => { + it('does not add the keymaps', async () => { const element1 = createTestElement('test-1') - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) atom.config.set('core.packagesWithKeymapsDisabled', ['package-with-keymaps-manifest']) - - waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps-manifest')) - - runs(() => expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0)) + await atom.packages.activatePackage('package-with-keymaps-manifest') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) }) }) @@ -903,19 +849,17 @@ describe('PackageManager', () => { }) describe("when the package's keymaps are disabled and re-enabled after it is activated", () => { - it('removes and re-adds the keymaps', () => { + it('removes and re-adds the keymaps', async () => { const element1 = createTestElement('test-1') atom.packages.observePackagesWithKeymapsDisabled() - waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps-manifest')) + await atom.packages.activatePackage('package-with-keymaps-manifest') - runs(() => { - atom.config.set('core.packagesWithKeymapsDisabled', ['package-with-keymaps-manifest']) - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) + atom.config.set('core.packagesWithKeymapsDisabled', ['package-with-keymaps-manifest']) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})).toHaveLength(0) - atom.config.set('core.packagesWithKeymapsDisabled', []) - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('keymap-1') - }) + atom.config.set('core.packagesWithKeymapsDisabled', []) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: element1})[0].command).toBe('keymap-1') }) }) @@ -944,28 +888,20 @@ describe('PackageManager', () => { temp.cleanupSync() }) - it("doesn't override user-defined keymaps", () => { + it("doesn't override user-defined keymaps", async () => { fs.writeFileSync(userKeymapPath, `".test-1": {"ctrl-z": "user-command"}`) atom.keymaps.loadUserKeymap() - waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) + await atom.packages.activatePackage('package-with-keymaps') + atom.keymaps.handleKeyboardEvent(buildKeydownEvent('z', {ctrl: true, target: element})) + expect(events.length).toBe(1) + expect(events[0].type).toBe('user-command') - runs(() => { - atom.keymaps.handleKeyboardEvent(buildKeydownEvent('z', {ctrl: true, target: element})) - - expect(events.length).toBe(1) - expect(events[0].type).toBe('user-command') - }) - - waitsForPromise(() => atom.packages.deactivatePackage('package-with-keymaps')) - - waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) - - runs(() => { - atom.keymaps.handleKeyboardEvent(buildKeydownEvent('z', {ctrl: true, target: element})) - expect(events.length).toBe(2) - expect(events[1].type).toBe('user-command') - }) + await atom.packages.deactivatePackage('package-with-keymaps') + await atom.packages.activatePackage('package-with-keymaps') + atom.keymaps.handleKeyboardEvent(buildKeydownEvent('z', {ctrl: true, target: element})) + expect(events.length).toBe(2) + expect(events[1].type).toBe('user-command') }) }) }) @@ -977,53 +913,45 @@ describe('PackageManager', () => { }) describe("when the metadata does not contain a 'menus' manifest", () => { - it('loads all the .cson/.json files in the menus directory', () => { + it('loads all the .cson/.json files in the menus directory', async () => { const element = createTestElement('test-1') - expect(atom.contextMenu.templateForElement(element)).toEqual([]) - waitsForPromise(() => atom.packages.activatePackage('package-with-menus')) - - runs(() => { - expect(atom.menu.template.length).toBe(2) - expect(atom.menu.template[0].label).toBe('Second to Last') - expect(atom.menu.template[1].label).toBe('Last') - expect(atom.contextMenu.templateForElement(element)[0].label).toBe('Menu item 1') - expect(atom.contextMenu.templateForElement(element)[1].label).toBe('Menu item 2') - expect(atom.contextMenu.templateForElement(element)[2].label).toBe('Menu item 3') - }) + await atom.packages.activatePackage('package-with-menus') + expect(atom.menu.template.length).toBe(2) + expect(atom.menu.template[0].label).toBe('Second to Last') + expect(atom.menu.template[1].label).toBe('Last') + expect(atom.contextMenu.templateForElement(element)[0].label).toBe('Menu item 1') + expect(atom.contextMenu.templateForElement(element)[1].label).toBe('Menu item 2') + expect(atom.contextMenu.templateForElement(element)[2].label).toBe('Menu item 3') }) }) describe("when the metadata contains a 'menus' manifest", () => { - it('loads only the menus specified by the manifest, in the specified order', () => { + it('loads only the menus specified by the manifest, in the specified order', async () => { const element = createTestElement('test-1') - expect(atom.contextMenu.templateForElement(element)).toEqual([]) - waitsForPromise(() => atom.packages.activatePackage('package-with-menus-manifest')) - - runs(() => { - expect(atom.menu.template[0].label).toBe('Second to Last') - expect(atom.menu.template[1].label).toBe('Last') - expect(atom.contextMenu.templateForElement(element)[0].label).toBe('Menu item 2') - expect(atom.contextMenu.templateForElement(element)[1].label).toBe('Menu item 1') - expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined() - }) + await atom.packages.activatePackage('package-with-menus-manifest') + expect(atom.menu.template[0].label).toBe('Second to Last') + expect(atom.menu.template[1].label).toBe('Last') + expect(atom.contextMenu.templateForElement(element)[0].label).toBe('Menu item 2') + expect(atom.contextMenu.templateForElement(element)[1].label).toBe('Menu item 1') + expect(atom.contextMenu.templateForElement(element)[2]).toBeUndefined() }) }) describe('when the menu file is empty', () => { - it('does not throw an error on activation', () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-empty-menu')) - runs(() => expect(atom.packages.isPackageActive('package-with-empty-menu')).toBe(true)) + it('does not throw an error on activation', async () => { + await atom.packages.activatePackage('package-with-empty-menu') + expect(atom.packages.isPackageActive('package-with-empty-menu')).toBe(true) }) }) }) describe('stylesheet loading', () => { describe("when the metadata contains a 'styleSheets' manifest", () => { - it('loads style sheets from the styles directory as specified by the manifest', () => { + it('loads style sheets from the styles directory as specified by the manifest', async () => { const one = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/1.css') const two = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/2.less') const three = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/3.css') @@ -1032,20 +960,16 @@ describe('PackageManager', () => { expect(atom.themes.stylesheetElementForId(two)).toBeNull() expect(atom.themes.stylesheetElementForId(three)).toBeNull() - waitsForPromise(() => atom.packages.activatePackage('package-with-style-sheets-manifest')) - - runs(() => { - expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(three)).toBeNull() - - expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe('1px') - }) + await atom.packages.activatePackage('package-with-style-sheets-manifest') + expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(three)).toBeNull() + expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe('1px') }) }) describe("when the metadata does not contain a 'styleSheets' manifest", () => { - it('loads all style sheets from the styles directory', () => { + it('loads all style sheets from the styles directory', async () => { const one = require.resolve('./fixtures/packages/package-with-styles/styles/1.css') const two = require.resolve('./fixtures/packages/package-with-styles/styles/2.less') const three = require.resolve('./fixtures/packages/package-with-styles/styles/3.test-context.css') @@ -1056,73 +980,64 @@ describe('PackageManager', () => { expect(atom.themes.stylesheetElementForId(three)).toBeNull() expect(atom.themes.stylesheetElementForId(four)).toBeNull() - waitsForPromise(() => atom.packages.activatePackage('package-with-styles')) - - runs(() => { - expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(three)).not.toBeNull() - expect(atom.themes.stylesheetElementForId(four)).not.toBeNull() - expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe('3px') - }) + await atom.packages.activatePackage('package-with-styles') + expect(atom.themes.stylesheetElementForId(one)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(two)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(three)).not.toBeNull() + expect(atom.themes.stylesheetElementForId(four)).not.toBeNull() + expect(getComputedStyle(document.querySelector('#jasmine-content')).fontSize).toBe('3px') }) }) - it("assigns the stylesheet's context based on the filename", () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-styles')) + it("assigns the stylesheet's context based on the filename", async () => { + await atom.packages.activatePackage('package-with-styles') - runs(() => { - let count = 0 - - for (let styleElement of atom.styles.getStyleElements()) { - if (styleElement.sourcePath.match(/1.css/)) { - expect(styleElement.context).toBe(undefined) - count++ - } - - if (styleElement.sourcePath.match(/2.less/)) { - expect(styleElement.context).toBe(undefined) - count++ - } - - if (styleElement.sourcePath.match(/3.test-context.css/)) { - expect(styleElement.context).toBe('test-context') - count++ - } - - if (styleElement.sourcePath.match(/4.css/)) { - expect(styleElement.context).toBe(undefined) - count++ - } + let count = 0 + for (let styleElement of atom.styles.getStyleElements()) { + if (styleElement.sourcePath.match(/1.css/)) { + expect(styleElement.context).toBe(undefined) + count++ } - expect(count).toBe(4) - }) + if (styleElement.sourcePath.match(/2.less/)) { + expect(styleElement.context).toBe(undefined) + count++ + } + + if (styleElement.sourcePath.match(/3.test-context.css/)) { + expect(styleElement.context).toBe('test-context') + count++ + } + + if (styleElement.sourcePath.match(/4.css/)) { + expect(styleElement.context).toBe(undefined) + count++ + } + } + + expect(count).toBe(4) }) }) describe('grammar loading', () => { - it("loads the package's grammars", () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-grammars')) - - runs(() => { - expect(atom.grammars.selectGrammar('a.alot').name).toBe('Alot') - expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Alittle') - }) + it("loads the package's grammars", async () => { + await atom.packages.activatePackage('package-with-grammars') + expect(atom.grammars.selectGrammar('a.alot').name).toBe('Alot') + expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Alittle') }) }) describe('scoped-property loading', () => { - it('loads the scoped properties', () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-settings')) - - runs(() => expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a')) + it('loads the scoped properties', async () => { + await atom.packages.activatePackage('package-with-settings') + expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a') }) }) describe('service registration', () => { - it("registers the package's provided and consumed services", () => { + it("registers the package's provided and consumed services", async () => { const consumerModule = require('./fixtures/packages/package-with-consumed-services') + let firstServiceV3Disposed = false let firstServiceV4Disposed = false let secondServiceDisposed = false @@ -1130,241 +1045,154 @@ describe('PackageManager', () => { spyOn(consumerModule, 'consumeFirstServiceV4').andReturn(new Disposable(() => { firstServiceV4Disposed = true })) spyOn(consumerModule, 'consumeSecondService').andReturn(new Disposable(() => { secondServiceDisposed = true })) - waitsForPromise(() => atom.packages.activatePackage('package-with-consumed-services')) + await atom.packages.activatePackage('package-with-consumed-services') + await atom.packages.activatePackage('package-with-provided-services') + expect(consumerModule.consumeFirstServiceV3.callCount).toBe(1) + expect(consumerModule.consumeFirstServiceV3).toHaveBeenCalledWith('first-service-v3') + expect(consumerModule.consumeFirstServiceV4).toHaveBeenCalledWith('first-service-v4') + expect(consumerModule.consumeSecondService).toHaveBeenCalledWith('second-service') - waitsForPromise(() => atom.packages.activatePackage('package-with-provided-services')) + consumerModule.consumeFirstServiceV3.reset() + consumerModule.consumeFirstServiceV4.reset() + consumerModule.consumeSecondService.reset() - runs(() => { - expect(consumerModule.consumeFirstServiceV3.callCount).toBe(1) - expect(consumerModule.consumeFirstServiceV3).toHaveBeenCalledWith('first-service-v3') - expect(consumerModule.consumeFirstServiceV4).toHaveBeenCalledWith('first-service-v4') - expect(consumerModule.consumeSecondService).toHaveBeenCalledWith('second-service') + await atom.packages.deactivatePackage('package-with-provided-services') + expect(firstServiceV3Disposed).toBe(true) + expect(firstServiceV4Disposed).toBe(true) + expect(secondServiceDisposed).toBe(true) - consumerModule.consumeFirstServiceV3.reset() - consumerModule.consumeFirstServiceV4.reset() - consumerModule.consumeSecondService.reset() - }) - - waitsForPromise(() => atom.packages.deactivatePackage('package-with-provided-services')) - - runs(() => { - expect(firstServiceV3Disposed).toBe(true) - expect(firstServiceV4Disposed).toBe(true) - expect(secondServiceDisposed).toBe(true) - }) - - waitsForPromise(() => atom.packages.deactivatePackage('package-with-consumed-services')) - - waitsForPromise(() => atom.packages.activatePackage('package-with-provided-services')) - - runs(() => { - expect(consumerModule.consumeFirstServiceV3).not.toHaveBeenCalled() - expect(consumerModule.consumeFirstServiceV4).not.toHaveBeenCalled() - expect(consumerModule.consumeSecondService).not.toHaveBeenCalled() - }) + await atom.packages.deactivatePackage('package-with-consumed-services') + await atom.packages.activatePackage('package-with-provided-services') + expect(consumerModule.consumeFirstServiceV3).not.toHaveBeenCalled() + expect(consumerModule.consumeFirstServiceV4).not.toHaveBeenCalled() + expect(consumerModule.consumeSecondService).not.toHaveBeenCalled() }) - it('ignores provided and consumed services that do not exist', () => { + it('ignores provided and consumed services that do not exist', async () => { const addErrorHandler = jasmine.createSpy() atom.notifications.onDidAddNotification(addErrorHandler) - waitsForPromise(() => atom.packages.activatePackage('package-with-missing-consumed-services')) - - waitsForPromise(() => atom.packages.activatePackage('package-with-missing-provided-services')) - - runs(() => { - expect(atom.packages.isPackageActive('package-with-missing-consumed-services')).toBe(true) - expect(atom.packages.isPackageActive('package-with-missing-provided-services')).toBe(true) - expect(addErrorHandler.callCount).toBe(0) - }) + await atom.packages.activatePackage('package-with-missing-consumed-services') + await atom.packages.activatePackage('package-with-missing-provided-services') + expect(atom.packages.isPackageActive('package-with-missing-consumed-services')).toBe(true) + expect(atom.packages.isPackageActive('package-with-missing-provided-services')).toBe(true) + expect(addErrorHandler.callCount).toBe(0) }) }) }) describe('::serialize', () => { - it('does not serialize packages that threw an error during activation', () => { + it('does not serialize packages that threw an error during activation', async () => { spyOn(atom, 'inSpecMode').andReturn(false) spyOn(console, 'warn') - let badPack - waitsForPromise(() => atom.packages.activatePackage('package-that-throws-on-activate').then(p => { - badPack = p - })) + const badPack = await atom.packages.activatePackage('package-that-throws-on-activate') + spyOn(badPack.mainModule, 'serialize').andCallThrough() - runs(() => { - spyOn(badPack.mainModule, 'serialize').andCallThrough() - - atom.packages.serialize() - expect(badPack.mainModule.serialize).not.toHaveBeenCalled() - }) + atom.packages.serialize() + expect(badPack.mainModule.serialize).not.toHaveBeenCalled() }) - it("absorbs exceptions that are thrown by the package module's serialize method", () => { + it("absorbs exceptions that are thrown by the package module's serialize method", async () => { spyOn(console, 'error') - waitsForPromise(() => atom.packages.activatePackage('package-with-serialize-error')) - - waitsForPromise(() => atom.packages.activatePackage('package-with-serialization')) - - runs(() => { - atom.packages.serialize() - expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined() - expect(atom.packages.packageStates['package-with-serialization']).toEqual({someNumber: 1}) - expect(console.error).toHaveBeenCalled() - }) + await atom.packages.activatePackage('package-with-serialize-error') + await atom.packages.activatePackage('package-with-serialization') + atom.packages.serialize() + expect(atom.packages.packageStates['package-with-serialize-error']).toBeUndefined() + expect(atom.packages.packageStates['package-with-serialization']).toEqual({someNumber: 1}) + expect(console.error).toHaveBeenCalled() }) }) describe('::deactivatePackages()', () => { - it('deactivates all packages but does not serialize them', () => { - let pack1, pack2 + it('deactivates all packages but does not serialize them', async () => { + const pack1 = await atom.packages.activatePackage('package-with-deactivate') + const pack2 = await atom.packages.activatePackage('package-with-serialization') - waitsForPromise(() => { - atom.packages.activatePackage('package-with-deactivate').then(p => { - pack1 = p - }) - return atom.packages.activatePackage('package-with-serialization').then(p => { - pack2 = p - }) - }) - - runs(() => { - spyOn(pack1.mainModule, 'deactivate') - spyOn(pack2.mainModule, 'serialize') - }) - - waitsForPromise(() => atom.packages.deactivatePackages()) - - runs(() => { - expect(pack1.mainModule.deactivate).toHaveBeenCalled() - expect(pack2.mainModule.serialize).not.toHaveBeenCalled() - }) + spyOn(pack1.mainModule, 'deactivate') + spyOn(pack2.mainModule, 'serialize') + await atom.packages.deactivatePackages() + expect(pack1.mainModule.deactivate).toHaveBeenCalled() + expect(pack2.mainModule.serialize).not.toHaveBeenCalled() }) }) describe('::deactivatePackage(id)', () => { afterEach(() => atom.packages.unloadPackages()) - it("calls `deactivate` on the package's main module if activate was successful", () => { + it("calls `deactivate` on the package's main module if activate was successful", async () => { spyOn(atom, 'inSpecMode').andReturn(false) - let pack - waitsForPromise(() => atom.packages.activatePackage('package-with-deactivate').then(p => { - pack = p - })) + const pack = await atom.packages.activatePackage('package-with-deactivate') + expect(atom.packages.isPackageActive('package-with-deactivate')).toBeTruthy() + spyOn(pack.mainModule, 'deactivate').andCallThrough() - runs(() => { - expect(atom.packages.isPackageActive('package-with-deactivate')).toBeTruthy() - spyOn(pack.mainModule, 'deactivate').andCallThrough() - }) + await atom.packages.deactivatePackage('package-with-deactivate') + expect(pack.mainModule.deactivate).toHaveBeenCalled() + expect(atom.packages.isPackageActive('package-with-module')).toBeFalsy() - waitsForPromise(() => atom.packages.deactivatePackage('package-with-deactivate')) + spyOn(console, 'warn') + const badPack = await atom.packages.activatePackage('package-that-throws-on-activate') + expect(atom.packages.isPackageActive('package-that-throws-on-activate')).toBeTruthy() + spyOn(badPack.mainModule, 'deactivate').andCallThrough() - runs(() => { - expect(pack.mainModule.deactivate).toHaveBeenCalled() - expect(atom.packages.isPackageActive('package-with-module')).toBeFalsy() - - spyOn(console, 'warn') - }) - - let badPack = null - waitsForPromise(() => atom.packages.activatePackage('package-that-throws-on-activate').then(p => { - badPack = p - })) - - runs(() => { - expect(atom.packages.isPackageActive('package-that-throws-on-activate')).toBeTruthy() - spyOn(badPack.mainModule, 'deactivate').andCallThrough() - }) - - waitsForPromise(() => atom.packages.deactivatePackage('package-that-throws-on-activate')) - - runs(() => { - expect(badPack.mainModule.deactivate).not.toHaveBeenCalled() - expect(atom.packages.isPackageActive('package-that-throws-on-activate')).toBeFalsy() - }) + await atom.packages.deactivatePackage('package-that-throws-on-activate') + expect(badPack.mainModule.deactivate).not.toHaveBeenCalled() + expect(atom.packages.isPackageActive('package-that-throws-on-activate')).toBeFalsy() }) - it("absorbs exceptions that are thrown by the package module's deactivate method", () => { + it("absorbs exceptions that are thrown by the package module's deactivate method", async () => { spyOn(console, 'error') - let thrownError = null - - waitsForPromise(() => atom.packages.activatePackage('package-that-throws-on-deactivate')) - - waitsForPromise(() => { - try { - return atom.packages.deactivatePackage('package-that-throws-on-deactivate') - } catch (error) { - thrownError = error - } - }) - - runs(() => { - expect(thrownError).toBeNull() - expect(console.error).toHaveBeenCalled() - }) + await atom.packages.activatePackage('package-that-throws-on-deactivate') + await atom.packages.deactivatePackage('package-that-throws-on-deactivate') + expect(console.error).toHaveBeenCalled() }) - it("removes the package's grammars", () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-grammars')) - - waitsForPromise(() => atom.packages.deactivatePackage('package-with-grammars')) - - runs(() => { - expect(atom.grammars.selectGrammar('a.alot').name).toBe('Null Grammar') - expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Null Grammar') - }) + it("removes the package's grammars", async () => { + await atom.packages.activatePackage('package-with-grammars') + await atom.packages.deactivatePackage('package-with-grammars') + expect(atom.grammars.selectGrammar('a.alot').name).toBe('Null Grammar') + expect(atom.grammars.selectGrammar('a.alittle').name).toBe('Null Grammar') }) - it("removes the package's keymaps", () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-keymaps')) - - waitsForPromise(() => atom.packages.deactivatePackage('package-with-keymaps')) - - runs(() => { - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: createTestElement('test-1')})).toHaveLength(0) - expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: createTestElement('test-2')})).toHaveLength(0) - }) + it("removes the package's keymaps", async () => { + await atom.packages.activatePackage('package-with-keymaps') + await atom.packages.deactivatePackage('package-with-keymaps') + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: createTestElement('test-1')})).toHaveLength(0) + expect(atom.keymaps.findKeyBindings({keystrokes: 'ctrl-z', target: createTestElement('test-2')})).toHaveLength(0) }) - it("removes the package's stylesheets", () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-styles')) + it("removes the package's stylesheets", async () => { + await atom.packages.activatePackage('package-with-styles') + await atom.packages.deactivatePackage('package-with-styles') - waitsForPromise(() => atom.packages.deactivatePackage('package-with-styles')) - - runs(() => { - const one = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/1.css') - const two = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/2.less') - const three = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/3.css') - expect(atom.themes.stylesheetElementForId(one)).not.toExist() - expect(atom.themes.stylesheetElementForId(two)).not.toExist() - expect(atom.themes.stylesheetElementForId(three)).not.toExist() - }) + const one = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/1.css') + const two = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/2.less') + const three = require.resolve('./fixtures/packages/package-with-style-sheets-manifest/styles/3.css') + expect(atom.themes.stylesheetElementForId(one)).not.toExist() + expect(atom.themes.stylesheetElementForId(two)).not.toExist() + expect(atom.themes.stylesheetElementForId(three)).not.toExist() }) - it("removes the package's scoped-properties", () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-settings')) + it("removes the package's scoped-properties", async () => { + await atom.packages.activatePackage('package-with-settings') + expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a') - runs(() => expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBe('^a')) - - waitsForPromise(() => atom.packages.deactivatePackage('package-with-settings')) - - runs(() => expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBeUndefined()) + await atom.packages.deactivatePackage('package-with-settings') + expect(atom.config.get('editor.increaseIndentPattern', {scope: ['.source.omg']})).toBeUndefined() }) - it('invokes ::onDidDeactivatePackage listeners with the deactivated package', () => { - waitsForPromise(() => atom.packages.activatePackage('package-with-main')) + it('invokes ::onDidDeactivatePackage listeners with the deactivated package', async () => { + await atom.packages.activatePackage('package-with-main') let deactivatedPackage - runs(() => { - atom.packages.onDidDeactivatePackage(pack => { - deactivatedPackage = pack - }) + atom.packages.onDidDeactivatePackage(pack => { + deactivatedPackage = pack }) - waitsForPromise(() => atom.packages.deactivatePackage('package-with-main')) - - runs(() => expect(deactivatedPackage.name).toBe('package-with-main')) + await atom.packages.deactivatePackage('package-with-main') + expect(deactivatedPackage.name).toBe('package-with-main') }) }) @@ -1379,20 +1207,19 @@ describe('PackageManager', () => { expect(loadedPackages.length).toBeGreaterThan(0) }) - afterEach(() => { - waitsForPromise(() => atom.packages.deactivatePackages()) - runs(() => { - atom.packages.unloadPackages() - jasmine.restoreDeprecationsSnapshot() - }) + afterEach(async () => { + await atom.packages.deactivatePackages() + atom.packages.unloadPackages() + jasmine.restoreDeprecationsSnapshot() }) - it('sets hasActivatedInitialPackages', () => { + it('sets hasActivatedInitialPackages', async () => { spyOn(atom.styles, 'getUserStyleSheetPath').andReturn(null) spyOn(atom.packages, 'activatePackages') expect(atom.packages.hasActivatedInitialPackages()).toBe(false) - waitsForPromise(() => atom.packages.activate()) - runs(() => expect(atom.packages.hasActivatedInitialPackages()).toBe(true)) + + await atom.packages.activate() + expect(atom.packages.hasActivatedInitialPackages()).toBe(true) }) it('activates all the packages, and none of the themes', () => { @@ -1411,73 +1238,52 @@ describe('PackageManager', () => { themes.map((theme) => expect(['theme']).toContain(theme.getType())) }) - it('calls callbacks registered with ::onDidActivateInitialPackages', () => { + it('calls callbacks registered with ::onDidActivateInitialPackages', async () => { const package1 = atom.packages.loadPackage('package-with-main') const package2 = atom.packages.loadPackage('package-with-index') const package3 = atom.packages.loadPackage('package-with-activation-commands') spyOn(atom.packages, 'getLoadedPackages').andReturn([package1, package2, package3]) spyOn(atom.themes, 'activatePackages') - const activateSpy = jasmine.createSpy('activateSpy') - atom.packages.onDidActivateInitialPackages(activateSpy) atom.packages.activate() - waitsFor(() => activateSpy.callCount > 0) - runs(() => { - let needle, needle1, needle2 - jasmine.unspy(atom.packages, 'getLoadedPackages') - expect(atom.packages.getActivePackages().includes(package1)).toBe(true) - expect(atom.packages.getActivePackages().includes(package2)).toBe(true) - expect(atom.packages.getActivePackages().includes(package3)).toBe(false) - }) + await new Promise(resolve => atom.packages.onDidActivateInitialPackages(resolve)) + + jasmine.unspy(atom.packages, 'getLoadedPackages') + expect(atom.packages.getActivePackages().includes(package1)).toBe(true) + expect(atom.packages.getActivePackages().includes(package2)).toBe(true) + expect(atom.packages.getActivePackages().includes(package3)).toBe(false) }) }) describe('::enablePackage(id) and ::disablePackage(id)', () => { describe('with packages', () => { - it('enables a disabled package', () => { + it('enables a disabled package', async () => { const packageName = 'package-with-main' atom.config.pushAtKeyPath('core.disabledPackages', packageName) atom.packages.observeDisabledPackages() expect(atom.config.get('core.disabledPackages')).toContain(packageName) const pack = atom.packages.enablePackage(packageName) - const loadedPackages = atom.packages.getLoadedPackages() - let activatedPackages = null - waitsFor(() => { - activatedPackages = atom.packages.getActivePackages() - return activatedPackages.length > 0 - }) + await new Promise(resolve => atom.packages.onDidActivatePackage(resolve)) - runs(() => { - expect(loadedPackages).toContain(pack) - expect(activatedPackages).toContain(pack) - expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) - }) + expect(atom.packages.getLoadedPackages()).toContain(pack) + expect(atom.packages.getActivePackages()).toContain(pack) + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) }) - it('disables an enabled package', () => { + it('disables an enabled package', async () => { const packageName = 'package-with-main' - let pack = null - let activatedPackages = null + const pack = await atom.packages.activatePackage(packageName) - waitsForPromise(() => atom.packages.activatePackage(packageName)) - - runs(() => { - atom.packages.observeDisabledPackages() - expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) - - pack = atom.packages.disablePackage(packageName) + atom.packages.observeDisabledPackages() + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) + await new Promise(resolve => { + atom.packages.onDidDeactivatePackage(resolve) + atom.packages.disablePackage(packageName) }) - waitsFor(() => { - activatedPackages = atom.packages.getActivePackages() - return activatedPackages.length === 0 - }) - - runs(() => { - expect(activatedPackages).not.toContain(pack) - expect(atom.config.get('core.disabledPackages')).toContain(packageName) - }) + expect(atom.packages.getActivePackages()).not.toContain(pack) + expect(atom.config.get('core.disabledPackages')).toContain(packageName) }) it('returns null if the package cannot be loaded', () => { @@ -1499,48 +1305,30 @@ describe('PackageManager', () => { }) describe('with themes', () => { - let didChangeActiveThemesHandler = null + beforeEach(() => atom.themes.activateThemes()) + afterEach(() => atom.themes.deactivateThemes()) - beforeEach(() => { - waitsForPromise(() => atom.themes.activateThemes()) - }) - - afterEach(() => { - waitsForPromise(() => atom.themes.deactivateThemes()) - }) - - it('enables and disables a theme', () => { + it('enables and disables a theme', async () => { const packageName = 'theme-with-package-file' - expect(atom.config.get('core.themes')).not.toContain(packageName) expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) // enabling of theme - let pack = atom.packages.enablePackage(packageName) + const pack = atom.packages.enablePackage(packageName) + await new Promise(resolve => atom.packages.onDidActivatePackage(resolve)) + expect(atom.packages.isPackageActive(packageName)).toBe(true) + expect(atom.config.get('core.themes')).toContain(packageName) + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) - waitsFor('theme to enable', 500, () => { - return atom.packages.getActivePackages().includes(pack) + await new Promise(resolve => { + atom.themes.onDidChangeActiveThemes(resolve) + atom.packages.disablePackage(packageName) }) - runs(() => { - expect(atom.config.get('core.themes')).toContain(packageName) - expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) - - didChangeActiveThemesHandler = jasmine.createSpy('didChangeActiveThemesHandler') - didChangeActiveThemesHandler.reset() - atom.themes.onDidChangeActiveThemes(didChangeActiveThemesHandler) - - pack = atom.packages.disablePackage(packageName) - }) - - waitsFor('did-change-active-themes event to fire', 500, () => didChangeActiveThemesHandler.callCount === 1) - - runs(() => { - expect(atom.packages.getActivePackages()).not.toContain(pack) - expect(atom.config.get('core.themes')).not.toContain(packageName) - expect(atom.config.get('core.themes')).not.toContain(packageName) - expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) - }) + expect(atom.packages.getActivePackages()).not.toContain(pack) + expect(atom.config.get('core.themes')).not.toContain(packageName) + expect(atom.config.get('core.themes')).not.toContain(packageName) + expect(atom.config.get('core.disabledPackages')).not.toContain(packageName) }) }) }) From a778d5e09c7a5a4e92aa32ef9af5ae6955be1a62 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 28 Sep 2017 17:09:55 -0700 Subject: [PATCH 62/81] :art: --- spec/package-manager-spec.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js index 9b28ab437..1d949859d 100644 --- a/spec/package-manager-spec.js +++ b/spec/package-manager-spec.js @@ -51,7 +51,9 @@ describe('PackageManager', () => { describe('when the core.apmPath setting is set', () => { beforeEach(() => atom.config.set('core.apmPath', '/path/to/apm')) - it('returns the value of the core.apmPath config setting', () => expect(atom.packages.getApmPath()).toBe('/path/to/apm')) + it('returns the value of the core.apmPath config setting', () => { + expect(atom.packages.getApmPath()).toBe('/path/to/apm') + }) }) }) @@ -111,7 +113,9 @@ describe('PackageManager', () => { expect(addErrorHandler.argsForCall[0][0].options.packageName).toEqual('package-with-broken-package-json') }) - it('returns null if the package name or path starts with a dot', () => expect(atom.packages.loadPackage('/Users/user/.atom/packages/.git')).toBeNull()) + it('returns null if the package name or path starts with a dot', () => { + expect(atom.packages.loadPackage('/Users/user/.atom/packages/.git')).toBeNull() + }) it('normalizes short repository urls in package.json', () => { let {metadata} = atom.packages.loadPackage('package-with-short-url-package-json') From be2aa7b6f57286508d457ac26bb9fcb20c37cf99 Mon Sep 17 00:00:00 2001 From: Michelle Tilley Date: Fri, 29 Sep 2017 09:37:36 -0700 Subject: [PATCH 63/81] :arrow_up: github@0.6.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 73a317d5a..9ed2dc6a0 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "exception-reporting": "0.41.4", "find-and-replace": "0.212.3", "fuzzy-finder": "1.6.1", - "github": "0.6.2", + "github": "0.6.3", "git-diff": "1.3.6", "go-to-line": "0.32.1", "grammar-selector": "0.49.6", From e537a2429acecbc1a9dcde12737dec1993dc7110 Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Fri, 29 Sep 2017 13:29:06 -0600 Subject: [PATCH 64/81] :arrow_up: autocomplete-plus@2.36.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9ed2dc6a0..1fd8ca343 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", - "autocomplete-plus": "2.35.11", + "autocomplete-plus": "2.36.0", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.6", From eb46d0a5c6d24d970db8f41b479d0ff654993be1 Mon Sep 17 00:00:00 2001 From: Matthew Dapena-Tretter Date: Fri, 29 Sep 2017 14:34:43 -0700 Subject: [PATCH 65/81] Fix mouseup listener cleanup when dragging in text editor --- src/text-editor-component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 093f2590e..5c3b6d1bc 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -1883,7 +1883,7 @@ class TextEditorComponent { function didMouseUp () { window.removeEventListener('mousemove', didMouseMove) - window.removeEventListener('mouseup', didMouseUp) + window.removeEventListener('mouseup', didMouseUp, {capture: true}) bufferWillChangeDisposable.dispose() if (dragging) { dragging = false From 72d89625e8242bf6886b229b5f9221e3a8d7515b Mon Sep 17 00:00:00 2001 From: Ian Olsen Date: Thu, 28 Sep 2017 13:45:07 -0700 Subject: [PATCH 66/81] :arrow_up: electron@1.6.14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1fd8ca343..98efa107a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/atom/atom/issues" }, "license": "MIT", - "electronVersion": "1.6.9", + "electronVersion": "1.6.14", "dependencies": { "@atom/source-map-support": "^0.3.4", "async": "0.2.6", From 9f12b4f5692d58e86f9132be848ca2532e988662 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Sep 2017 14:38:49 -0700 Subject: [PATCH 67/81] Convert Cursor to JS --- src/cursor.coffee | 659 ---------------------------------------- src/cursor.js | 754 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 754 insertions(+), 659 deletions(-) delete mode 100644 src/cursor.coffee create mode 100644 src/cursor.js diff --git a/src/cursor.coffee b/src/cursor.coffee deleted file mode 100644 index 2acbfecf4..000000000 --- a/src/cursor.coffee +++ /dev/null @@ -1,659 +0,0 @@ -{Point, Range} = require 'text-buffer' -{Emitter} = require 'event-kit' -_ = require 'underscore-plus' -Model = require './model' - -EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g - -# Extended: The `Cursor` class represents the little blinking line identifying -# where text can be inserted. -# -# Cursors belong to {TextEditor}s and have some metadata attached in the form -# of a {DisplayMarker}. -module.exports = -class Cursor extends Model - screenPosition: null - bufferPosition: null - goalColumn: null - - # Instantiated by a {TextEditor} - constructor: ({@editor, @marker, id}) -> - @emitter = new Emitter - @assignId(id) - - destroy: -> - @marker.destroy() - - ### - Section: Event Subscription - ### - - # Public: Calls your `callback` when the cursor has been moved. - # - # * `callback` {Function} - # * `event` {Object} - # * `oldBufferPosition` {Point} - # * `oldScreenPosition` {Point} - # * `newBufferPosition` {Point} - # * `newScreenPosition` {Point} - # * `textChanged` {Boolean} - # * `cursor` {Cursor} that triggered the event - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangePosition: (callback) -> - @emitter.on 'did-change-position', callback - - # Public: Calls your `callback` when the cursor is destroyed - # - # * `callback` {Function} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidDestroy: (callback) -> - @emitter.once 'did-destroy', callback - - ### - Section: Managing Cursor Position - ### - - # Public: Moves a cursor to a given screen position. - # - # * `screenPosition` {Array} of two numbers: the screen row, and the screen column. - # * `options` (optional) {Object} with the following keys: - # * `autoscroll` A Boolean which, if `true`, scrolls the {TextEditor} to wherever - # the cursor moves to. - setScreenPosition: (screenPosition, options={}) -> - @changePosition options, => - @marker.setHeadScreenPosition(screenPosition, options) - - # Public: Returns the screen position of the cursor as a {Point}. - getScreenPosition: -> - @marker.getHeadScreenPosition() - - # Public: Moves a cursor to a given buffer position. - # - # * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column. - # * `options` (optional) {Object} with the following keys: - # * `autoscroll` {Boolean} indicating whether to autoscroll to the new - # position. Defaults to `true` if this is the most recently added cursor, - # `false` otherwise. - setBufferPosition: (bufferPosition, options={}) -> - @changePosition options, => - @marker.setHeadBufferPosition(bufferPosition, options) - - # Public: Returns the current buffer position as an Array. - getBufferPosition: -> - @marker.getHeadBufferPosition() - - # Public: Returns the cursor's current screen row. - getScreenRow: -> - @getScreenPosition().row - - # Public: Returns the cursor's current screen column. - getScreenColumn: -> - @getScreenPosition().column - - # Public: Retrieves the cursor's current buffer row. - getBufferRow: -> - @getBufferPosition().row - - # Public: Returns the cursor's current buffer column. - getBufferColumn: -> - @getBufferPosition().column - - # Public: Returns the cursor's current buffer row of text excluding its line - # ending. - getCurrentBufferLine: -> - @editor.lineTextForBufferRow(@getBufferRow()) - - # Public: Returns whether the cursor is at the start of a line. - isAtBeginningOfLine: -> - @getBufferPosition().column is 0 - - # Public: Returns whether the cursor is on the line return character. - isAtEndOfLine: -> - @getBufferPosition().isEqual(@getCurrentLineBufferRange().end) - - ### - Section: Cursor Position Details - ### - - # Public: Returns the underlying {DisplayMarker} for the cursor. - # Useful with overlay {Decoration}s. - getMarker: -> @marker - - # Public: Identifies if the cursor is surrounded by whitespace. - # - # "Surrounded" here means that the character directly before and after the - # cursor are both whitespace. - # - # Returns a {Boolean}. - isSurroundedByWhitespace: -> - {row, column} = @getBufferPosition() - range = [[row, column - 1], [row, column + 1]] - /^\s+$/.test @editor.getTextInBufferRange(range) - - # Public: Returns whether the cursor is currently between a word and non-word - # character. The non-word characters are defined by the - # `editor.nonWordCharacters` config value. - # - # This method returns false if the character before or after the cursor is - # whitespace. - # - # Returns a Boolean. - isBetweenWordAndNonWord: -> - return false if @isAtBeginningOfLine() or @isAtEndOfLine() - - {row, column} = @getBufferPosition() - range = [[row, column - 1], [row, column + 1]] - [before, after] = @editor.getTextInBufferRange(range) - return false if /\s/.test(before) or /\s/.test(after) - - nonWordCharacters = @getNonWordCharacters() - nonWordCharacters.includes(before) isnt nonWordCharacters.includes(after) - - # Public: Returns whether this cursor is between a word's start and end. - # - # * `options` (optional) {Object} - # * `wordRegex` A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}). - # - # Returns a {Boolean} - isInsideWord: (options) -> - {row, column} = @getBufferPosition() - range = [[row, column], [row, Infinity]] - @editor.getTextInBufferRange(range).search(options?.wordRegex ? @wordRegExp()) is 0 - - # Public: Returns the indentation level of the current line. - getIndentLevel: -> - if @editor.getSoftTabs() - @getBufferColumn() / @editor.getTabLength() - else - @getBufferColumn() - - # Public: Retrieves the scope descriptor for the cursor's current position. - # - # Returns a {ScopeDescriptor} - getScopeDescriptor: -> - @editor.scopeDescriptorForBufferPosition(@getBufferPosition()) - - # Public: Returns true if this cursor has no non-whitespace characters before - # its current position. - hasPrecedingCharactersOnLine: -> - bufferPosition = @getBufferPosition() - line = @editor.lineTextForBufferRow(bufferPosition.row) - firstCharacterColumn = line.search(/\S/) - - if firstCharacterColumn is -1 - false - else - bufferPosition.column > firstCharacterColumn - - # Public: Identifies if this cursor is the last in the {TextEditor}. - # - # "Last" is defined as the most recently added cursor. - # - # Returns a {Boolean}. - isLastCursor: -> - this is @editor.getLastCursor() - - ### - Section: Moving the Cursor - ### - - # Public: Moves the cursor up one screen row. - # - # * `rowCount` (optional) {Number} number of rows to move (default: 1) - # * `options` (optional) {Object} with the following keys: - # * `moveToEndOfSelection` if true, move to the left of the selection if a - # selection exists. - moveUp: (rowCount=1, {moveToEndOfSelection}={}) -> - range = @marker.getScreenRange() - if moveToEndOfSelection and not range.isEmpty() - {row, column} = range.start - else - {row, column} = @getScreenPosition() - - column = @goalColumn if @goalColumn? - @setScreenPosition({row: row - rowCount, column: column}, skipSoftWrapIndentation: true) - @goalColumn = column - - # Public: Moves the cursor down one screen row. - # - # * `rowCount` (optional) {Number} number of rows to move (default: 1) - # * `options` (optional) {Object} with the following keys: - # * `moveToEndOfSelection` if true, move to the left of the selection if a - # selection exists. - moveDown: (rowCount=1, {moveToEndOfSelection}={}) -> - range = @marker.getScreenRange() - if moveToEndOfSelection and not range.isEmpty() - {row, column} = range.end - else - {row, column} = @getScreenPosition() - - column = @goalColumn if @goalColumn? - @setScreenPosition({row: row + rowCount, column: column}, skipSoftWrapIndentation: true) - @goalColumn = column - - # Public: Moves the cursor left one screen column. - # - # * `columnCount` (optional) {Number} number of columns to move (default: 1) - # * `options` (optional) {Object} with the following keys: - # * `moveToEndOfSelection` if true, move to the left of the selection if a - # selection exists. - moveLeft: (columnCount=1, {moveToEndOfSelection}={}) -> - range = @marker.getScreenRange() - if moveToEndOfSelection and not range.isEmpty() - @setScreenPosition(range.start) - else - {row, column} = @getScreenPosition() - - while columnCount > column and row > 0 - columnCount -= column - column = @editor.lineLengthForScreenRow(--row) - columnCount-- # subtract 1 for the row move - - column = column - columnCount - @setScreenPosition({row, column}, clipDirection: 'backward') - - # Public: Moves the cursor right one screen column. - # - # * `columnCount` (optional) {Number} number of columns to move (default: 1) - # * `options` (optional) {Object} with the following keys: - # * `moveToEndOfSelection` if true, move to the right of the selection if a - # selection exists. - moveRight: (columnCount=1, {moveToEndOfSelection}={}) -> - range = @marker.getScreenRange() - if moveToEndOfSelection and not range.isEmpty() - @setScreenPosition(range.end) - else - {row, column} = @getScreenPosition() - maxLines = @editor.getScreenLineCount() - rowLength = @editor.lineLengthForScreenRow(row) - columnsRemainingInLine = rowLength - column - - while columnCount > columnsRemainingInLine and row < maxLines - 1 - columnCount -= columnsRemainingInLine - columnCount-- # subtract 1 for the row move - - column = 0 - rowLength = @editor.lineLengthForScreenRow(++row) - columnsRemainingInLine = rowLength - - column = column + columnCount - @setScreenPosition({row, column}, clipDirection: 'forward') - - # Public: Moves the cursor to the top of the buffer. - moveToTop: -> - @setBufferPosition([0, 0]) - - # Public: Moves the cursor to the bottom of the buffer. - moveToBottom: -> - @setBufferPosition(@editor.getEofBufferPosition()) - - # Public: Moves the cursor to the beginning of the line. - moveToBeginningOfScreenLine: -> - @setScreenPosition([@getScreenRow(), 0]) - - # Public: Moves the cursor to the beginning of the buffer line. - moveToBeginningOfLine: -> - @setBufferPosition([@getBufferRow(), 0]) - - # Public: Moves the cursor to the beginning of the first character in the - # line. - moveToFirstCharacterOfLine: -> - screenRow = @getScreenRow() - screenLineStart = @editor.clipScreenPosition([screenRow, 0], skipSoftWrapIndentation: true) - screenLineEnd = [screenRow, Infinity] - screenLineBufferRange = @editor.bufferRangeForScreenRange([screenLineStart, screenLineEnd]) - - firstCharacterColumn = null - @editor.scanInBufferRange /\S/, screenLineBufferRange, ({range, stop}) -> - firstCharacterColumn = range.start.column - stop() - - if firstCharacterColumn? and firstCharacterColumn isnt @getBufferColumn() - targetBufferColumn = firstCharacterColumn - else - targetBufferColumn = screenLineBufferRange.start.column - - @setBufferPosition([screenLineBufferRange.start.row, targetBufferColumn]) - - # Public: Moves the cursor to the end of the line. - moveToEndOfScreenLine: -> - @setScreenPosition([@getScreenRow(), Infinity]) - - # Public: Moves the cursor to the end of the buffer line. - moveToEndOfLine: -> - @setBufferPosition([@getBufferRow(), Infinity]) - - # Public: Moves the cursor to the beginning of the word. - moveToBeginningOfWord: -> - @setBufferPosition(@getBeginningOfCurrentWordBufferPosition()) - - # Public: Moves the cursor to the end of the word. - moveToEndOfWord: -> - if position = @getEndOfCurrentWordBufferPosition() - @setBufferPosition(position) - - # Public: Moves the cursor to the beginning of the next word. - moveToBeginningOfNextWord: -> - if position = @getBeginningOfNextWordBufferPosition() - @setBufferPosition(position) - - # Public: Moves the cursor to the previous word boundary. - moveToPreviousWordBoundary: -> - if position = @getPreviousWordBoundaryBufferPosition() - @setBufferPosition(position) - - # Public: Moves the cursor to the next word boundary. - moveToNextWordBoundary: -> - if position = @getNextWordBoundaryBufferPosition() - @setBufferPosition(position) - - # Public: Moves the cursor to the previous subword boundary. - moveToPreviousSubwordBoundary: -> - options = {wordRegex: @subwordRegExp(backwards: true)} - if position = @getPreviousWordBoundaryBufferPosition(options) - @setBufferPosition(position) - - # Public: Moves the cursor to the next subword boundary. - moveToNextSubwordBoundary: -> - options = {wordRegex: @subwordRegExp()} - if position = @getNextWordBoundaryBufferPosition(options) - @setBufferPosition(position) - - # Public: Moves the cursor to the beginning of the buffer line, skipping all - # whitespace. - skipLeadingWhitespace: -> - position = @getBufferPosition() - scanRange = @getCurrentLineBufferRange() - endOfLeadingWhitespace = null - @editor.scanInBufferRange /^[ \t]*/, scanRange, ({range}) -> - endOfLeadingWhitespace = range.end - - @setBufferPosition(endOfLeadingWhitespace) if endOfLeadingWhitespace.isGreaterThan(position) - - # Public: Moves the cursor to the beginning of the next paragraph - moveToBeginningOfNextParagraph: -> - if position = @getBeginningOfNextParagraphBufferPosition() - @setBufferPosition(position) - - # Public: Moves the cursor to the beginning of the previous paragraph - moveToBeginningOfPreviousParagraph: -> - if position = @getBeginningOfPreviousParagraphBufferPosition() - @setBufferPosition(position) - - ### - Section: Local Positions and Ranges - ### - - # Public: Returns buffer position of previous word boundary. It might be on - # the current word, or the previous word. - # - # * `options` (optional) {Object} with the following keys: - # * `wordRegex` A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}) - getPreviousWordBoundaryBufferPosition: (options = {}) -> - currentBufferPosition = @getBufferPosition() - previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row) - scanRange = [[previousNonBlankRow ? 0, 0], currentBufferPosition] - - beginningOfWordPosition = null - @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) -> - if range.start.row < currentBufferPosition.row and currentBufferPosition.column > 0 - # force it to stop at the beginning of each line - beginningOfWordPosition = new Point(currentBufferPosition.row, 0) - else if range.end.isLessThan(currentBufferPosition) - beginningOfWordPosition = range.end - else - beginningOfWordPosition = range.start - - if not beginningOfWordPosition?.isEqual(currentBufferPosition) - stop() - - beginningOfWordPosition or currentBufferPosition - - # Public: Returns buffer position of the next word boundary. It might be on - # the current word, or the previous word. - # - # * `options` (optional) {Object} with the following keys: - # * `wordRegex` A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}) - getNextWordBoundaryBufferPosition: (options = {}) -> - currentBufferPosition = @getBufferPosition() - scanRange = [currentBufferPosition, @editor.getEofBufferPosition()] - - endOfWordPosition = null - @editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) -> - if range.start.row > currentBufferPosition.row - # force it to stop at the beginning of each line - endOfWordPosition = new Point(range.start.row, 0) - else if range.start.isGreaterThan(currentBufferPosition) - endOfWordPosition = range.start - else - endOfWordPosition = range.end - - if not endOfWordPosition?.isEqual(currentBufferPosition) - stop() - - endOfWordPosition or currentBufferPosition - - # Public: Retrieves the buffer position of where the current word starts. - # - # * `options` (optional) An {Object} with the following keys: - # * `wordRegex` A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}). - # * `includeNonWordCharacters` A {Boolean} indicating whether to include - # non-word characters in the default word regex. - # Has no effect if wordRegex is set. - # * `allowPrevious` A {Boolean} indicating whether the beginning of the - # previous word can be returned. - # - # Returns a {Range}. - getBeginningOfCurrentWordBufferPosition: (options = {}) -> - allowPrevious = options.allowPrevious ? true - currentBufferPosition = @getBufferPosition() - previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row) ? 0 - scanRange = [[previousNonBlankRow, 0], currentBufferPosition] - - beginningOfWordPosition = null - @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, matchText, stop}) -> - # Ignore 'empty line' matches between '\r' and '\n' - return if matchText is '' and range.start.column isnt 0 - - if range.start.isLessThan(currentBufferPosition) - if range.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious - beginningOfWordPosition = range.start - stop() - - if beginningOfWordPosition? - beginningOfWordPosition - else if allowPrevious - new Point(0, 0) - else - currentBufferPosition - - # Public: Retrieves the buffer position of where the current word ends. - # - # * `options` (optional) {Object} with the following keys: - # * `wordRegex` A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}) - # * `includeNonWordCharacters` A Boolean indicating whether to include - # non-word characters in the default word regex. Has no effect if - # wordRegex is set. - # - # Returns a {Range}. - getEndOfCurrentWordBufferPosition: (options = {}) -> - allowNext = options.allowNext ? true - currentBufferPosition = @getBufferPosition() - scanRange = [currentBufferPosition, @editor.getEofBufferPosition()] - - endOfWordPosition = null - @editor.scanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, matchText, stop}) -> - # Ignore 'empty line' matches between '\r' and '\n' - return if matchText is '' and range.start.column isnt 0 - - if range.end.isGreaterThan(currentBufferPosition) - if allowNext or range.start.isLessThanOrEqual(currentBufferPosition) - endOfWordPosition = range.end - stop() - - endOfWordPosition ? currentBufferPosition - - # Public: Retrieves the buffer position of where the next word starts. - # - # * `options` (optional) {Object} - # * `wordRegex` A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}). - # - # Returns a {Range} - getBeginningOfNextWordBufferPosition: (options = {}) -> - currentBufferPosition = @getBufferPosition() - start = if @isInsideWord(options) then @getEndOfCurrentWordBufferPosition(options) else currentBufferPosition - scanRange = [start, @editor.getEofBufferPosition()] - - beginningOfNextWordPosition = null - @editor.scanInBufferRange (options.wordRegex ? @wordRegExp()), scanRange, ({range, stop}) -> - beginningOfNextWordPosition = range.start - stop() - - beginningOfNextWordPosition or currentBufferPosition - - # Public: Returns the buffer Range occupied by the word located under the cursor. - # - # * `options` (optional) {Object} - # * `wordRegex` A {RegExp} indicating what constitutes a "word" - # (default: {::wordRegExp}). - getCurrentWordBufferRange: (options={}) -> - startOptions = Object.assign(_.clone(options), allowPrevious: false) - endOptions = Object.assign(_.clone(options), allowNext: false) - new Range(@getBeginningOfCurrentWordBufferPosition(startOptions), @getEndOfCurrentWordBufferPosition(endOptions)) - - # Public: Returns the buffer Range for the current line. - # - # * `options` (optional) {Object} - # * `includeNewline` A {Boolean} which controls whether the Range should - # include the newline. - getCurrentLineBufferRange: (options) -> - @editor.bufferRangeForBufferRow(@getBufferRow(), options) - - # Public: Retrieves the range for the current paragraph. - # - # A paragraph is defined as a block of text surrounded by empty lines or comments. - # - # Returns a {Range}. - getCurrentParagraphBufferRange: -> - @editor.rowRangeForParagraphAtBufferRow(@getBufferRow()) - - # Public: Returns the characters preceding the cursor in the current word. - getCurrentWordPrefix: -> - @editor.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()]) - - ### - Section: Visibility - ### - - ### - Section: Comparing to another cursor - ### - - # Public: Compare this cursor's buffer position to another cursor's buffer position. - # - # See {Point::compare} for more details. - # - # * `otherCursor`{Cursor} to compare against - compare: (otherCursor) -> - @getBufferPosition().compare(otherCursor.getBufferPosition()) - - ### - Section: Utilities - ### - - # Public: Deselects the current selection. - clearSelection: (options) -> - @selection?.clear(options) - - # Public: Get the RegExp used by the cursor to determine what a "word" is. - # - # * `options` (optional) {Object} with the following keys: - # * `includeNonWordCharacters` A {Boolean} indicating whether to include - # non-word characters in the regex. (default: true) - # - # Returns a {RegExp}. - wordRegExp: (options) -> - nonWordCharacters = _.escapeRegExp(@getNonWordCharacters()) - source = "^[\t ]*$|[^\\s#{nonWordCharacters}]+" - if options?.includeNonWordCharacters ? true - source += "|" + "[#{nonWordCharacters}]+" - new RegExp(source, "g") - - # Public: Get the RegExp used by the cursor to determine what a "subword" is. - # - # * `options` (optional) {Object} with the following keys: - # * `backwards` A {Boolean} indicating whether to look forwards or backwards - # for the next subword. (default: false) - # - # Returns a {RegExp}. - subwordRegExp: (options={}) -> - nonWordCharacters = @getNonWordCharacters() - lowercaseLetters = 'a-z\\u00DF-\\u00F6\\u00F8-\\u00FF' - uppercaseLetters = 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE' - snakeCamelSegment = "[#{uppercaseLetters}]?[#{lowercaseLetters}]+" - segments = [ - "^[\t ]+", - "[\t ]+$", - "[#{uppercaseLetters}]+(?![#{lowercaseLetters}])", - "\\d+" - ] - if options.backwards - segments.push("#{snakeCamelSegment}_*") - segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+\\s*") - else - segments.push("_*#{snakeCamelSegment}") - segments.push("\\s*[#{_.escapeRegExp(nonWordCharacters)}]+") - segments.push("_+") - new RegExp(segments.join("|"), "g") - - ### - Section: Private - ### - - getNonWordCharacters: -> - @editor.getNonWordCharacters(@getScopeDescriptor().getScopesArray()) - - changePosition: (options, fn) -> - @clearSelection(autoscroll: false) - fn() - @autoscroll() if options.autoscroll ? @isLastCursor() - - getScreenRange: -> - {row, column} = @getScreenPosition() - new Range(new Point(row, column), new Point(row, column + 1)) - - autoscroll: (options = {}) -> - options.clip = false - @editor.scrollToScreenRange(@getScreenRange(), options) - - getBeginningOfNextParagraphBufferPosition: -> - start = @getBufferPosition() - eof = @editor.getEofBufferPosition() - scanRange = [start, eof] - - {row, column} = eof - position = new Point(row, column - 1) - - @editor.scanInBufferRange EmptyLineRegExp, scanRange, ({range, stop}) -> - position = range.start.traverse(Point(1, 0)) - stop() unless position.isEqual(start) - position - - getBeginningOfPreviousParagraphBufferPosition: -> - start = @getBufferPosition() - - {row, column} = start - scanRange = [[row-1, column], [0, 0]] - position = new Point(0, 0) - @editor.backwardsScanInBufferRange EmptyLineRegExp, scanRange, ({range, stop}) -> - position = range.start.traverse(Point(1, 0)) - stop() unless position.isEqual(start) - position diff --git a/src/cursor.js b/src/cursor.js new file mode 100644 index 000000000..712847bc7 --- /dev/null +++ b/src/cursor.js @@ -0,0 +1,754 @@ +const {Point, Range} = require('text-buffer') +const {Emitter} = require('event-kit') +const _ = require('underscore-plus') +const Model = require('./model') + +const EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g + +// Extended: The `Cursor` class represents the little blinking line identifying +// where text can be inserted. +// +// Cursors belong to {TextEditor}s and have some metadata attached in the form +// of a {DisplayMarker}. +module.exports = +class Cursor extends Model { + // Instantiated by a {TextEditor} + constructor (params) { + super(params) + this.editor = params.editor + this.marker = params.marker + this.emitter = new Emitter() + } + + destroy () { + this.marker.destroy() + } + + /* + Section: Event Subscription + */ + + // Public: Calls your `callback` when the cursor has been moved. + // + // * `callback` {Function} + // * `event` {Object} + // * `oldBufferPosition` {Point} + // * `oldScreenPosition` {Point} + // * `newBufferPosition` {Point} + // * `newScreenPosition` {Point} + // * `textChanged` {Boolean} + // * `cursor` {Cursor} that triggered the event + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidChangePosition (callback) { + return this.emitter.on('did-change-position', callback) + } + + // Public: Calls your `callback` when the cursor is destroyed + // + // * `callback` {Function} + // + // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidDestroy (callback) { + return this.emitter.once('did-destroy', callback) + } + + /* + Section: Managing Cursor Position + */ + + // Public: Moves a cursor to a given screen position. + // + // * `screenPosition` {Array} of two numbers: the screen row, and the screen column. + // * `options` (optional) {Object} with the following keys: + // * `autoscroll` A Boolean which, if `true`, scrolls the {TextEditor} to wherever + // the cursor moves to. + setScreenPosition (screenPosition, options = {}) { + this.changePosition(options, () => { + this.marker.setHeadScreenPosition(screenPosition, options) + }) + } + + // Public: Returns the screen position of the cursor as a {Point}. + getScreenPosition () { + return this.marker.getHeadScreenPosition() + } + + // Public: Moves a cursor to a given buffer position. + // + // * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column. + // * `options` (optional) {Object} with the following keys: + // * `autoscroll` {Boolean} indicating whether to autoscroll to the new + // position. Defaults to `true` if this is the most recently added cursor, + // `false` otherwise. + setBufferPosition (bufferPosition, options = {}) { + this.changePosition(options, () => { + this.marker.setHeadBufferPosition(bufferPosition, options) + }) + } + + // Public: Returns the current buffer position as an Array. + getBufferPosition () { + return this.marker.getHeadBufferPosition() + } + + // Public: Returns the cursor's current screen row. + getScreenRow () { + return this.getScreenPosition().row + } + + // Public: Returns the cursor's current screen column. + getScreenColumn () { + return this.getScreenPosition().column + } + + // Public: Retrieves the cursor's current buffer row. + getBufferRow () { + return this.getBufferPosition().row + } + + // Public: Returns the cursor's current buffer column. + getBufferColumn () { + return this.getBufferPosition().column + } + + // Public: Returns the cursor's current buffer row of text excluding its line + // ending. + getCurrentBufferLine () { + return this.editor.lineTextForBufferRow(this.getBufferRow()) + } + + // Public: Returns whether the cursor is at the start of a line. + isAtBeginningOfLine () { + return this.getBufferPosition().column === 0 + } + + // Public: Returns whether the cursor is on the line return character. + isAtEndOfLine () { + return this.getBufferPosition().isEqual(this.getCurrentLineBufferRange().end) + } + + /* + Section: Cursor Position Details + */ + + // Public: Returns the underlying {DisplayMarker} for the cursor. + // Useful with overlay {Decoration}s. + getMarker () { return this.marker } + + // Public: Identifies if the cursor is surrounded by whitespace. + // + // "Surrounded" here means that the character directly before and after the + // cursor are both whitespace. + // + // Returns a {Boolean}. + isSurroundedByWhitespace () { + const {row, column} = this.getBufferPosition() + const range = [[row, column - 1], [row, column + 1]] + return /^\s+$/.test(this.editor.getTextInBufferRange(range)) + } + + // Public: Returns whether the cursor is currently between a word and non-word + // character. The non-word characters are defined by the + // `editor.nonWordCharacters` config value. + // + // This method returns false if the character before or after the cursor is + // whitespace. + // + // Returns a Boolean. + isBetweenWordAndNonWord () { + if (this.isAtBeginningOfLine() || this.isAtEndOfLine()) return false + + const {row, column} = this.getBufferPosition() + const range = [[row, column - 1], [row, column + 1]] + const text = this.editor.getTextInBufferRange(range) + if (/\s/.test(text[0]) || /\s/.test(text[1])) return false + + const nonWordCharacters = this.getNonWordCharacters() + return nonWordCharacters.includes(text[0]) !== nonWordCharacters.includes(text[1]) + } + + // Public: Returns whether this cursor is between a word's start and end. + // + // * `options` (optional) {Object} + // * `wordRegex` A {RegExp} indicating what constitutes a "word" + // (default: {::wordRegExp}). + // + // Returns a {Boolean} + isInsideWord (options) { + const {row, column} = this.getBufferPosition() + const range = [[row, column], [row, Infinity]] + const text = this.editor.getTextInBufferRange(range) + return text.search((options && options.wordRegex) || this.wordRegExp()) === 0 + } + + // Public: Returns the indentation level of the current line. + getIndentLevel () { + if (this.editor.getSoftTabs()) { + return this.getBufferColumn() / this.editor.getTabLength() + } else { + return this.getBufferColumn() + } + } + + // Public: Retrieves the scope descriptor for the cursor's current position. + // + // Returns a {ScopeDescriptor} + getScopeDescriptor () { + return this.editor.scopeDescriptorForBufferPosition(this.getBufferPosition()) + } + + // Public: Returns true if this cursor has no non-whitespace characters before + // its current position. + hasPrecedingCharactersOnLine () { + const bufferPosition = this.getBufferPosition() + const line = this.editor.lineTextForBufferRow(bufferPosition.row) + const firstCharacterColumn = line.search(/\S/) + + if (firstCharacterColumn === -1) { + return false + } else { + return bufferPosition.column > firstCharacterColumn + } + } + + // Public: Identifies if this cursor is the last in the {TextEditor}. + // + // "Last" is defined as the most recently added cursor. + // + // Returns a {Boolean}. + isLastCursor () { + return this === this.editor.getLastCursor() + } + + /* + Section: Moving the Cursor + */ + + // Public: Moves the cursor up one screen row. + // + // * `rowCount` (optional) {Number} number of rows to move (default: 1) + // * `options` (optional) {Object} with the following keys: + // * `moveToEndOfSelection` if true, move to the left of the selection if a + // selection exists. + moveUp (rowCount = 1, {moveToEndOfSelection} = {}) { + let row, column + const range = this.marker.getScreenRange() + if (moveToEndOfSelection && !range.isEmpty()) { + ({row, column} = range.start) + } else { + ({row, column} = this.getScreenPosition()) + } + + if (this.goalColumn != null) column = this.goalColumn + this.setScreenPosition({row: row - rowCount, column}, {skipSoftWrapIndentation: true}) + this.goalColumn = column + } + + // Public: Moves the cursor down one screen row. + // + // * `rowCount` (optional) {Number} number of rows to move (default: 1) + // * `options` (optional) {Object} with the following keys: + // * `moveToEndOfSelection` if true, move to the left of the selection if a + // selection exists. + moveDown (rowCount = 1, {moveToEndOfSelection} = {}) { + let row, column + const range = this.marker.getScreenRange() + if (moveToEndOfSelection && !range.isEmpty()) { + ({row, column} = range.end) + } else { + ({row, column} = this.getScreenPosition()) + } + + if (this.goalColumn != null) column = this.goalColumn + this.setScreenPosition({row: row + rowCount, column}, {skipSoftWrapIndentation: true}) + this.goalColumn = column + } + + // Public: Moves the cursor left one screen column. + // + // * `columnCount` (optional) {Number} number of columns to move (default: 1) + // * `options` (optional) {Object} with the following keys: + // * `moveToEndOfSelection` if true, move to the left of the selection if a + // selection exists. + moveLeft (columnCount = 1, {moveToEndOfSelection} = {}) { + const range = this.marker.getScreenRange() + if (moveToEndOfSelection && !range.isEmpty()) { + this.setScreenPosition(range.start) + } else { + let {row, column} = this.getScreenPosition() + + while (columnCount > column && row > 0) { + columnCount -= column + column = this.editor.lineLengthForScreenRow(--row) + columnCount-- + } // subtract 1 for the row move + + column = column - columnCount + this.setScreenPosition({row, column}, {clipDirection: 'backward'}) + } + } + + // Public: Moves the cursor right one screen column. + // + // * `columnCount` (optional) {Number} number of columns to move (default: 1) + // * `options` (optional) {Object} with the following keys: + // * `moveToEndOfSelection` if true, move to the right of the selection if a + // selection exists. + moveRight (columnCount = 1, {moveToEndOfSelection} = {}) { + const range = this.marker.getScreenRange() + if (moveToEndOfSelection && !range.isEmpty()) { + this.setScreenPosition(range.end) + } else { + let {row, column} = this.getScreenPosition() + const maxLines = this.editor.getScreenLineCount() + let rowLength = this.editor.lineLengthForScreenRow(row) + let columnsRemainingInLine = rowLength - column + + while (columnCount > columnsRemainingInLine && row < maxLines - 1) { + columnCount -= columnsRemainingInLine + columnCount-- // subtract 1 for the row move + + column = 0 + rowLength = this.editor.lineLengthForScreenRow(++row) + columnsRemainingInLine = rowLength + } + + column = column + columnCount + this.setScreenPosition({row, column}, {clipDirection: 'forward'}) + } + } + + // Public: Moves the cursor to the top of the buffer. + moveToTop () { + this.setBufferPosition([0, 0]) + } + + // Public: Moves the cursor to the bottom of the buffer. + moveToBottom () { + this.setBufferPosition(this.editor.getEofBufferPosition()) + } + + // Public: Moves the cursor to the beginning of the line. + moveToBeginningOfScreenLine () { + this.setScreenPosition([this.getScreenRow(), 0]) + } + + // Public: Moves the cursor to the beginning of the buffer line. + moveToBeginningOfLine () { + this.setBufferPosition([this.getBufferRow(), 0]) + } + + // Public: Moves the cursor to the beginning of the first character in the + // line. + moveToFirstCharacterOfLine () { + let targetBufferColumn + const screenRow = this.getScreenRow() + const screenLineStart = this.editor.clipScreenPosition([screenRow, 0], {skipSoftWrapIndentation: true}) + const screenLineEnd = [screenRow, Infinity] + const screenLineBufferRange = this.editor.bufferRangeForScreenRange([screenLineStart, screenLineEnd]) + + let firstCharacterColumn = null + this.editor.scanInBufferRange(/\S/, screenLineBufferRange, ({range, stop}) => { + firstCharacterColumn = range.start.column + stop() + }) + + if (firstCharacterColumn != null && firstCharacterColumn !== this.getBufferColumn()) { + targetBufferColumn = firstCharacterColumn + } else { + targetBufferColumn = screenLineBufferRange.start.column + } + + this.setBufferPosition([screenLineBufferRange.start.row, targetBufferColumn]) + } + + // Public: Moves the cursor to the end of the line. + moveToEndOfScreenLine () { + this.setScreenPosition([this.getScreenRow(), Infinity]) + } + + // Public: Moves the cursor to the end of the buffer line. + moveToEndOfLine () { + this.setBufferPosition([this.getBufferRow(), Infinity]) + } + + // Public: Moves the cursor to the beginning of the word. + moveToBeginningOfWord () { + this.setBufferPosition(this.getBeginningOfCurrentWordBufferPosition()) + } + + // Public: Moves the cursor to the end of the word. + moveToEndOfWord () { + const position = this.getEndOfCurrentWordBufferPosition() + if (position) this.setBufferPosition(position) + } + + // Public: Moves the cursor to the beginning of the next word. + moveToBeginningOfNextWord () { + const position = this.getBeginningOfNextWordBufferPosition() + if (position) this.setBufferPosition(position) + } + + // Public: Moves the cursor to the previous word boundary. + moveToPreviousWordBoundary () { + const position = this.getPreviousWordBoundaryBufferPosition() + if (position) this.setBufferPosition(position) + } + + // Public: Moves the cursor to the next word boundary. + moveToNextWordBoundary () { + const position = this.getNextWordBoundaryBufferPosition() + if (position) this.setBufferPosition(position) + } + + // Public: Moves the cursor to the previous subword boundary. + moveToPreviousSubwordBoundary () { + const options = {wordRegex: this.subwordRegExp({backwards: true})} + const position = this.getPreviousWordBoundaryBufferPosition(options) + if (position) this.setBufferPosition(position) + } + + // Public: Moves the cursor to the next subword boundary. + moveToNextSubwordBoundary () { + const options = {wordRegex: this.subwordRegExp()} + const position = this.getNextWordBoundaryBufferPosition(options) + if (position) this.setBufferPosition(position) + } + + // Public: Moves the cursor to the beginning of the buffer line, skipping all + // whitespace. + skipLeadingWhitespace () { + const position = this.getBufferPosition() + const scanRange = this.getCurrentLineBufferRange() + let endOfLeadingWhitespace = null + this.editor.scanInBufferRange(/^[ \t]*/, scanRange, ({range}) => { + endOfLeadingWhitespace = range.end + }) + + if (endOfLeadingWhitespace.isGreaterThan(position)) this.setBufferPosition(endOfLeadingWhitespace) + } + + // Public: Moves the cursor to the beginning of the next paragraph + moveToBeginningOfNextParagraph () { + const position = this.getBeginningOfNextParagraphBufferPosition() + if (position) this.setBufferPosition(position) + } + + // Public: Moves the cursor to the beginning of the previous paragraph + moveToBeginningOfPreviousParagraph () { + const position = this.getBeginningOfPreviousParagraphBufferPosition() + if (position) this.setBufferPosition(position) + } + + /* + Section: Local Positions and Ranges + */ + + // Public: Returns buffer position of previous word boundary. It might be on + // the current word, or the previous word. + // + // * `options` (optional) {Object} with the following keys: + // * `wordRegex` A {RegExp} indicating what constitutes a "word" + // (default: {::wordRegExp}) + getPreviousWordBoundaryBufferPosition (options = {}) { + const currentBufferPosition = this.getBufferPosition() + const previousNonBlankRow = this.editor.buffer.previousNonBlankRow(currentBufferPosition.row) + const scanRange = [[previousNonBlankRow || 0, 0], currentBufferPosition] + + let beginningOfWordPosition + this.editor.backwardsScanInBufferRange(options.wordRegex || this.wordRegExp(), scanRange, ({range, stop}) => { + if (range.start.row < currentBufferPosition.row && currentBufferPosition.column > 0) { + // force it to stop at the beginning of each line + beginningOfWordPosition = new Point(currentBufferPosition.row, 0) + } else if (range.end.isLessThan(currentBufferPosition)) { + beginningOfWordPosition = range.end + } else { + beginningOfWordPosition = range.start + } + + if (!beginningOfWordPosition.isEqual(currentBufferPosition)) stop() + }) + + return beginningOfWordPosition || currentBufferPosition + } + + // Public: Returns buffer position of the next word boundary. It might be on + // the current word, or the previous word. + // + // * `options` (optional) {Object} with the following keys: + // * `wordRegex` A {RegExp} indicating what constitutes a "word" + // (default: {::wordRegExp}) + getNextWordBoundaryBufferPosition (options = {}) { + const currentBufferPosition = this.getBufferPosition() + const scanRange = [currentBufferPosition, this.editor.getEofBufferPosition()] + + let endOfWordPosition + this.editor.scanInBufferRange((options.wordRegex != null ? options.wordRegex : this.wordRegExp()), scanRange, function ({range, stop}) { + if (range.start.row > currentBufferPosition.row) { + // force it to stop at the beginning of each line + endOfWordPosition = new Point(range.start.row, 0) + } else if (range.start.isGreaterThan(currentBufferPosition)) { + endOfWordPosition = range.start + } else { + endOfWordPosition = range.end + } + + if (!endOfWordPosition.isEqual(currentBufferPosition)) stop() + }) + + return endOfWordPosition || currentBufferPosition + } + + // Public: Retrieves the buffer position of where the current word starts. + // + // * `options` (optional) An {Object} with the following keys: + // * `wordRegex` A {RegExp} indicating what constitutes a "word" + // (default: {::wordRegExp}). + // * `includeNonWordCharacters` A {Boolean} indicating whether to include + // non-word characters in the default word regex. + // Has no effect if wordRegex is set. + // * `allowPrevious` A {Boolean} indicating whether the beginning of the + // previous word can be returned. + // + // Returns a {Range}. + getBeginningOfCurrentWordBufferPosition (options = {}) { + const allowPrevious = options.allowPrevious !== false + const currentBufferPosition = this.getBufferPosition() + const previousNonBlankRow = this.editor.buffer.previousNonBlankRow(currentBufferPosition.row) || 0 + const scanRange = [[previousNonBlankRow, 0], currentBufferPosition] + + let beginningOfWordPosition + this.editor.backwardsScanInBufferRange(options.wordRegex || this.wordRegExp(options), scanRange, ({range, matchText, stop}) => { + // Ignore 'empty line' matches between '\r' and '\n' + if ((matchText === '') && range.start.column !== 0) return + + if (range.start.isLessThan(currentBufferPosition)) { + if (range.end.isGreaterThanOrEqual(currentBufferPosition) || allowPrevious) { + beginningOfWordPosition = range.start + } + stop() + } + }) + + if (beginningOfWordPosition) { + return beginningOfWordPosition + } else if (allowPrevious) { + return new Point(0, 0) + } else { + return currentBufferPosition + } + } + + // Public: Retrieves the buffer position of where the current word ends. + // + // * `options` (optional) {Object} with the following keys: + // * `wordRegex` A {RegExp} indicating what constitutes a "word" + // (default: {::wordRegExp}) + // * `includeNonWordCharacters` A Boolean indicating whether to include + // non-word characters in the default word regex. Has no effect if + // wordRegex is set. + // + // Returns a {Range}. + getEndOfCurrentWordBufferPosition (options = {}) { + const allowNext = options.allowNext !== false + const currentBufferPosition = this.getBufferPosition() + const scanRange = [currentBufferPosition, this.editor.getEofBufferPosition()] + + let endOfWordPosition + this.editor.scanInBufferRange(options.wordRegex || this.wordRegExp(options), scanRange, ({range, matchText, stop}) => { + // Ignore 'empty line' matches between '\r' and '\n' + if (matchText === '' && range.start.column !== 0) return + + if (range.end.isGreaterThan(currentBufferPosition)) { + if (allowNext || range.start.isLessThanOrEqual(currentBufferPosition)) { + endOfWordPosition = range.end + } + stop() + } + }) + + return endOfWordPosition || currentBufferPosition + } + + // Public: Retrieves the buffer position of where the next word starts. + // + // * `options` (optional) {Object} + // * `wordRegex` A {RegExp} indicating what constitutes a "word" + // (default: {::wordRegExp}). + // + // Returns a {Range} + getBeginningOfNextWordBufferPosition (options = {}) { + const currentBufferPosition = this.getBufferPosition() + const start = this.isInsideWord(options) ? this.getEndOfCurrentWordBufferPosition(options) : currentBufferPosition + const scanRange = [start, this.editor.getEofBufferPosition()] + + let beginningOfNextWordPosition + this.editor.scanInBufferRange(options.wordRegex || this.wordRegExp(), scanRange, ({range, stop}) => { + beginningOfNextWordPosition = range.start + stop() + }) + + return beginningOfNextWordPosition || currentBufferPosition + } + + // Public: Returns the buffer Range occupied by the word located under the cursor. + // + // * `options` (optional) {Object} + // * `wordRegex` A {RegExp} indicating what constitutes a "word" + // (default: {::wordRegExp}). + getCurrentWordBufferRange (options = {}) { + const startOptions = Object.assign(_.clone(options), {allowPrevious: false}) + const endOptions = Object.assign(_.clone(options), {allowNext: false}) + return new Range(this.getBeginningOfCurrentWordBufferPosition(startOptions), this.getEndOfCurrentWordBufferPosition(endOptions)) + } + + // Public: Returns the buffer Range for the current line. + // + // * `options` (optional) {Object} + // * `includeNewline` A {Boolean} which controls whether the Range should + // include the newline. + getCurrentLineBufferRange (options) { + return this.editor.bufferRangeForBufferRow(this.getBufferRow(), options) + } + + // Public: Retrieves the range for the current paragraph. + // + // A paragraph is defined as a block of text surrounded by empty lines or comments. + // + // Returns a {Range}. + getCurrentParagraphBufferRange () { + return this.editor.rowRangeForParagraphAtBufferRow(this.getBufferRow()) + } + + // Public: Returns the characters preceding the cursor in the current word. + getCurrentWordPrefix () { + return this.editor.getTextInBufferRange([this.getBeginningOfCurrentWordBufferPosition(), this.getBufferPosition()]) + } + + /* + Section: Visibility + */ + + /* + Section: Comparing to another cursor + */ + + // Public: Compare this cursor's buffer position to another cursor's buffer position. + // + // See {Point::compare} for more details. + // + // * `otherCursor`{Cursor} to compare against + compare (otherCursor) { + return this.getBufferPosition().compare(otherCursor.getBufferPosition()) + } + + /* + Section: Utilities + */ + + // Public: Deselects the current selection. + clearSelection (options) { + if (this.selection) this.selection.clear(options) + } + + // Public: Get the RegExp used by the cursor to determine what a "word" is. + // + // * `options` (optional) {Object} with the following keys: + // * `includeNonWordCharacters` A {Boolean} indicating whether to include + // non-word characters in the regex. (default: true) + // + // Returns a {RegExp}. + wordRegExp (options) { + const nonWordCharacters = _.escapeRegExp(this.getNonWordCharacters()) + let source = `^[\t ]*$|[^\\s${nonWordCharacters}]+` + if (!options || options.includeNonWordCharacters !== false) { + source += `|${`[${nonWordCharacters}]+`}` + } + return new RegExp(source, 'g') + } + + // Public: Get the RegExp used by the cursor to determine what a "subword" is. + // + // * `options` (optional) {Object} with the following keys: + // * `backwards` A {Boolean} indicating whether to look forwards or backwards + // for the next subword. (default: false) + // + // Returns a {RegExp}. + subwordRegExp (options = {}) { + const nonWordCharacters = this.getNonWordCharacters() + const lowercaseLetters = 'a-z\\u00DF-\\u00F6\\u00F8-\\u00FF' + const uppercaseLetters = 'A-Z\\u00C0-\\u00D6\\u00D8-\\u00DE' + const snakeCamelSegment = `[${uppercaseLetters}]?[${lowercaseLetters}]+` + const segments = [ + '^[\t ]+', + '[\t ]+$', + `[${uppercaseLetters}]+(?![${lowercaseLetters}])`, + '\\d+' + ] + if (options.backwards) { + segments.push(`${snakeCamelSegment}_*`) + segments.push(`[${_.escapeRegExp(nonWordCharacters)}]+\\s*`) + } else { + segments.push(`_*${snakeCamelSegment}`) + segments.push(`\\s*[${_.escapeRegExp(nonWordCharacters)}]+`) + } + segments.push('_+') + return new RegExp(segments.join('|'), 'g') + } + + /* + Section: Private + */ + + getNonWordCharacters () { + return this.editor.getNonWordCharacters(this.getScopeDescriptor().getScopesArray()) + } + + changePosition (options, fn) { + this.clearSelection({autoscroll: false}) + fn() + const autoscroll = (options && options.autoscroll != null) + ? options.autoscroll + : this.isLastCursor() + if (autoscroll) this.autoscroll() + } + + getScreenRange () { + const {row, column} = this.getScreenPosition() + return new Range(new Point(row, column), new Point(row, column + 1)) + } + + autoscroll (options = {}) { + options.clip = false + this.editor.scrollToScreenRange(this.getScreenRange(), options) + } + + getBeginningOfNextParagraphBufferPosition () { + const start = this.getBufferPosition() + const eof = this.editor.getEofBufferPosition() + const scanRange = [start, eof] + + const {row, column} = eof + let position = new Point(row, column - 1) + + this.editor.scanInBufferRange(EmptyLineRegExp, scanRange, ({range, stop}) => { + position = range.start.traverse(Point(1, 0)) + if (!position.isEqual(start)) stop() + }) + return position + } + + getBeginningOfPreviousParagraphBufferPosition () { + const start = this.getBufferPosition() + + const {row, column} = start + const scanRange = [[row - 1, column], [0, 0]] + let position = new Point(0, 0) + this.editor.backwardsScanInBufferRange(EmptyLineRegExp, scanRange, ({range, stop}) => { + position = range.start.traverse(Point(1, 0)) + if (!position.isEqual(start)) stop() + }) + return position + } +} From 6c4a9c1987708fa795967c5507d2500c309a8b4c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Sep 2017 15:04:21 -0700 Subject: [PATCH 68/81] Optimize getCurrentWordBufferRange --- src/cursor.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/cursor.js b/src/cursor.js index 712847bc7..37fbcb78b 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -598,9 +598,14 @@ class Cursor extends Model { // * `wordRegex` A {RegExp} indicating what constitutes a "word" // (default: {::wordRegExp}). getCurrentWordBufferRange (options = {}) { - const startOptions = Object.assign(_.clone(options), {allowPrevious: false}) - const endOptions = Object.assign(_.clone(options), {allowNext: false}) - return new Range(this.getBeginningOfCurrentWordBufferPosition(startOptions), this.getEndOfCurrentWordBufferPosition(endOptions)) + const position = this.getBufferPosition() + const ranges = this.editor.buffer.buffer.findAllInRangeSync( + options.wordRegex || this.wordRegExp(), + new Range(new Point(position.row, 0), new Point(position.row, Infinity)) + ) + return ranges.find(range => + range.end.column >= position.column && range.start.column <= position.column + ) || new Range(position, position) } // Public: Returns the buffer Range for the current line. From 43aa3c788fd10fe9879f1cf27d37d94df855d276 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 29 Sep 2017 16:50:59 -0700 Subject: [PATCH 69/81] Optimize cursor methods that find the current word --- package.json | 2 +- src/cursor.js | 64 +++++++++++++++++++++++---------------------------- 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 1fd8ca343..9726214e9 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.4.0", + "text-buffer": "13.4.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", diff --git a/src/cursor.js b/src/cursor.js index 37fbcb78b..2e300ca9d 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -514,30 +514,24 @@ class Cursor extends Model { // Returns a {Range}. getBeginningOfCurrentWordBufferPosition (options = {}) { const allowPrevious = options.allowPrevious !== false - const currentBufferPosition = this.getBufferPosition() - const previousNonBlankRow = this.editor.buffer.previousNonBlankRow(currentBufferPosition.row) || 0 - const scanRange = [[previousNonBlankRow, 0], currentBufferPosition] + const position = this.getBufferPosition() - let beginningOfWordPosition - this.editor.backwardsScanInBufferRange(options.wordRegex || this.wordRegExp(options), scanRange, ({range, matchText, stop}) => { - // Ignore 'empty line' matches between '\r' and '\n' - if ((matchText === '') && range.start.column !== 0) return + const scanRange = allowPrevious + ? new Range(new Point(position.row - 1, 0), position) + : new Range(new Point(position.row, 0), position) - if (range.start.isLessThan(currentBufferPosition)) { - if (range.end.isGreaterThanOrEqual(currentBufferPosition) || allowPrevious) { - beginningOfWordPosition = range.start - } - stop() - } - }) + const ranges = this.editor.buffer.buffer.findAllInRangeSync( + options.wordRegex || this.wordRegExp(), + scanRange + ) - if (beginningOfWordPosition) { - return beginningOfWordPosition - } else if (allowPrevious) { - return new Point(0, 0) - } else { - return currentBufferPosition + let result + for (let range of ranges) { + if (position.isLessThanOrEqual(range.start)) break + if (allowPrevious || position.isLessThanOrEqual(range.end)) result = range.start } + + return result || (allowPrevious ? new Point(0, 0) : position) } // Public: Retrieves the buffer position of where the current word ends. @@ -552,23 +546,23 @@ class Cursor extends Model { // Returns a {Range}. getEndOfCurrentWordBufferPosition (options = {}) { const allowNext = options.allowNext !== false - const currentBufferPosition = this.getBufferPosition() - const scanRange = [currentBufferPosition, this.editor.getEofBufferPosition()] + const position = this.getBufferPosition() - let endOfWordPosition - this.editor.scanInBufferRange(options.wordRegex || this.wordRegExp(options), scanRange, ({range, matchText, stop}) => { - // Ignore 'empty line' matches between '\r' and '\n' - if (matchText === '' && range.start.column !== 0) return + const scanRange = allowNext + ? new Range(position, new Point(position.row + 2, 0)) + : new Range(position, new Point(position.row, Infinity)) - if (range.end.isGreaterThan(currentBufferPosition)) { - if (allowNext || range.start.isLessThanOrEqual(currentBufferPosition)) { - endOfWordPosition = range.end - } - stop() - } - }) + const ranges = this.editor.buffer.buffer.findAllInRangeSync( + options.wordRegex || this.wordRegExp(), + scanRange + ) - return endOfWordPosition || currentBufferPosition + for (let range of ranges) { + if (position.isLessThan(range.start) && !allowNext) break + if (position.isLessThan(range.end)) return range.end + } + + return allowNext ? this.editor.getEofBufferPosition() : position } // Public: Retrieves the buffer position of where the next word starts. @@ -666,7 +660,7 @@ class Cursor extends Model { // Returns a {RegExp}. wordRegExp (options) { const nonWordCharacters = _.escapeRegExp(this.getNonWordCharacters()) - let source = `^[\t ]*$|[^\\s${nonWordCharacters}]+` + let source = `^[\t\r ]*$|[^\\s${nonWordCharacters}]+` if (!options || options.includeNonWordCharacters !== false) { source += `|${`[${nonWordCharacters}]+`}` } From 887ebd913b6e8afd05bb6dbe1ba78e2cf72372ae Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 30 Sep 2017 23:38:16 -0700 Subject: [PATCH 70/81] :arrow_up: text-buffer --- package.json | 2 +- src/cursor.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9726214e9..b08f4c6bd 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.4.2", + "text-buffer": "13.5.1", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", diff --git a/src/cursor.js b/src/cursor.js index 2e300ca9d..004921b94 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -520,7 +520,7 @@ class Cursor extends Model { ? new Range(new Point(position.row - 1, 0), position) : new Range(new Point(position.row, 0), position) - const ranges = this.editor.buffer.buffer.findAllInRangeSync( + const ranges = this.editor.buffer.findAllInRangeSync( options.wordRegex || this.wordRegExp(), scanRange ) @@ -552,7 +552,7 @@ class Cursor extends Model { ? new Range(position, new Point(position.row + 2, 0)) : new Range(position, new Point(position.row, Infinity)) - const ranges = this.editor.buffer.buffer.findAllInRangeSync( + const ranges = this.editor.buffer.findAllInRangeSync( options.wordRegex || this.wordRegExp(), scanRange ) @@ -593,7 +593,7 @@ class Cursor extends Model { // (default: {::wordRegExp}). getCurrentWordBufferRange (options = {}) { const position = this.getBufferPosition() - const ranges = this.editor.buffer.buffer.findAllInRangeSync( + const ranges = this.editor.buffer.findAllInRangeSync( options.wordRegex || this.wordRegExp(), new Range(new Point(position.row, 0), new Point(position.row, Infinity)) ) From 97e07fc59cc2ebf6c17a6a6ebf94984111a38129 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 1 Oct 2017 09:38:38 -0700 Subject: [PATCH 71/81] :arrow_up: text-buffer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b08f4c6bd..d2fa2961c 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "service-hub": "^0.7.4", "sinon": "1.17.4", "temp": "^0.8.3", - "text-buffer": "13.5.1", + "text-buffer": "13.5.2", "typescript-simple": "1.0.0", "underscore-plus": "^1.6.6", "winreg": "^1.2.1", From 8a6ef7061126ed5d51949f882363642a23ac1902 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 1 Oct 2017 09:39:51 -0700 Subject: [PATCH 72/81] Fix comment misplaced by decaffeinate --- src/cursor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cursor.js b/src/cursor.js index 004921b94..1425f5b49 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -281,8 +281,8 @@ class Cursor extends Model { while (columnCount > column && row > 0) { columnCount -= column column = this.editor.lineLengthForScreenRow(--row) - columnCount-- - } // subtract 1 for the row move + columnCount-- // subtract 1 for the row move + } column = column - columnCount this.setScreenPosition({row, column}, {clipDirection: 'backward'}) From c019eb2cf9a1f149df36996f1fd16482f68d6a75 Mon Sep 17 00:00:00 2001 From: Arnav Borborah Date: Sun, 1 Oct 2017 12:50:13 -0400 Subject: [PATCH 73/81] Shortened last three methods in color class Used the ternary operator to shorten the last three methods in color.js --- src/color.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/color.js b/src/color.js index 6208d6837..2f2947e16 100644 --- a/src/color.js +++ b/src/color.js @@ -112,27 +112,15 @@ export default class Color { function parseColor (colorString) { const color = parseInt(colorString, 10) - if (isNaN(color)) { - return 0 - } else { - return Math.min(Math.max(color, 0), 255) - } + return isNaN(color) ? 0 : Math.min(Math.max(color, 0), 255) } function parseAlpha (alphaString) { const alpha = parseFloat(alphaString) - if (isNaN(alpha)) { - return 1 - } else { - return Math.min(Math.max(alpha, 0), 1) - } + return isNaN(alpha) ? 1 : Math.min(Math.max(alpha, 0), 1) } function numberToHexString (number) { const hex = number.toString(16) - if (number < 16) { - return `0${hex}` - } else { - return hex - } + return number < 16 ? `0${hex}` : hex } From 9444ce6ac507ff1f299cffb68979225f8d6d22a6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 2 Oct 2017 13:15:06 -0700 Subject: [PATCH 74/81] :arrow_up: encoding-selector --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6c908cf70..12d9e5d5b 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "dalek": "0.2.1", "deprecation-cop": "0.56.9", "dev-live-reload": "0.47.1", - "encoding-selector": "0.23.6", + "encoding-selector": "0.23.7", "exception-reporting": "0.41.4", "find-and-replace": "0.212.3", "fuzzy-finder": "1.6.1", From 3a3c58e04e37f3fe653d742134a0067bc8adb8dc Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Mon, 2 Oct 2017 15:00:01 -0600 Subject: [PATCH 75/81] :arrow_up: autocomplete-plus@2.36.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 12d9e5d5b..d3612e972 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", - "autocomplete-plus": "2.36.0", + "autocomplete-plus": "2.36.1", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.6", From 293b52d797208568f527c6ee97ddd3a2f4a20e12 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 3 Oct 2017 10:32:29 -0600 Subject: [PATCH 76/81] Fix rendering bug when folds hide the vertical scrollbar w/ soft wrap on --- spec/text-editor-component-spec.js | 25 +++++++++++++++++++++++++ src/text-editor-component.js | 26 ++++++++++++++++++++------ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 82764c438..fa72e42ef 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -286,6 +286,31 @@ describe('TextEditorComponent', () => { expect(lineNumberNodeForScreenRow(component, 0).querySelector('.foldable')).toBeNull() }) + it('gracefully handles folds that change the soft-wrap boundary by causing the vertical scrollbar to disappear (regression)', async () => { + const text = ('x'.repeat(100) + '\n') + 'y\n'.repeat(28) + ' z\n'.repeat(50) + const {component, element, editor} = buildComponent({text, height: 1000, width: 500}) + + element.addEventListener('scroll', (event) => { + event.stopPropagation() + }, true) + + editor.setSoftWrapped(true) + jasmine.attachToDOM(element) + await component.getNextUpdatePromise() + + const firstScreenLineLengthWithVerticalScrollbar = element.querySelector('.line').textContent.length + + setScrollTop(component, 620) + await component.getNextUpdatePromise() + + editor.foldBufferRow(28) + await component.getNextUpdatePromise() + + const firstLineElement = element.querySelector('.line') + expect(firstLineElement.dataset.screenRow).toBe('0') + expect(firstLineElement.textContent.length).toBeGreaterThan(firstScreenLineLengthWithVerticalScrollbar) + }) + it('shows the foldable icon on the last screen row of a buffer row that can be folded', async () => { const {component, element, editor} = buildComponent({text: 'abc\n de\nfghijklm\n no', softWrapped: true}) await setEditorWidthInCharacters(component, 5) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 5c3b6d1bc..3060b6857 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -362,7 +362,7 @@ class TextEditorComponent { this.requestHorizontalMeasurement(screenRange.start.row, screenRange.start.column) this.requestHorizontalMeasurement(screenRange.end.row, screenRange.end.column) } - this.populateVisibleRowRange() + this.populateVisibleRowRange(this.getRenderedStartRow()) this.populateVisibleTiles() this.queryScreenLinesToRender() this.queryLongestLine() @@ -2096,14 +2096,29 @@ class TextEditorComponent { return marginInBaseCharacters * this.getBaseCharacterWidth() } + // This method is called at the beginning of a frame render to relay any + // potential changes in the editor's width into the model before proceeding. updateModelSoftWrapColumn () { const {model} = this.props const newEditorWidthInChars = this.getScrollContainerClientWidthInBaseCharacters() if (newEditorWidthInChars !== model.getEditorWidthInChars()) { this.suppressUpdates = true + + const renderedStartRow = this.getRenderedStartRow() this.props.model.setEditorWidthInChars(newEditorWidthInChars) - // Wrapping may cause a vertical scrollbar to appear, which will change the width again. + + // Relaying a change in to the editor's client width may cause the + // vertical scrollbar to appear or disappear, which causes the editor's + // client width to change *again*. Make sure the display layer is fully + // populated for the visible area before recalculating the editor's + // width in characters. Then update the display layer *again* just in + // case a change in scrollbar visibility causes lines to wrap + // differently. We capture the renderedStartRow before resetting the + // display layer because once it has been reset, we can't compute the + // rendered start row accurately. 😥 + this.populateVisibleRowRange(renderedStartRow) this.props.model.setEditorWidthInChars(this.getScrollContainerClientWidthInBaseCharacters()) + this.suppressUpdates = false } } @@ -2867,12 +2882,11 @@ class TextEditorComponent { } } - // Ensure the spatial index is populated with rows that are currently - // visible so we *at least* get the longest row in the visible range. - populateVisibleRowRange () { + // Ensure the spatial index is populated with rows that are currently visible + populateVisibleRowRange (renderedStartRow) { const editorHeightInTiles = this.getScrollContainerHeight() / this.getLineHeight() const visibleTileCount = Math.ceil(editorHeightInTiles) + 1 - const lastRenderedRow = this.getRenderedStartRow() + (visibleTileCount * this.getRowsPerTile()) + const lastRenderedRow = renderedStartRow + (visibleTileCount * this.getRowsPerTile()) this.props.model.displayLayer.populateSpatialIndexIfNeeded(Infinity, lastRenderedRow) } From 44d6868855c16f8d608ea321c907347c3d9f1936 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 Oct 2017 10:14:45 -0700 Subject: [PATCH 77/81] Preserve indentation when toggling comments on whitespace-only lines --- spec/tokenized-buffer-spec.js | 2 +- src/tokenized-buffer.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/tokenized-buffer-spec.js b/spec/tokenized-buffer-spec.js index b2324d392..ba43f9ff3 100644 --- a/spec/tokenized-buffer-spec.js +++ b/spec/tokenized-buffer-spec.js @@ -807,7 +807,7 @@ describe('TokenizedBuffer', () => { buffer.setText(' ') tokenizedBuffer.toggleLineCommentsForBufferRows(0, 0) - expect(buffer.lineForRow(0)).toBe('// ') + expect(buffer.lineForRow(0)).toBe(' // ') buffer.setText(' a\n \n b') tokenizedBuffer.toggleLineCommentsForBufferRows(0, 2) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index 1d52411ae..8b7569cca 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -222,13 +222,18 @@ class TokenizedBuffer { } } else { let minIndentLevel = null + let minBlankIndentLevel for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row) if (NON_WHITESPACE_REGEX.test(line)) { const indentLevel = this.indentLevelForLine(line) if (minIndentLevel == null || indentLevel < minIndentLevel) minIndentLevel = indentLevel + } else if (minIndentLevel == null) { + const indentLevel = this.indentLevelForLine(line) + if (minBlankIndentLevel == null || indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel } } + if (minIndentLevel == null) minIndentLevel = minBlankIndentLevel if (minIndentLevel == null) minIndentLevel = 0 const tabLength = this.getTabLength() From 96d8b3db55ba7ffc1ef502f67ed4e34a2377782a Mon Sep 17 00:00:00 2001 From: Justin Ratner Date: Tue, 3 Oct 2017 11:39:34 -0600 Subject: [PATCH 78/81] :arrow_up: autocomplete-plus@2.36.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d3612e972..70c3f2f34 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "autocomplete-atom-api": "0.10.3", "autocomplete-css": "0.17.3", "autocomplete-html": "0.8.2", - "autocomplete-plus": "2.36.1", + "autocomplete-plus": "2.36.2", "autocomplete-snippets": "1.11.1", "autoflow": "0.29.0", "autosave": "0.24.6", From 4d057a16d6d28cd17fccbf42ff0c5293e0e4e4a1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 Oct 2017 10:01:53 -0700 Subject: [PATCH 79/81] Prompt to save when unloading if editor is in conflict --- spec/text-editor-spec.coffee | 31 ---------------------- spec/text-editor-spec.js | 50 ++++++++++++++++++++++++++++++++++++ src/text-editor.coffee | 2 +- 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index efe3bf048..53011fdcc 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5324,37 +5324,6 @@ describe "TextEditor", -> [[6, 3], [6, 4]], ]) - describe ".shouldPromptToSave()", -> - it "returns true when buffer changed", -> - jasmine.unspy(editor, 'shouldPromptToSave') - expect(editor.shouldPromptToSave()).toBeFalsy() - buffer.setText('changed') - expect(editor.shouldPromptToSave()).toBeTruthy() - - it "returns false when an edit session's buffer is in use by more than one session", -> - jasmine.unspy(editor, 'shouldPromptToSave') - buffer.setText('changed') - - editor2 = null - waitsForPromise -> - atom.workspace.getActivePane().splitRight() - atom.workspace.open('sample.js', autoIndent: false).then (o) -> editor2 = o - - runs -> - expect(editor.shouldPromptToSave()).toBeFalsy() - editor2.destroy() - expect(editor.shouldPromptToSave()).toBeTruthy() - - it "returns false when close of a window requested and edit session opened inside project", -> - jasmine.unspy(editor, 'shouldPromptToSave') - buffer.setText('changed') - expect(editor.shouldPromptToSave(windowCloseRequested: true, projectHasPaths: true)).toBeFalsy() - - it "returns true when close of a window requested and edit session opened without project", -> - jasmine.unspy(editor, 'shouldPromptToSave') - buffer.setText('changed') - expect(editor.shouldPromptToSave(windowCloseRequested: true, projectHasPaths: false)).toBeTruthy() - describe "when the editor contains surrogate pair characters", -> it "correctly backspaces over them", -> editor.setText('\uD835\uDF97\uD835\uDF97\uD835\uDF97') diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js index 82ad3bc90..c81df8089 100644 --- a/spec/text-editor-spec.js +++ b/spec/text-editor-spec.js @@ -1,3 +1,5 @@ +const fs = require('fs') +const temp = require('temp').track() const {Point, Range} = require('text-buffer') const {it, fit, ffit, fffit, beforeEach, afterEach} = require('./async-spec-helpers') @@ -8,6 +10,54 @@ describe('TextEditor', () => { editor.destroy() }) + describe('.shouldPromptToSave()', () => { + beforeEach(async () => { + editor = await atom.workspace.open('sample.js') + jasmine.unspy(editor, 'shouldPromptToSave') + }) + + it('returns true when buffer has unsaved changes', () => { + expect(editor.shouldPromptToSave()).toBeFalsy() + editor.setText('changed') + expect(editor.shouldPromptToSave()).toBeTruthy() + }) + + it("returns false when an editor's buffer is in use by more than one buffer", async () => { + editor.setText('changed') + + atom.workspace.getActivePane().splitRight() + const editor2 = await atom.workspace.open('sample.js', {autoIndent: false}) + expect(editor.shouldPromptToSave()).toBeFalsy() + + editor2.destroy() + expect(editor.shouldPromptToSave()).toBeTruthy() + }) + + it('returns true when the window is closing if the file has changed on disk', async () => { + jasmine.useRealClock() + + editor.setText('initial stuff') + await editor.saveAs(temp.openSync('test-file').path) + + editor.setText('other stuff') + fs.writeFileSync(editor.getPath(), 'new stuff') + expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: true})).toBeFalsy() + + await new Promise(resolve => editor.onDidConflict(resolve)) + expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: true})).toBeTruthy() + }) + + it('returns false when the window is closing and the project has one or more directory paths', () => { + editor.setText('changed') + expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: true})).toBeFalsy() + }) + + it('returns false when the window is closing and the project has no directory paths', () => { + editor.setText('changed') + expect(editor.shouldPromptToSave({windowCloseRequested: true, projectHasPaths: false})).toBeTruthy() + }) + }) + describe('folding', () => { beforeEach(async () => { await atom.packages.activatePackage('language-javascript') diff --git a/src/text-editor.coffee b/src/text-editor.coffee index c9813e445..c00508f09 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -961,7 +961,7 @@ class TextEditor extends Model # this editor. shouldPromptToSave: ({windowCloseRequested, projectHasPaths}={}) -> if windowCloseRequested and projectHasPaths and atom.stateStore.isConnected() - false + @buffer.isInConflict() else @isModified() and not @buffer.hasMultipleEditors() From 4975f659c0932a96f931acf063655f1bdb2861ee Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 Oct 2017 11:03:37 -0700 Subject: [PATCH 80/81] :art: toggleLineCommentsForBufferRows --- src/tokenized-buffer.js | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index 8b7569cca..4eed4110a 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -199,17 +199,20 @@ class TokenizedBuffer { }) } } else { - let allBlank = true - let allBlankOrCommented = true - + let hasCommentedLines = false + let hasUncommentedLines = false for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row) - const blank = line.match(/^\s*$/) - if (!blank) allBlank = false - if (!blank && !commentStartRegex.testSync(line)) allBlankOrCommented = false + if (NON_WHITESPACE_REGEX.test(line)) { + if (commentStartRegex.testSync(line)) { + hasCommentedLines = true + } else { + hasUncommentedLines = true + } + } } - const shouldUncomment = allBlankOrCommented && !allBlank + const shouldUncomment = hasCommentedLines && !hasUncommentedLines if (shouldUncomment) { for (let row = start; row <= end; row++) { @@ -221,20 +224,22 @@ class TokenizedBuffer { } } } else { - let minIndentLevel = null - let minBlankIndentLevel + let minIndentLevel = Infinity + let minBlankIndentLevel = Infinity for (let row = start; row <= end; row++) { const line = this.buffer.lineForRow(row) + const indentLevel = this.indentLevelForLine(line) if (NON_WHITESPACE_REGEX.test(line)) { - const indentLevel = this.indentLevelForLine(line) - if (minIndentLevel == null || indentLevel < minIndentLevel) minIndentLevel = indentLevel - } else if (minIndentLevel == null) { - const indentLevel = this.indentLevelForLine(line) - if (minBlankIndentLevel == null || indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel + if (indentLevel < minIndentLevel) minIndentLevel = indentLevel + } else { + if (indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel } } - if (minIndentLevel == null) minIndentLevel = minBlankIndentLevel - if (minIndentLevel == null) minIndentLevel = 0 + minIndentLevel = Number.isFinite(minIndentLevel) + ? minIndentLevel + : Number.isFinite(minBlankIndentLevel) + ? minBlankIndentLevel + : 0 const tabLength = this.getTabLength() const indentString = ' '.repeat(tabLength * minIndentLevel) From c9c495792188693ca234e4ac08efcb83b0036cf7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 Oct 2017 12:31:33 -0700 Subject: [PATCH 81/81] Avoid unnecessary work in TokenizedBuffer.isFoldableAtRow --- src/tokenized-buffer.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/tokenized-buffer.js b/src/tokenized-buffer.js index 4eed4110a..b4bc0d41c 100644 --- a/src/tokenized-buffer.js +++ b/src/tokenized-buffer.js @@ -471,7 +471,7 @@ class TokenizedBuffer { } isFoldableAtRow (row) { - return this.endRowForFoldAtRow(row, 1) != null + return this.endRowForFoldAtRow(row, 1, true) != null } buildTokenizedLinesForRows (startRow, endRow, startingStack, startingopenScopes) { @@ -773,27 +773,28 @@ class TokenizedBuffer { return result } - endRowForFoldAtRow (row, tabLength) { + endRowForFoldAtRow (row, tabLength, existenceOnly = false) { if (this.isRowCommented(row)) { - return this.endRowForCommentFoldAtRow(row) + return this.endRowForCommentFoldAtRow(row, existenceOnly) } else { - return this.endRowForCodeFoldAtRow(row, tabLength) + return this.endRowForCodeFoldAtRow(row, tabLength, existenceOnly) } } - endRowForCommentFoldAtRow (row) { + endRowForCommentFoldAtRow (row, existenceOnly) { if (this.isRowCommented(row - 1)) return let endRow for (let nextRow = row + 1, end = this.buffer.getLineCount(); nextRow < end; nextRow++) { if (!this.isRowCommented(nextRow)) break endRow = nextRow + if (existenceOnly) break } return endRow } - endRowForCodeFoldAtRow (row, tabLength) { + endRowForCodeFoldAtRow (row, tabLength, existenceOnly) { let foldEndRow const line = this.buffer.lineForRow(row) if (!NON_WHITESPACE_REGEX.test(line)) return @@ -811,6 +812,7 @@ class TokenizedBuffer { break } foldEndRow = nextRow + if (existenceOnly) break } return foldEndRow }