Merge remote-tracking branch 'origin/master' into mkt-url-based-command-dispatch

This commit is contained in:
Michelle Tilley
2017-10-03 13:16:02 -07:00
33 changed files with 5275 additions and 4812 deletions

View File

@@ -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
}

View File

@@ -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.languageMode.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

753
src/cursor.js Normal file
View File

@@ -0,0 +1,753 @@
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 position = this.getBufferPosition()
const scanRange = allowPrevious
? new Range(new Point(position.row - 1, 0), position)
: new Range(new Point(position.row, 0), position)
const ranges = this.editor.buffer.findAllInRangeSync(
options.wordRegex || this.wordRegExp(),
scanRange
)
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.
//
// * `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 position = this.getBufferPosition()
const scanRange = allowNext
? new Range(position, new Point(position.row + 2, 0))
: new Range(position, new Point(position.row, Infinity))
const ranges = this.editor.buffer.findAllInRangeSync(
options.wordRegex || this.wordRegExp(),
scanRange
)
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.
//
// * `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 position = this.getBufferPosition()
const ranges = this.editor.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.
//
// * `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\r ]*$|[^\\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
}
}

View File

@@ -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 -- <path>
# git checkout HEAD -- <path>
# ```
#
# * `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()

603
src/git-repository.js Normal file
View File

@@ -0,0 +1,603 @@
/*
* 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')
let nextId = 0
// 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.id = nextId++
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.statusRefreshCount = 0
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()
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 () {
this.repo = null
if (this.emitter) {
this.emitter.emit('did-destroy')
this.emitter.dispose()
this.emitter = null
}
if (this.subscriptions) {
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 -- <path>
// git checkout HEAD -- <path>
// ```
//
// * `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.
async refreshStatus () {
const statusRefreshCount = ++this.statusRefreshCount
const repo = this.getRepo()
const relativeProjectPaths = this.project && this.project.getPaths()
.map(projectPath => this.relativize(projectPath))
.filter(projectPath => (projectPath.length > 0) && !path.isAbsolute(projectPath))
const branch = await repo.getHeadAsync()
const upstream = await repo.getAheadBehindCountAsync()
const statuses = {}
const repoStatus = relativeProjectPaths.length > 0
? await repo.getStatusAsync(relativeProjectPaths)
: await repo.getStatusAsync()
for (let filePath in repoStatus) {
statuses[filePath] = repoStatus[filePath]
}
const submodules = {}
for (let submodulePath in repo.submodules) {
const submoduleRepo = repo.submodules[submodulePath]
submodules[submodulePath] = {
branch: await submoduleRepo.getHeadAsync(),
upstream: await submoduleRepo.getAheadBehindCountAsync()
}
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 (this.statusRefreshCount !== statusRefreshCount || this.isDestroyed()) return
const statusesUnchanged =
_.isEqual(branch, this.branch) &&
_.isEqual(statuses, this.statuses) &&
_.isEqual(upstream, this.upstream) &&
_.isEqual(submodules, this.submodules)
this.branch = branch
this.statuses = statuses
this.upstream = upstream
this.submodules = submodules
for (let submodulePath in repo.submodules) {
repo.submodules[submodulePath].upstream = submodules[submodulePath].upstream
}
if (!statusesUnchanged) this.emitter.emit('did-change-statuses')
}
}

View File

@@ -1,350 +0,0 @@
{Range} = 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 = {}
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
# 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})")
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]
startRow = bufferRow
while startRow > firstRow
break if @isLineCommentedAtBufferRow(startRow - 1) isnt isOriginalRowComment
break unless /\S/.test(filterCommentStart(@editor.lineTextForBufferRow(startRow - 1)))
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)))
endRow++
new Range([startRow, 0], [endRow, @editor.lineTextForBufferRow(endRow).length])
# 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.
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.
#
# 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))
foldEndRegexForScopeDescriptor: (scopeDescriptor) ->
@cacheRegex(@editor.getFoldEndPattern(scopeDescriptor))

View File

@@ -534,7 +534,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()

View File

@@ -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)
@@ -119,6 +126,7 @@ class PaneElement extends HTMLElement
paneDestroyed: ->
@subscriptions.dispose()
@changePathDisposable?.dispose()
flexScaleChanged: (flexScale) ->
@style.flexGrow = flexScale

View File

@@ -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}

View File

@@ -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)

View File

@@ -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()
@@ -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
@@ -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)
}

View File

@@ -429,3 +429,5 @@ class ScopedSettingsDelegate {
}
}
}
TextEditorRegistry.ScopedSettingsDelegate = ScopedSettingsDelegate

View File

@@ -4,7 +4,6 @@ fs = require 'fs-plus'
Grim = require 'grim'
{CompositeDisposable, Disposable, Emitter} = require 'event-kit'
{Point, Range} = TextBuffer = require 'text-buffer'
LanguageMode = require './language-mode'
DecorationManager = require './decoration-manager'
TokenizedBuffer = require './tokenized-buffer'
Cursor = require './cursor'
@@ -16,6 +15,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
@@ -78,7 +78,6 @@ class TextEditor extends Model
serializationVersion: 1
buffer: null
languageMode: null
cursors: null
showCursorOnSelection: null
selections: null
@@ -122,6 +121,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?
@@ -243,8 +244,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'
@@ -482,7 +481,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()
@@ -963,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()
@@ -2210,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
@@ -3311,13 +3309,15 @@ 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)
{row} = @getCursorBufferPosition()
position = Point(row, Infinity)
@displayLayer.destroyFoldsIntersectingBufferRange(Range(position, position))
# Essential: Fold the given row in buffer coordinates based on its indentation
# level.
@@ -3327,13 +3327,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 +3355,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 +3567,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.
@@ -3603,18 +3624,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
###
@@ -3850,14 +3859,51 @@ 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) if indentLevel?
toggleLineCommentForBufferRow: (row) -> @languageMode.toggleLineCommentsForBufferRow(row)
toggleLineCommentForBufferRow: (row) -> @toggleLineCommentsForBufferRows(row, row)
toggleLineCommentsForBufferRows: (start, end) -> @languageMode.toggleLineCommentsForBufferRows(start, end)
toggleLineCommentsForBufferRows: (start, end) -> @tokenizedBuffer.toggleLineCommentsForBufferRows(start, end)
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)))

View File

@@ -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)

875
src/tokenized-buffer.js Normal file
View File

@@ -0,0 +1,875 @@
const _ = require('underscore-plus')
const {CompositeDisposable, Emitter} = require('event-kit')
const {Point, Range} = require('text-buffer')
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 {OnigRegExp} = require('oniguruma')
const {toFirstMateScopeId} = require('./first-mate-helpers')
const NON_WHITESPACE_REGEX = /\S/
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.regexesByPattern = {}
this.alive = true
this.visible = false
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.scopedSettingsDelegate = params.scopedSettingsDelegate
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
}
/*
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. It may also return undefined if no change should
// be made.
//
// * bufferRow - The row {Number}
//
// Returns a {Number}.
suggestedIndentForEditedBufferRow (bufferRow) {
const line = this.buffer.lineForRow(bufferRow)
const currentIndentLevel = this.indentLevelForLine(line)
if (currentIndentLevel === 0) return
const scopeDescriptor = this.scopeDescriptorForPosition([bufferRow, 0])
const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
if (!decreaseIndentRegex) return
if (!decreaseIndentRegex.testSync(line)) return
const precedingRow = this.buffer.previousNonBlankRow(bufferRow)
if (precedingRow == null) return
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
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)
}
/*
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 hasCommentedLines = false
let hasUncommentedLines = false
for (let row = start; row <= end; row++) {
const line = this.buffer.lineForRow(row)
if (NON_WHITESPACE_REGEX.test(line)) {
if (commentStartRegex.testSync(line)) {
hasCommentedLines = true
} else {
hasUncommentedLines = true
}
}
}
const shouldUncomment = hasCommentedLines && !hasUncommentedLines
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 {
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)) {
if (indentLevel < minIndentLevel) minIndentLevel = indentLevel
} else {
if (indentLevel < minBlankIndentLevel) minBlankIndentLevel = indentLevel
}
}
minIndentLevel = Number.isFinite(minIndentLevel)
? minIndentLevel
: Number.isFinite(minBlankIndentLevel)
? minBlankIndentLevel
: 0
const tabLength = this.getTabLength()
const indentString = ' '.repeat(tabLength * minIndentLevel)
for (let row = start; row <= end; row++) {
const line = this.buffer.lineForRow(row)
if (NON_WHITESPACE_REGEX.test(line)) {
const indentColumn = this.columnForIndentLevel(line, minIndentLevel)
this.buffer.insert(Point(row, indentColumn), commentStartString)
} else {
this.buffer.setTextInRange(
new Range(new Point(row, 0), new Point(row, Infinity)),
indentString + commentStartString
)
}
}
}
}
}
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.buffer.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.endRowForFoldAtRow(row, 1, true) != null
}
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)
]
this.tokenizedLines[bufferRow] = new TokenizedLine({
openScopes: [],
text,
tags,
lineEnding,
tokenIterator: this.tokenIterator,
grammar: this.grammar
})
return this.tokenizedLines[bufferRow]
}
}
}
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())}\``
error.privateMetadata = {
filePath: this.buffer.getPath(),
fileContents: this.buffer.getText()
}
})
break
}
}
}
}
}
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++) {
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))
}
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, existenceOnly = false) {
if (this.isRowCommented(row)) {
return this.endRowForCommentFoldAtRow(row, existenceOnly)
} else {
return this.endRowForCodeFoldAtRow(row, tabLength, existenceOnly)
}
}
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, existenceOnly) {
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
if (existenceOnly) break
}
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))
}
}
commentStringsForScopeDescriptor (scopes) {
if (this.scopedSettingsDelegate) {
return this.scopedSettingsDelegate.getCommentStrings(scopes)
}
}
regexForPattern (pattern) {
if (pattern) {
if (!this.regexesByPattern[pattern]) {
this.regexesByPattern[pattern] = new OnigRegExp(pattern)
}
return this.regexesByPattern[pattern]
}
}
logLines (start = 0, end = this.buffer.getLastRow()) {
for (let row = start; row <= end; row++) {
const line = this.tokenizedLines[row].text
console.log(row, line, line.length)
}
}
}
module.exports.prototype.chunkSize = 50
function selectorMatchesAnyScope (selector, scopes) {
const targetClasses = selector.replace(/^\./, '').split('.')
return scopes.some((scope) => {
const scopeClasses = scope.split('.')
return _.isSubset(targetClasses, scopeClasses)
})
}