mirror of
https://github.com/atom/atom.git
synced 2026-02-08 13:45:09 -05:00
Merge remote-tracking branch 'origin/master' into mkt-core-uri-handlers
This commit is contained in:
@@ -43,7 +43,6 @@ PaneContainer = require './pane-container'
|
||||
PaneAxis = require './pane-axis'
|
||||
Pane = require './pane'
|
||||
Dock = require './dock'
|
||||
Project = require './project'
|
||||
TextEditor = require './text-editor'
|
||||
TextBuffer = require 'text-buffer'
|
||||
Gutter = require './gutter'
|
||||
|
||||
@@ -89,7 +89,7 @@ module.exports = class CommandRegistry {
|
||||
// DOM element, the command will be associated with just that element.
|
||||
// * `commandName` A {String} containing the name of a command you want to
|
||||
// handle such as `user:insert-date`.
|
||||
// * `listener` A listener which handles the event. Either A {Function} to
|
||||
// * `listener` A listener which handles the event. Either a {Function} to
|
||||
// call when the given command is invoked on an element matching the
|
||||
// selector, or an {Object} with a `didDispatch` property which is such a
|
||||
// function.
|
||||
@@ -97,7 +97,7 @@ module.exports = class CommandRegistry {
|
||||
// The function (`listener` itself if it is a function, or the `didDispatch`
|
||||
// method if `listener` is an object) will be called with `this` referencing
|
||||
// the matching DOM node and the following argument:
|
||||
// * `event` A standard DOM event instance. Call `stopPropagation` or
|
||||
// * `event`: A standard DOM event instance. Call `stopPropagation` or
|
||||
// `stopImmediatePropagation` to terminate bubbling early.
|
||||
//
|
||||
// Additionally, `listener` may have additional properties which are returned
|
||||
@@ -107,6 +107,13 @@ module.exports = class CommandRegistry {
|
||||
// otherwise be generated from the event name.
|
||||
// * `description`: Used by consumers to display detailed information about
|
||||
// the command.
|
||||
// * `hiddenInCommandPalette`: If `true`, this command will not appear in
|
||||
// the bundled command palette by default, but can still be shown with.
|
||||
// the `Command Palette: Show Hidden Commands` command. This is a good
|
||||
// option when you need to register large numbers of commands that don't
|
||||
// make sense to be executed from the command palette. Please use this
|
||||
// option conservatively, as it could reduce the discoverability of your
|
||||
// package's commands.
|
||||
//
|
||||
// ## Arguments: Registering Multiple Commands
|
||||
//
|
||||
|
||||
@@ -594,7 +594,7 @@ class Cursor extends Model {
|
||||
getCurrentWordBufferRange (options = {}) {
|
||||
const position = this.getBufferPosition()
|
||||
const ranges = this.editor.buffer.findAllInRangeSync(
|
||||
options.wordRegex || this.wordRegExp(),
|
||||
options.wordRegex || this.wordRegExp(options),
|
||||
new Range(new Point(position.row, 0), new Point(position.row, Infinity))
|
||||
)
|
||||
const range = ranges.find(range =>
|
||||
|
||||
@@ -58,10 +58,10 @@ class GrammarRegistry extends FirstMate.GrammarRegistry {
|
||||
|
||||
let score = this.getGrammarPathScore(grammar, filePath)
|
||||
if ((score > 0) && !grammar.bundledPackage) {
|
||||
score += 0.25
|
||||
score += 0.125
|
||||
}
|
||||
if (this.grammarMatchesContents(grammar, contents)) {
|
||||
score += 0.125
|
||||
score += 0.25
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
|
||||
const nsfw = require('nsfw')
|
||||
const nsfw = require('@atom/nsfw')
|
||||
const {NativeWatcherRegistry} = require('./native-watcher-registry')
|
||||
|
||||
// Private: Associate native watcher action flags with descriptive String equivalents.
|
||||
|
||||
@@ -174,6 +174,11 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
|
||||
'core:cut': -> @cutSelectedText()
|
||||
'core:copy': -> @copySelectedText()
|
||||
'core:paste': -> @pasteText()
|
||||
'editor:paste-without-reformatting': -> @pasteText({
|
||||
normalizeLineEndings: false,
|
||||
autoIndent: false,
|
||||
preserveTrailingLineIndentation: true
|
||||
})
|
||||
'editor:delete-to-previous-word-boundary': -> @deleteToPreviousWordBoundary()
|
||||
'editor:delete-to-next-word-boundary': -> @deleteToNextWordBoundary()
|
||||
'editor:delete-to-beginning-of-word': -> @deleteToBeginningOfWord()
|
||||
|
||||
@@ -1,834 +0,0 @@
|
||||
{Point, Range} = require 'text-buffer'
|
||||
{pick} = require 'underscore-plus'
|
||||
{Emitter} = require 'event-kit'
|
||||
Model = require './model'
|
||||
|
||||
NonWhitespaceRegExp = /\S/
|
||||
|
||||
# Extended: Represents a selection in the {TextEditor}.
|
||||
module.exports =
|
||||
class Selection extends Model
|
||||
cursor: null
|
||||
marker: null
|
||||
editor: null
|
||||
initialScreenRange: null
|
||||
wordwise: false
|
||||
|
||||
constructor: ({@cursor, @marker, @editor, id}) ->
|
||||
@emitter = new Emitter
|
||||
|
||||
@assignId(id)
|
||||
@cursor.selection = this
|
||||
@decoration = @editor.decorateMarker(@marker, type: 'highlight', class: 'selection')
|
||||
|
||||
@marker.onDidChange (e) => @markerDidChange(e)
|
||||
@marker.onDidDestroy => @markerDidDestroy()
|
||||
|
||||
destroy: ->
|
||||
@marker.destroy()
|
||||
|
||||
isLastSelection: ->
|
||||
this is @editor.getLastSelection()
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Extended: Calls your `callback` when the selection was moved.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `event` {Object}
|
||||
# * `oldBufferRange` {Range}
|
||||
# * `oldScreenRange` {Range}
|
||||
# * `newBufferRange` {Range}
|
||||
# * `newScreenRange` {Range}
|
||||
# * `selection` {Selection} that triggered the event
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeRange: (callback) ->
|
||||
@emitter.on 'did-change-range', callback
|
||||
|
||||
# Extended: Calls your `callback` when the selection was destroyed
|
||||
#
|
||||
# * `callback` {Function}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.once 'did-destroy', callback
|
||||
|
||||
###
|
||||
Section: Managing the selection range
|
||||
###
|
||||
|
||||
# Public: Returns the screen {Range} for the selection.
|
||||
getScreenRange: ->
|
||||
@marker.getScreenRange()
|
||||
|
||||
# Public: Modifies the screen range for the selection.
|
||||
#
|
||||
# * `screenRange` The new {Range} to use.
|
||||
# * `options` (optional) {Object} options matching those found in {::setBufferRange}.
|
||||
setScreenRange: (screenRange, options) ->
|
||||
@setBufferRange(@editor.bufferRangeForScreenRange(screenRange), options)
|
||||
|
||||
# Public: Returns the buffer {Range} for the selection.
|
||||
getBufferRange: ->
|
||||
@marker.getBufferRange()
|
||||
|
||||
# Public: Modifies the buffer {Range} for the selection.
|
||||
#
|
||||
# * `bufferRange` The new {Range} to select.
|
||||
# * `options` (optional) {Object} with the keys:
|
||||
# * `preserveFolds` if `true`, the fold settings are preserved after the
|
||||
# selection moves.
|
||||
# * `autoscroll` {Boolean} indicating whether to autoscroll to the new
|
||||
# range. Defaults to `true` if this is the most recently added selection,
|
||||
# `false` otherwise.
|
||||
setBufferRange: (bufferRange, options={}) ->
|
||||
bufferRange = Range.fromObject(bufferRange)
|
||||
options.reversed ?= @isReversed()
|
||||
@editor.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds
|
||||
@modifySelection =>
|
||||
needsFlash = options.flash
|
||||
delete options.flash if options.flash?
|
||||
@marker.setBufferRange(bufferRange, options)
|
||||
@autoscroll() if options?.autoscroll ? @isLastSelection()
|
||||
@decoration.flash('flash', @editor.selectionFlashDuration) if needsFlash
|
||||
|
||||
# Public: Returns the starting and ending buffer rows the selection is
|
||||
# highlighting.
|
||||
#
|
||||
# Returns an {Array} of two {Number}s: the starting row, and the ending row.
|
||||
getBufferRowRange: ->
|
||||
range = @getBufferRange()
|
||||
start = range.start.row
|
||||
end = range.end.row
|
||||
end = Math.max(start, end - 1) if range.end.column is 0
|
||||
[start, end]
|
||||
|
||||
getTailScreenPosition: ->
|
||||
@marker.getTailScreenPosition()
|
||||
|
||||
getTailBufferPosition: ->
|
||||
@marker.getTailBufferPosition()
|
||||
|
||||
getHeadScreenPosition: ->
|
||||
@marker.getHeadScreenPosition()
|
||||
|
||||
getHeadBufferPosition: ->
|
||||
@marker.getHeadBufferPosition()
|
||||
|
||||
###
|
||||
Section: Info about the selection
|
||||
###
|
||||
|
||||
# Public: Determines if the selection contains anything.
|
||||
isEmpty: ->
|
||||
@getBufferRange().isEmpty()
|
||||
|
||||
# Public: Determines if the ending position of a marker is greater than the
|
||||
# starting position.
|
||||
#
|
||||
# This can happen when, for example, you highlight text "up" in a {TextBuffer}.
|
||||
isReversed: ->
|
||||
@marker.isReversed()
|
||||
|
||||
# Public: Returns whether the selection is a single line or not.
|
||||
isSingleScreenLine: ->
|
||||
@getScreenRange().isSingleLine()
|
||||
|
||||
# Public: Returns the text in the selection.
|
||||
getText: ->
|
||||
@editor.buffer.getTextInRange(@getBufferRange())
|
||||
|
||||
# Public: Identifies if a selection intersects with a given buffer range.
|
||||
#
|
||||
# * `bufferRange` A {Range} to check against.
|
||||
#
|
||||
# Returns a {Boolean}
|
||||
intersectsBufferRange: (bufferRange) ->
|
||||
@getBufferRange().intersectsWith(bufferRange)
|
||||
|
||||
intersectsScreenRowRange: (startRow, endRow) ->
|
||||
@getScreenRange().intersectsRowRange(startRow, endRow)
|
||||
|
||||
intersectsScreenRow: (screenRow) ->
|
||||
@getScreenRange().intersectsRow(screenRow)
|
||||
|
||||
# Public: Identifies if a selection intersects with another selection.
|
||||
#
|
||||
# * `otherSelection` A {Selection} to check against.
|
||||
#
|
||||
# Returns a {Boolean}
|
||||
intersectsWith: (otherSelection, exclusive) ->
|
||||
@getBufferRange().intersectsWith(otherSelection.getBufferRange(), exclusive)
|
||||
|
||||
###
|
||||
Section: Modifying the selected range
|
||||
###
|
||||
|
||||
# Public: Clears the selection, moving the marker to the head.
|
||||
#
|
||||
# * `options` (optional) {Object} with the following keys:
|
||||
# * `autoscroll` {Boolean} indicating whether to autoscroll to the new
|
||||
# range. Defaults to `true` if this is the most recently added selection,
|
||||
# `false` otherwise.
|
||||
clear: (options) ->
|
||||
@goalScreenRange = null
|
||||
@marker.clearTail() unless @retainSelection
|
||||
@autoscroll() if options?.autoscroll ? @isLastSelection()
|
||||
@finalize()
|
||||
|
||||
# Public: Selects the text from the current cursor position to a given screen
|
||||
# position.
|
||||
#
|
||||
# * `position` An instance of {Point}, with a given `row` and `column`.
|
||||
selectToScreenPosition: (position, options) ->
|
||||
position = Point.fromObject(position)
|
||||
|
||||
@modifySelection =>
|
||||
if @initialScreenRange
|
||||
if position.isLessThan(@initialScreenRange.start)
|
||||
@marker.setScreenRange([position, @initialScreenRange.end], reversed: true)
|
||||
else
|
||||
@marker.setScreenRange([@initialScreenRange.start, position], reversed: false)
|
||||
else
|
||||
@cursor.setScreenPosition(position, options)
|
||||
|
||||
if @linewise
|
||||
@expandOverLine(options)
|
||||
else if @wordwise
|
||||
@expandOverWord(options)
|
||||
|
||||
# Public: Selects the text from the current cursor position to a given buffer
|
||||
# position.
|
||||
#
|
||||
# * `position` An instance of {Point}, with a given `row` and `column`.
|
||||
selectToBufferPosition: (position) ->
|
||||
@modifySelection => @cursor.setBufferPosition(position)
|
||||
|
||||
# Public: Selects the text one position right of the cursor.
|
||||
#
|
||||
# * `columnCount` (optional) {Number} number of columns to select (default: 1)
|
||||
selectRight: (columnCount) ->
|
||||
@modifySelection => @cursor.moveRight(columnCount)
|
||||
|
||||
# Public: Selects the text one position left of the cursor.
|
||||
#
|
||||
# * `columnCount` (optional) {Number} number of columns to select (default: 1)
|
||||
selectLeft: (columnCount) ->
|
||||
@modifySelection => @cursor.moveLeft(columnCount)
|
||||
|
||||
# Public: Selects all the text one position above the cursor.
|
||||
#
|
||||
# * `rowCount` (optional) {Number} number of rows to select (default: 1)
|
||||
selectUp: (rowCount) ->
|
||||
@modifySelection => @cursor.moveUp(rowCount)
|
||||
|
||||
# Public: Selects all the text one position below the cursor.
|
||||
#
|
||||
# * `rowCount` (optional) {Number} number of rows to select (default: 1)
|
||||
selectDown: (rowCount) ->
|
||||
@modifySelection => @cursor.moveDown(rowCount)
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the top of
|
||||
# the buffer.
|
||||
selectToTop: ->
|
||||
@modifySelection => @cursor.moveToTop()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the bottom
|
||||
# of the buffer.
|
||||
selectToBottom: ->
|
||||
@modifySelection => @cursor.moveToBottom()
|
||||
|
||||
# Public: Selects all the text in the buffer.
|
||||
selectAll: ->
|
||||
@setBufferRange(@editor.buffer.getRange(), autoscroll: false)
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the
|
||||
# beginning of the line.
|
||||
selectToBeginningOfLine: ->
|
||||
@modifySelection => @cursor.moveToBeginningOfLine()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the first
|
||||
# character of the line.
|
||||
selectToFirstCharacterOfLine: ->
|
||||
@modifySelection => @cursor.moveToFirstCharacterOfLine()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the end of
|
||||
# the screen line.
|
||||
selectToEndOfLine: ->
|
||||
@modifySelection => @cursor.moveToEndOfScreenLine()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the end of
|
||||
# the buffer line.
|
||||
selectToEndOfBufferLine: ->
|
||||
@modifySelection => @cursor.moveToEndOfLine()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the
|
||||
# beginning of the word.
|
||||
selectToBeginningOfWord: ->
|
||||
@modifySelection => @cursor.moveToBeginningOfWord()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the end of
|
||||
# the word.
|
||||
selectToEndOfWord: ->
|
||||
@modifySelection => @cursor.moveToEndOfWord()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the
|
||||
# beginning of the next word.
|
||||
selectToBeginningOfNextWord: ->
|
||||
@modifySelection => @cursor.moveToBeginningOfNextWord()
|
||||
|
||||
# Public: Selects text to the previous word boundary.
|
||||
selectToPreviousWordBoundary: ->
|
||||
@modifySelection => @cursor.moveToPreviousWordBoundary()
|
||||
|
||||
# Public: Selects text to the next word boundary.
|
||||
selectToNextWordBoundary: ->
|
||||
@modifySelection => @cursor.moveToNextWordBoundary()
|
||||
|
||||
# Public: Selects text to the previous subword boundary.
|
||||
selectToPreviousSubwordBoundary: ->
|
||||
@modifySelection => @cursor.moveToPreviousSubwordBoundary()
|
||||
|
||||
# Public: Selects text to the next subword boundary.
|
||||
selectToNextSubwordBoundary: ->
|
||||
@modifySelection => @cursor.moveToNextSubwordBoundary()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the
|
||||
# beginning of the next paragraph.
|
||||
selectToBeginningOfNextParagraph: ->
|
||||
@modifySelection => @cursor.moveToBeginningOfNextParagraph()
|
||||
|
||||
# Public: Selects all the text from the current cursor position to the
|
||||
# beginning of the previous paragraph.
|
||||
selectToBeginningOfPreviousParagraph: ->
|
||||
@modifySelection => @cursor.moveToBeginningOfPreviousParagraph()
|
||||
|
||||
# Public: Modifies the selection to encompass the current word.
|
||||
#
|
||||
# Returns a {Range}.
|
||||
selectWord: (options={}) ->
|
||||
options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace()
|
||||
if @cursor.isBetweenWordAndNonWord()
|
||||
options.includeNonWordCharacters = false
|
||||
|
||||
@setBufferRange(@cursor.getCurrentWordBufferRange(options), options)
|
||||
@wordwise = true
|
||||
@initialScreenRange = @getScreenRange()
|
||||
|
||||
# Public: Expands the newest selection to include the entire word on which
|
||||
# the cursors rests.
|
||||
expandOverWord: (options) ->
|
||||
@setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange()), autoscroll: false)
|
||||
@cursor.autoscroll() if options?.autoscroll ? true
|
||||
|
||||
# Public: Selects an entire line in the buffer.
|
||||
#
|
||||
# * `row` The line {Number} to select (default: the row of the cursor).
|
||||
selectLine: (row, options) ->
|
||||
if row?
|
||||
@setBufferRange(@editor.bufferRangeForBufferRow(row, includeNewline: true), options)
|
||||
else
|
||||
startRange = @editor.bufferRangeForBufferRow(@marker.getStartBufferPosition().row)
|
||||
endRange = @editor.bufferRangeForBufferRow(@marker.getEndBufferPosition().row, includeNewline: true)
|
||||
@setBufferRange(startRange.union(endRange), options)
|
||||
|
||||
@linewise = true
|
||||
@wordwise = false
|
||||
@initialScreenRange = @getScreenRange()
|
||||
|
||||
# Public: Expands the newest selection to include the entire line on which
|
||||
# the cursor currently rests.
|
||||
#
|
||||
# It also includes the newline character.
|
||||
expandOverLine: (options) ->
|
||||
range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true))
|
||||
@setBufferRange(range, autoscroll: false)
|
||||
@cursor.autoscroll() if options?.autoscroll ? true
|
||||
|
||||
###
|
||||
Section: Modifying the selected text
|
||||
###
|
||||
|
||||
# Public: Replaces text at the current selection.
|
||||
#
|
||||
# * `text` A {String} representing the text to add
|
||||
# * `options` (optional) {Object} with keys:
|
||||
# * `select` if `true`, selects the newly added text.
|
||||
# * `autoIndent` if `true`, indents all inserted text appropriately.
|
||||
# * `autoIndentNewline` if `true`, indent newline appropriately.
|
||||
# * `autoDecreaseIndent` if `true`, decreases indent level appropriately
|
||||
# (for example, when a closing bracket is inserted).
|
||||
# * `normalizeLineEndings` (optional) {Boolean} (default: true)
|
||||
# * `undo` if `skip`, skips the undo stack for this operation.
|
||||
insertText: (text, options={}) ->
|
||||
oldBufferRange = @getBufferRange()
|
||||
wasReversed = @isReversed()
|
||||
@clear(options)
|
||||
|
||||
autoIndentFirstLine = false
|
||||
precedingText = @editor.getTextInRange([[oldBufferRange.start.row, 0], oldBufferRange.start])
|
||||
remainingLines = text.split('\n')
|
||||
firstInsertedLine = remainingLines.shift()
|
||||
|
||||
if options.indentBasis?
|
||||
indentAdjustment = @editor.indentLevelForLine(precedingText) - options.indentBasis
|
||||
@adjustIndent(remainingLines, indentAdjustment)
|
||||
|
||||
textIsAutoIndentable = text is '\n' or text is '\r\n' or NonWhitespaceRegExp.test(text)
|
||||
if options.autoIndent and textIsAutoIndentable and not NonWhitespaceRegExp.test(precedingText) and remainingLines.length > 0
|
||||
autoIndentFirstLine = true
|
||||
firstLine = precedingText + firstInsertedLine
|
||||
desiredIndentLevel = @editor.tokenizedBuffer.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine)
|
||||
indentAdjustment = desiredIndentLevel - @editor.indentLevelForLine(firstLine)
|
||||
@adjustIndent(remainingLines, indentAdjustment)
|
||||
|
||||
text = firstInsertedLine
|
||||
text += '\n' + remainingLines.join('\n') if remainingLines.length > 0
|
||||
|
||||
newBufferRange = @editor.buffer.setTextInRange(oldBufferRange, text, pick(options, 'undo', 'normalizeLineEndings'))
|
||||
|
||||
if options.select
|
||||
@setBufferRange(newBufferRange, reversed: wasReversed)
|
||||
else
|
||||
@cursor.setBufferPosition(newBufferRange.end) if wasReversed
|
||||
|
||||
if autoIndentFirstLine
|
||||
@editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel)
|
||||
|
||||
if options.autoIndentNewline and text is '\n'
|
||||
@editor.autoIndentBufferRow(newBufferRange.end.row, preserveLeadingWhitespace: true, skipBlankLines: false)
|
||||
else if options.autoDecreaseIndent and NonWhitespaceRegExp.test(text)
|
||||
@editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row)
|
||||
|
||||
@autoscroll() if options.autoscroll ? @isLastSelection()
|
||||
|
||||
newBufferRange
|
||||
|
||||
# Public: Removes the first character before the selection if the selection
|
||||
# is empty otherwise it deletes the selection.
|
||||
backspace: ->
|
||||
@selectLeft() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes the selection or, if nothing is selected, then all
|
||||
# characters from the start of the selection back to the previous word
|
||||
# boundary.
|
||||
deleteToPreviousWordBoundary: ->
|
||||
@selectToPreviousWordBoundary() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes the selection or, if nothing is selected, then all
|
||||
# characters from the start of the selection up to the next word
|
||||
# boundary.
|
||||
deleteToNextWordBoundary: ->
|
||||
@selectToNextWordBoundary() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes from the start of the selection to the beginning of the
|
||||
# current word if the selection is empty otherwise it deletes the selection.
|
||||
deleteToBeginningOfWord: ->
|
||||
@selectToBeginningOfWord() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes from the beginning of the line which the selection begins on
|
||||
# all the way through to the end of the selection.
|
||||
deleteToBeginningOfLine: ->
|
||||
if @isEmpty() and @cursor.isAtBeginningOfLine()
|
||||
@selectLeft()
|
||||
else
|
||||
@selectToBeginningOfLine()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes the selection or the next character after the start of the
|
||||
# selection if the selection is empty.
|
||||
delete: ->
|
||||
@selectRight() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: If the selection is empty, removes all text from the cursor to the
|
||||
# end of the line. If the cursor is already at the end of the line, it
|
||||
# removes the following newline. If the selection isn't empty, only deletes
|
||||
# the contents of the selection.
|
||||
deleteToEndOfLine: ->
|
||||
return @delete() if @isEmpty() and @cursor.isAtEndOfLine()
|
||||
@selectToEndOfLine() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes the selection or all characters from the start of the
|
||||
# selection to the end of the current word if nothing is selected.
|
||||
deleteToEndOfWord: ->
|
||||
@selectToEndOfWord() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes the selection or all characters from the start of the
|
||||
# selection to the end of the current word if nothing is selected.
|
||||
deleteToBeginningOfSubword: ->
|
||||
@selectToPreviousSubwordBoundary() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes the selection or all characters from the start of the
|
||||
# selection to the end of the current word if nothing is selected.
|
||||
deleteToEndOfSubword: ->
|
||||
@selectToNextSubwordBoundary() if @isEmpty()
|
||||
@deleteSelectedText()
|
||||
|
||||
# Public: Removes only the selected text.
|
||||
deleteSelectedText: ->
|
||||
bufferRange = @getBufferRange()
|
||||
@editor.buffer.delete(bufferRange) unless bufferRange.isEmpty()
|
||||
@cursor?.setBufferPosition(bufferRange.start)
|
||||
|
||||
# Public: Removes the line at the beginning of the selection if the selection
|
||||
# is empty unless the selection spans multiple lines in which case all lines
|
||||
# are removed.
|
||||
deleteLine: ->
|
||||
if @isEmpty()
|
||||
start = @cursor.getScreenRow()
|
||||
range = @editor.bufferRowsForScreenRows(start, start + 1)
|
||||
if range[1] > range[0]
|
||||
@editor.buffer.deleteRows(range[0], range[1] - 1)
|
||||
else
|
||||
@editor.buffer.deleteRow(range[0])
|
||||
else
|
||||
range = @getBufferRange()
|
||||
start = range.start.row
|
||||
end = range.end.row
|
||||
if end isnt @editor.buffer.getLastRow() and range.end.column is 0
|
||||
end--
|
||||
@editor.buffer.deleteRows(start, end)
|
||||
|
||||
# Public: Joins the current line with the one below it. Lines will
|
||||
# be separated by a single space.
|
||||
#
|
||||
# If there selection spans more than one line, all the lines are joined together.
|
||||
joinLines: ->
|
||||
selectedRange = @getBufferRange()
|
||||
if selectedRange.isEmpty()
|
||||
return if selectedRange.start.row is @editor.buffer.getLastRow()
|
||||
else
|
||||
joinMarker = @editor.markBufferRange(selectedRange, invalidate: 'never')
|
||||
|
||||
rowCount = Math.max(1, selectedRange.getRowCount() - 1)
|
||||
for [0...rowCount]
|
||||
@cursor.setBufferPosition([selectedRange.start.row])
|
||||
@cursor.moveToEndOfLine()
|
||||
|
||||
# Remove trailing whitespace from the current line
|
||||
scanRange = @cursor.getCurrentLineBufferRange()
|
||||
trailingWhitespaceRange = null
|
||||
@editor.scanInBufferRange /[ \t]+$/, scanRange, ({range}) ->
|
||||
trailingWhitespaceRange = range
|
||||
if trailingWhitespaceRange?
|
||||
@setBufferRange(trailingWhitespaceRange)
|
||||
@deleteSelectedText()
|
||||
|
||||
currentRow = selectedRange.start.row
|
||||
nextRow = currentRow + 1
|
||||
insertSpace = nextRow <= @editor.buffer.getLastRow() and
|
||||
@editor.buffer.lineLengthForRow(nextRow) > 0 and
|
||||
@editor.buffer.lineLengthForRow(currentRow) > 0
|
||||
@insertText(' ') if insertSpace
|
||||
|
||||
@cursor.moveToEndOfLine()
|
||||
|
||||
# Remove leading whitespace from the line below
|
||||
@modifySelection =>
|
||||
@cursor.moveRight()
|
||||
@cursor.moveToFirstCharacterOfLine()
|
||||
@deleteSelectedText()
|
||||
|
||||
@cursor.moveLeft() if insertSpace
|
||||
|
||||
if joinMarker?
|
||||
newSelectedRange = joinMarker.getBufferRange()
|
||||
@setBufferRange(newSelectedRange)
|
||||
joinMarker.destroy()
|
||||
|
||||
# Public: Removes one level of indent from the currently selected rows.
|
||||
outdentSelectedRows: ->
|
||||
[start, end] = @getBufferRowRange()
|
||||
buffer = @editor.buffer
|
||||
leadingTabRegex = new RegExp("^( {1,#{@editor.getTabLength()}}|\t)")
|
||||
for row in [start..end]
|
||||
if matchLength = buffer.lineForRow(row).match(leadingTabRegex)?[0].length
|
||||
buffer.delete [[row, 0], [row, matchLength]]
|
||||
return
|
||||
|
||||
# Public: Sets the indentation level of all selected rows to values suggested
|
||||
# by the relevant grammars.
|
||||
autoIndentSelectedRows: ->
|
||||
[start, end] = @getBufferRowRange()
|
||||
@editor.autoIndentBufferRows(start, end)
|
||||
|
||||
# Public: Wraps the selected lines in comments if they aren't currently part
|
||||
# of a comment.
|
||||
#
|
||||
# Removes the comment if they are currently wrapped in a comment.
|
||||
toggleLineComments: ->
|
||||
@editor.toggleLineCommentsForBufferRows(@getBufferRowRange()...)
|
||||
|
||||
# Public: Cuts the selection until the end of the screen line.
|
||||
cutToEndOfLine: (maintainClipboard) ->
|
||||
@selectToEndOfLine() if @isEmpty()
|
||||
@cut(maintainClipboard)
|
||||
|
||||
# Public: Cuts the selection until the end of the buffer line.
|
||||
cutToEndOfBufferLine: (maintainClipboard) ->
|
||||
@selectToEndOfBufferLine() if @isEmpty()
|
||||
@cut(maintainClipboard)
|
||||
|
||||
# Public: Copies the selection to the clipboard and then deletes it.
|
||||
#
|
||||
# * `maintainClipboard` {Boolean} (default: false) See {::copy}
|
||||
# * `fullLine` {Boolean} (default: false) See {::copy}
|
||||
cut: (maintainClipboard=false, fullLine=false) ->
|
||||
@copy(maintainClipboard, fullLine)
|
||||
@delete()
|
||||
|
||||
# Public: Copies the current selection to the clipboard.
|
||||
#
|
||||
# * `maintainClipboard` {Boolean} if `true`, a specific metadata property
|
||||
# is created to store each content copied to the clipboard. The clipboard
|
||||
# `text` still contains the concatenation of the clipboard with the
|
||||
# current selection. (default: false)
|
||||
# * `fullLine` {Boolean} if `true`, the copied text will always be pasted
|
||||
# at the beginning of the line containing the cursor, regardless of the
|
||||
# cursor's horizontal position. (default: false)
|
||||
copy: (maintainClipboard=false, fullLine=false) ->
|
||||
return if @isEmpty()
|
||||
{start, end} = @getBufferRange()
|
||||
selectionText = @editor.getTextInRange([start, end])
|
||||
precedingText = @editor.getTextInRange([[start.row, 0], start])
|
||||
startLevel = @editor.indentLevelForLine(precedingText)
|
||||
|
||||
if maintainClipboard
|
||||
{text: clipboardText, metadata} = @editor.constructor.clipboard.readWithMetadata()
|
||||
metadata ?= {}
|
||||
unless metadata.selections?
|
||||
metadata.selections = [{
|
||||
text: clipboardText,
|
||||
indentBasis: metadata.indentBasis,
|
||||
fullLine: metadata.fullLine,
|
||||
}]
|
||||
metadata.selections.push({
|
||||
text: selectionText,
|
||||
indentBasis: startLevel,
|
||||
fullLine: fullLine
|
||||
})
|
||||
@editor.constructor.clipboard.write([clipboardText, selectionText].join("\n"), metadata)
|
||||
else
|
||||
@editor.constructor.clipboard.write(selectionText, {
|
||||
indentBasis: startLevel,
|
||||
fullLine: fullLine
|
||||
})
|
||||
|
||||
# Public: Creates a fold containing the current selection.
|
||||
fold: ->
|
||||
range = @getBufferRange()
|
||||
unless range.isEmpty()
|
||||
@editor.foldBufferRange(range)
|
||||
@cursor.setBufferPosition(range.end)
|
||||
|
||||
# Private: Increase the indentation level of the given text by given number
|
||||
# of levels. Leaves the first line unchanged.
|
||||
adjustIndent: (lines, indentAdjustment) ->
|
||||
for line, i in lines
|
||||
if indentAdjustment is 0 or line is ''
|
||||
continue
|
||||
else if indentAdjustment > 0
|
||||
lines[i] = @editor.buildIndentString(indentAdjustment) + line
|
||||
else
|
||||
currentIndentLevel = @editor.indentLevelForLine(lines[i])
|
||||
indentLevel = Math.max(0, currentIndentLevel + indentAdjustment)
|
||||
lines[i] = line.replace(/^[\t ]+/, @editor.buildIndentString(indentLevel))
|
||||
return
|
||||
|
||||
# Indent the current line(s).
|
||||
#
|
||||
# If the selection is empty, indents the current line if the cursor precedes
|
||||
# non-whitespace characters, and otherwise inserts a tab. If the selection is
|
||||
# non empty, calls {::indentSelectedRows}.
|
||||
#
|
||||
# * `options` (optional) {Object} with the keys:
|
||||
# * `autoIndent` If `true`, the line is indented to an automatically-inferred
|
||||
# level. Otherwise, {TextEditor::getTabText} is inserted.
|
||||
indent: ({autoIndent}={}) ->
|
||||
{row} = @cursor.getBufferPosition()
|
||||
|
||||
if @isEmpty()
|
||||
@cursor.skipLeadingWhitespace()
|
||||
desiredIndent = @editor.suggestedIndentForBufferRow(row)
|
||||
delta = desiredIndent - @cursor.getIndentLevel()
|
||||
|
||||
if autoIndent and delta > 0
|
||||
delta = Math.max(delta, 1) unless @editor.getSoftTabs()
|
||||
@insertText(@editor.buildIndentString(delta))
|
||||
else
|
||||
@insertText(@editor.buildIndentString(1, @cursor.getBufferColumn()))
|
||||
else
|
||||
@indentSelectedRows()
|
||||
|
||||
# Public: If the selection spans multiple rows, indent all of them.
|
||||
indentSelectedRows: ->
|
||||
[start, end] = @getBufferRowRange()
|
||||
for row in [start..end]
|
||||
@editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) is 0
|
||||
return
|
||||
|
||||
###
|
||||
Section: Managing multiple selections
|
||||
###
|
||||
|
||||
# Public: Moves the selection down one row.
|
||||
addSelectionBelow: ->
|
||||
range = @getGoalScreenRange().copy()
|
||||
nextRow = range.end.row + 1
|
||||
|
||||
for row in [nextRow..@editor.getLastScreenRow()]
|
||||
range.start.row = row
|
||||
range.end.row = row
|
||||
clippedRange = @editor.clipScreenRange(range, skipSoftWrapIndentation: true)
|
||||
|
||||
if range.isEmpty()
|
||||
continue if range.end.column > 0 and clippedRange.end.column is 0
|
||||
else
|
||||
continue if clippedRange.isEmpty()
|
||||
|
||||
selection = @editor.addSelectionForScreenRange(clippedRange)
|
||||
selection.setGoalScreenRange(range)
|
||||
break
|
||||
|
||||
return
|
||||
|
||||
# Public: Moves the selection up one row.
|
||||
addSelectionAbove: ->
|
||||
range = @getGoalScreenRange().copy()
|
||||
previousRow = range.end.row - 1
|
||||
|
||||
for row in [previousRow..0]
|
||||
range.start.row = row
|
||||
range.end.row = row
|
||||
clippedRange = @editor.clipScreenRange(range, skipSoftWrapIndentation: true)
|
||||
|
||||
if range.isEmpty()
|
||||
continue if range.end.column > 0 and clippedRange.end.column is 0
|
||||
else
|
||||
continue if clippedRange.isEmpty()
|
||||
|
||||
selection = @editor.addSelectionForScreenRange(clippedRange)
|
||||
selection.setGoalScreenRange(range)
|
||||
break
|
||||
|
||||
return
|
||||
|
||||
# Public: Combines the given selection into this selection and then destroys
|
||||
# the given selection.
|
||||
#
|
||||
# * `otherSelection` A {Selection} to merge with.
|
||||
# * `options` (optional) {Object} options matching those found in {::setBufferRange}.
|
||||
merge: (otherSelection, options) ->
|
||||
myGoalScreenRange = @getGoalScreenRange()
|
||||
otherGoalScreenRange = otherSelection.getGoalScreenRange()
|
||||
|
||||
if myGoalScreenRange? and otherGoalScreenRange?
|
||||
options.goalScreenRange = myGoalScreenRange.union(otherGoalScreenRange)
|
||||
else
|
||||
options.goalScreenRange = myGoalScreenRange ? otherGoalScreenRange
|
||||
|
||||
@setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), Object.assign(autoscroll: false, options))
|
||||
otherSelection.destroy()
|
||||
|
||||
###
|
||||
Section: Comparing to other selections
|
||||
###
|
||||
|
||||
# Public: Compare this selection's buffer range to another selection's buffer
|
||||
# range.
|
||||
#
|
||||
# See {Range::compare} for more details.
|
||||
#
|
||||
# * `otherSelection` A {Selection} to compare against
|
||||
compare: (otherSelection) ->
|
||||
@marker.compare(otherSelection.marker)
|
||||
|
||||
###
|
||||
Section: Private Utilities
|
||||
###
|
||||
|
||||
setGoalScreenRange: (range) ->
|
||||
@goalScreenRange = Range.fromObject(range)
|
||||
|
||||
getGoalScreenRange: ->
|
||||
@goalScreenRange ? @getScreenRange()
|
||||
|
||||
markerDidChange: (e) ->
|
||||
{oldHeadBufferPosition, oldTailBufferPosition, newHeadBufferPosition} = e
|
||||
{oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e
|
||||
{textChanged} = e
|
||||
|
||||
unless oldHeadScreenPosition.isEqual(newHeadScreenPosition)
|
||||
@cursor.goalColumn = null
|
||||
cursorMovedEvent = {
|
||||
oldBufferPosition: oldHeadBufferPosition
|
||||
oldScreenPosition: oldHeadScreenPosition
|
||||
newBufferPosition: newHeadBufferPosition
|
||||
newScreenPosition: newHeadScreenPosition
|
||||
textChanged: textChanged
|
||||
cursor: @cursor
|
||||
}
|
||||
@cursor.emitter.emit('did-change-position', cursorMovedEvent)
|
||||
@editor.cursorMoved(cursorMovedEvent)
|
||||
|
||||
@emitter.emit 'did-change-range'
|
||||
@editor.selectionRangeChanged(
|
||||
oldBufferRange: new Range(oldHeadBufferPosition, oldTailBufferPosition)
|
||||
oldScreenRange: new Range(oldHeadScreenPosition, oldTailScreenPosition)
|
||||
newBufferRange: @getBufferRange()
|
||||
newScreenRange: @getScreenRange()
|
||||
selection: this
|
||||
)
|
||||
|
||||
markerDidDestroy: ->
|
||||
return if @editor.isDestroyed()
|
||||
|
||||
@destroyed = true
|
||||
@cursor.destroyed = true
|
||||
|
||||
@editor.removeSelection(this)
|
||||
|
||||
@cursor.emitter.emit 'did-destroy'
|
||||
@emitter.emit 'did-destroy'
|
||||
|
||||
@cursor.emitter.dispose()
|
||||
@emitter.dispose()
|
||||
|
||||
finalize: ->
|
||||
@initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange())
|
||||
if @isEmpty()
|
||||
@wordwise = false
|
||||
@linewise = false
|
||||
|
||||
autoscroll: (options) ->
|
||||
if @marker.hasTail()
|
||||
@editor.scrollToScreenRange(@getScreenRange(), Object.assign({reversed: @isReversed()}, options))
|
||||
else
|
||||
@cursor.autoscroll(options)
|
||||
|
||||
clearAutoscroll: ->
|
||||
|
||||
modifySelection: (fn) ->
|
||||
@retainSelection = true
|
||||
@plantTail()
|
||||
fn()
|
||||
@retainSelection = false
|
||||
|
||||
# Sets the marker's tail to the same position as the marker's head.
|
||||
#
|
||||
# This only works if there isn't already a tail position.
|
||||
#
|
||||
# Returns a {Point} representing the new tail position.
|
||||
plantTail: ->
|
||||
@marker.plantTail()
|
||||
977
src/selection.js
Normal file
977
src/selection.js
Normal file
@@ -0,0 +1,977 @@
|
||||
const {Point, Range} = require('text-buffer')
|
||||
const {pick} = require('underscore-plus')
|
||||
const {Emitter} = require('event-kit')
|
||||
|
||||
const NonWhitespaceRegExp = /\S/
|
||||
let nextId = 0
|
||||
|
||||
// Extended: Represents a selection in the {TextEditor}.
|
||||
module.exports =
|
||||
class Selection {
|
||||
constructor ({cursor, marker, editor, id}) {
|
||||
this.id = (id != null) ? id : nextId++
|
||||
this.cursor = cursor
|
||||
this.marker = marker
|
||||
this.editor = editor
|
||||
this.emitter = new Emitter()
|
||||
this.initialScreenRange = null
|
||||
this.wordwise = false
|
||||
this.cursor.selection = this
|
||||
this.decoration = this.editor.decorateMarker(this.marker, {type: 'highlight', class: 'selection'})
|
||||
this.marker.onDidChange(e => this.markerDidChange(e))
|
||||
this.marker.onDidDestroy(() => this.markerDidDestroy())
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.marker.destroy()
|
||||
}
|
||||
|
||||
isLastSelection () {
|
||||
return this === this.editor.getLastSelection()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Extended: Calls your `callback` when the selection was moved.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
// * `event` {Object}
|
||||
// * `oldBufferRange` {Range}
|
||||
// * `oldScreenRange` {Range}
|
||||
// * `newBufferRange` {Range}
|
||||
// * `newScreenRange` {Range}
|
||||
// * `selection` {Selection} that triggered the event
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeRange (callback) {
|
||||
return this.emitter.on('did-change-range', callback)
|
||||
}
|
||||
|
||||
// Extended: Calls your `callback` when the selection was 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 the selection range
|
||||
*/
|
||||
|
||||
// Public: Returns the screen {Range} for the selection.
|
||||
getScreenRange () {
|
||||
return this.marker.getScreenRange()
|
||||
}
|
||||
|
||||
// Public: Modifies the screen range for the selection.
|
||||
//
|
||||
// * `screenRange` The new {Range} to use.
|
||||
// * `options` (optional) {Object} options matching those found in {::setBufferRange}.
|
||||
setScreenRange (screenRange, options) {
|
||||
return this.setBufferRange(this.editor.bufferRangeForScreenRange(screenRange), options)
|
||||
}
|
||||
|
||||
// Public: Returns the buffer {Range} for the selection.
|
||||
getBufferRange () {
|
||||
return this.marker.getBufferRange()
|
||||
}
|
||||
|
||||
// Public: Modifies the buffer {Range} for the selection.
|
||||
//
|
||||
// * `bufferRange` The new {Range} to select.
|
||||
// * `options` (optional) {Object} with the keys:
|
||||
// * `preserveFolds` if `true`, the fold settings are preserved after the
|
||||
// selection moves.
|
||||
// * `autoscroll` {Boolean} indicating whether to autoscroll to the new
|
||||
// range. Defaults to `true` if this is the most recently added selection,
|
||||
// `false` otherwise.
|
||||
setBufferRange (bufferRange, options = {}) {
|
||||
bufferRange = Range.fromObject(bufferRange)
|
||||
if (options.reversed == null) options.reversed = this.isReversed()
|
||||
if (!options.preserveFolds) this.editor.destroyFoldsContainingBufferPositions([bufferRange.start, bufferRange.end], true)
|
||||
this.modifySelection(() => {
|
||||
const needsFlash = options.flash
|
||||
options.flash = null
|
||||
this.marker.setBufferRange(bufferRange, options)
|
||||
const autoscroll = options.autoscroll != null ? options.autoscroll : this.isLastSelection()
|
||||
if (autoscroll) this.autoscroll()
|
||||
if (needsFlash) this.decoration.flash('flash', this.editor.selectionFlashDuration)
|
||||
})
|
||||
}
|
||||
|
||||
// Public: Returns the starting and ending buffer rows the selection is
|
||||
// highlighting.
|
||||
//
|
||||
// Returns an {Array} of two {Number}s: the starting row, and the ending row.
|
||||
getBufferRowRange () {
|
||||
const range = this.getBufferRange()
|
||||
const start = range.start.row
|
||||
let end = range.end.row
|
||||
if (range.end.column === 0) end = Math.max(start, end - 1)
|
||||
return [start, end]
|
||||
}
|
||||
|
||||
getTailScreenPosition () {
|
||||
return this.marker.getTailScreenPosition()
|
||||
}
|
||||
|
||||
getTailBufferPosition () {
|
||||
return this.marker.getTailBufferPosition()
|
||||
}
|
||||
|
||||
getHeadScreenPosition () {
|
||||
return this.marker.getHeadScreenPosition()
|
||||
}
|
||||
|
||||
getHeadBufferPosition () {
|
||||
return this.marker.getHeadBufferPosition()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Info about the selection
|
||||
*/
|
||||
|
||||
// Public: Determines if the selection contains anything.
|
||||
isEmpty () {
|
||||
return this.getBufferRange().isEmpty()
|
||||
}
|
||||
|
||||
// Public: Determines if the ending position of a marker is greater than the
|
||||
// starting position.
|
||||
//
|
||||
// This can happen when, for example, you highlight text "up" in a {TextBuffer}.
|
||||
isReversed () {
|
||||
return this.marker.isReversed()
|
||||
}
|
||||
|
||||
// Public: Returns whether the selection is a single line or not.
|
||||
isSingleScreenLine () {
|
||||
return this.getScreenRange().isSingleLine()
|
||||
}
|
||||
|
||||
// Public: Returns the text in the selection.
|
||||
getText () {
|
||||
return this.editor.buffer.getTextInRange(this.getBufferRange())
|
||||
}
|
||||
|
||||
// Public: Identifies if a selection intersects with a given buffer range.
|
||||
//
|
||||
// * `bufferRange` A {Range} to check against.
|
||||
//
|
||||
// Returns a {Boolean}
|
||||
intersectsBufferRange (bufferRange) {
|
||||
return this.getBufferRange().intersectsWith(bufferRange)
|
||||
}
|
||||
|
||||
intersectsScreenRowRange (startRow, endRow) {
|
||||
return this.getScreenRange().intersectsRowRange(startRow, endRow)
|
||||
}
|
||||
|
||||
intersectsScreenRow (screenRow) {
|
||||
return this.getScreenRange().intersectsRow(screenRow)
|
||||
}
|
||||
|
||||
// Public: Identifies if a selection intersects with another selection.
|
||||
//
|
||||
// * `otherSelection` A {Selection} to check against.
|
||||
//
|
||||
// Returns a {Boolean}
|
||||
intersectsWith (otherSelection, exclusive) {
|
||||
return this.getBufferRange().intersectsWith(otherSelection.getBufferRange(), exclusive)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Modifying the selected range
|
||||
*/
|
||||
|
||||
// Public: Clears the selection, moving the marker to the head.
|
||||
//
|
||||
// * `options` (optional) {Object} with the following keys:
|
||||
// * `autoscroll` {Boolean} indicating whether to autoscroll to the new
|
||||
// range. Defaults to `true` if this is the most recently added selection,
|
||||
// `false` otherwise.
|
||||
clear (options) {
|
||||
this.goalScreenRange = null
|
||||
if (!this.retainSelection) this.marker.clearTail()
|
||||
const autoscroll = options && options.autoscroll != null
|
||||
? options.autoscroll
|
||||
: this.isLastSelection()
|
||||
if (autoscroll) this.autoscroll()
|
||||
this.finalize()
|
||||
}
|
||||
|
||||
// Public: Selects the text from the current cursor position to a given screen
|
||||
// position.
|
||||
//
|
||||
// * `position` An instance of {Point}, with a given `row` and `column`.
|
||||
selectToScreenPosition (position, options) {
|
||||
position = Point.fromObject(position)
|
||||
|
||||
this.modifySelection(() => {
|
||||
if (this.initialScreenRange) {
|
||||
if (position.isLessThan(this.initialScreenRange.start)) {
|
||||
this.marker.setScreenRange([position, this.initialScreenRange.end], {reversed: true})
|
||||
} else {
|
||||
this.marker.setScreenRange([this.initialScreenRange.start, position], {reversed: false})
|
||||
}
|
||||
} else {
|
||||
this.cursor.setScreenPosition(position, options)
|
||||
}
|
||||
|
||||
if (this.linewise) {
|
||||
this.expandOverLine(options)
|
||||
} else if (this.wordwise) {
|
||||
this.expandOverWord(options)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Public: Selects the text from the current cursor position to a given buffer
|
||||
// position.
|
||||
//
|
||||
// * `position` An instance of {Point}, with a given `row` and `column`.
|
||||
selectToBufferPosition (position) {
|
||||
this.modifySelection(() => this.cursor.setBufferPosition(position))
|
||||
}
|
||||
|
||||
// Public: Selects the text one position right of the cursor.
|
||||
//
|
||||
// * `columnCount` (optional) {Number} number of columns to select (default: 1)
|
||||
selectRight (columnCount) {
|
||||
this.modifySelection(() => this.cursor.moveRight(columnCount))
|
||||
}
|
||||
|
||||
// Public: Selects the text one position left of the cursor.
|
||||
//
|
||||
// * `columnCount` (optional) {Number} number of columns to select (default: 1)
|
||||
selectLeft (columnCount) {
|
||||
this.modifySelection(() => this.cursor.moveLeft(columnCount))
|
||||
}
|
||||
|
||||
// Public: Selects all the text one position above the cursor.
|
||||
//
|
||||
// * `rowCount` (optional) {Number} number of rows to select (default: 1)
|
||||
selectUp (rowCount) {
|
||||
this.modifySelection(() => this.cursor.moveUp(rowCount))
|
||||
}
|
||||
|
||||
// Public: Selects all the text one position below the cursor.
|
||||
//
|
||||
// * `rowCount` (optional) {Number} number of rows to select (default: 1)
|
||||
selectDown (rowCount) {
|
||||
this.modifySelection(() => this.cursor.moveDown(rowCount))
|
||||
}
|
||||
|
||||
// Public: Selects all the text from the current cursor position to the top of
|
||||
// the buffer.
|
||||
selectToTop () {
|
||||
this.modifySelection(() => this.cursor.moveToTop())
|
||||
}
|
||||
|
||||
// Public: Selects all the text from the current cursor position to the bottom
|
||||
// of the buffer.
|
||||
selectToBottom () {
|
||||
this.modifySelection(() => this.cursor.moveToBottom())
|
||||
}
|
||||
|
||||
// Public: Selects all the text in the buffer.
|
||||
selectAll () {
|
||||
this.setBufferRange(this.editor.buffer.getRange(), {autoscroll: false})
|
||||
}
|
||||
|
||||
// Public: Selects all the text from the current cursor position to the
|
||||
// beginning of the line.
|
||||
selectToBeginningOfLine () {
|
||||
this.modifySelection(() => this.cursor.moveToBeginningOfLine())
|
||||
}
|
||||
|
||||
// Public: Selects all the text from the current cursor position to the first
|
||||
// character of the line.
|
||||
selectToFirstCharacterOfLine () {
|
||||
this.modifySelection(() => this.cursor.moveToFirstCharacterOfLine())
|
||||
}
|
||||
|
||||
// Public: Selects all the text from the current cursor position to the end of
|
||||
// the screen line.
|
||||
selectToEndOfLine () {
|
||||
this.modifySelection(() => this.cursor.moveToEndOfScreenLine())
|
||||
}
|
||||
|
||||
// Public: Selects all the text from the current cursor position to the end of
|
||||
// the buffer line.
|
||||
selectToEndOfBufferLine () {
|
||||
this.modifySelection(() => this.cursor.moveToEndOfLine())
|
||||
}
|
||||
|
||||
// Public: Selects all the text from the current cursor position to the
|
||||
// beginning of the word.
|
||||
selectToBeginningOfWord () {
|
||||
this.modifySelection(() => this.cursor.moveToBeginningOfWord())
|
||||
}
|
||||
|
||||
// Public: Selects all the text from the current cursor position to the end of
|
||||
// the word.
|
||||
selectToEndOfWord () {
|
||||
this.modifySelection(() => this.cursor.moveToEndOfWord())
|
||||
}
|
||||
|
||||
// Public: Selects all the text from the current cursor position to the
|
||||
// beginning of the next word.
|
||||
selectToBeginningOfNextWord () {
|
||||
this.modifySelection(() => this.cursor.moveToBeginningOfNextWord())
|
||||
}
|
||||
|
||||
// Public: Selects text to the previous word boundary.
|
||||
selectToPreviousWordBoundary () {
|
||||
this.modifySelection(() => this.cursor.moveToPreviousWordBoundary())
|
||||
}
|
||||
|
||||
// Public: Selects text to the next word boundary.
|
||||
selectToNextWordBoundary () {
|
||||
this.modifySelection(() => this.cursor.moveToNextWordBoundary())
|
||||
}
|
||||
|
||||
// Public: Selects text to the previous subword boundary.
|
||||
selectToPreviousSubwordBoundary () {
|
||||
this.modifySelection(() => this.cursor.moveToPreviousSubwordBoundary())
|
||||
}
|
||||
|
||||
// Public: Selects text to the next subword boundary.
|
||||
selectToNextSubwordBoundary () {
|
||||
this.modifySelection(() => this.cursor.moveToNextSubwordBoundary())
|
||||
}
|
||||
|
||||
// Public: Selects all the text from the current cursor position to the
|
||||
// beginning of the next paragraph.
|
||||
selectToBeginningOfNextParagraph () {
|
||||
this.modifySelection(() => this.cursor.moveToBeginningOfNextParagraph())
|
||||
}
|
||||
|
||||
// Public: Selects all the text from the current cursor position to the
|
||||
// beginning of the previous paragraph.
|
||||
selectToBeginningOfPreviousParagraph () {
|
||||
this.modifySelection(() => this.cursor.moveToBeginningOfPreviousParagraph())
|
||||
}
|
||||
|
||||
// Public: Modifies the selection to encompass the current word.
|
||||
//
|
||||
// Returns a {Range}.
|
||||
selectWord (options = {}) {
|
||||
if (this.cursor.isSurroundedByWhitespace()) options.wordRegex = /[\t ]*/
|
||||
if (this.cursor.isBetweenWordAndNonWord()) {
|
||||
options.includeNonWordCharacters = false
|
||||
}
|
||||
|
||||
this.setBufferRange(this.cursor.getCurrentWordBufferRange(options), options)
|
||||
this.wordwise = true
|
||||
this.initialScreenRange = this.getScreenRange()
|
||||
}
|
||||
|
||||
// Public: Expands the newest selection to include the entire word on which
|
||||
// the cursors rests.
|
||||
expandOverWord (options) {
|
||||
this.setBufferRange(this.getBufferRange().union(this.cursor.getCurrentWordBufferRange()), {autoscroll: false})
|
||||
const autoscroll = options && options.autoscroll != null ? options.autoscroll : this.isLastSelection()
|
||||
if (autoscroll) this.cursor.autoscroll()
|
||||
}
|
||||
|
||||
// Public: Selects an entire line in the buffer.
|
||||
//
|
||||
// * `row` The line {Number} to select (default: the row of the cursor).
|
||||
selectLine (row, options) {
|
||||
if (row != null) {
|
||||
this.setBufferRange(this.editor.bufferRangeForBufferRow(row, {includeNewline: true}), options)
|
||||
} else {
|
||||
const startRange = this.editor.bufferRangeForBufferRow(this.marker.getStartBufferPosition().row)
|
||||
const endRange = this.editor.bufferRangeForBufferRow(this.marker.getEndBufferPosition().row, {includeNewline: true})
|
||||
this.setBufferRange(startRange.union(endRange), options)
|
||||
}
|
||||
|
||||
this.linewise = true
|
||||
this.wordwise = false
|
||||
this.initialScreenRange = this.getScreenRange()
|
||||
}
|
||||
|
||||
// Public: Expands the newest selection to include the entire line on which
|
||||
// the cursor currently rests.
|
||||
//
|
||||
// It also includes the newline character.
|
||||
expandOverLine (options) {
|
||||
const range = this.getBufferRange().union(this.cursor.getCurrentLineBufferRange({includeNewline: true}))
|
||||
this.setBufferRange(range, {autoscroll: false})
|
||||
const autoscroll = options && options.autoscroll != null ? options.autoscroll : this.isLastSelection()
|
||||
if (autoscroll) this.cursor.autoscroll()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Modifying the selected text
|
||||
*/
|
||||
|
||||
// Public: Replaces text at the current selection.
|
||||
//
|
||||
// * `text` A {String} representing the text to add
|
||||
// * `options` (optional) {Object} with keys:
|
||||
// * `select` If `true`, selects the newly added text.
|
||||
// * `autoIndent` If `true`, indents all inserted text appropriately.
|
||||
// * `autoIndentNewline` If `true`, indent newline appropriately.
|
||||
// * `autoDecreaseIndent` If `true`, decreases indent level appropriately
|
||||
// (for example, when a closing bracket is inserted).
|
||||
// * `preserveTrailingLineIndentation` By default, when pasting multiple
|
||||
// lines, Atom attempts to preserve the relative indent level between the
|
||||
// first line and trailing lines, even if the indent level of the first
|
||||
// line has changed from the copied text. If this option is `true`, this
|
||||
// behavior is suppressed.
|
||||
// level between the first lines and the trailing lines.
|
||||
// * `normalizeLineEndings` (optional) {Boolean} (default: true)
|
||||
// * `undo` If `skip`, skips the undo stack for this operation.
|
||||
insertText (text, options = {}) {
|
||||
let desiredIndentLevel, indentAdjustment
|
||||
const oldBufferRange = this.getBufferRange()
|
||||
const wasReversed = this.isReversed()
|
||||
this.clear(options)
|
||||
|
||||
let autoIndentFirstLine = false
|
||||
const precedingText = this.editor.getTextInRange([[oldBufferRange.start.row, 0], oldBufferRange.start])
|
||||
const remainingLines = text.split('\n')
|
||||
const firstInsertedLine = remainingLines.shift()
|
||||
|
||||
if (options.indentBasis != null && !options.preserveTrailingLineIndentation) {
|
||||
indentAdjustment = this.editor.indentLevelForLine(precedingText) - options.indentBasis
|
||||
this.adjustIndent(remainingLines, indentAdjustment)
|
||||
}
|
||||
|
||||
const textIsAutoIndentable = (text === '\n') || (text === '\r\n') || NonWhitespaceRegExp.test(text)
|
||||
if (options.autoIndent && textIsAutoIndentable && !NonWhitespaceRegExp.test(precedingText) && (remainingLines.length > 0)) {
|
||||
autoIndentFirstLine = true
|
||||
const firstLine = precedingText + firstInsertedLine
|
||||
desiredIndentLevel = this.editor.tokenizedBuffer.suggestedIndentForLineAtBufferRow(oldBufferRange.start.row, firstLine)
|
||||
indentAdjustment = desiredIndentLevel - this.editor.indentLevelForLine(firstLine)
|
||||
this.adjustIndent(remainingLines, indentAdjustment)
|
||||
}
|
||||
|
||||
text = firstInsertedLine
|
||||
if (remainingLines.length > 0) text += `\n${remainingLines.join('\n')}`
|
||||
|
||||
const newBufferRange = this.editor.buffer.setTextInRange(oldBufferRange, text, pick(options, 'undo', 'normalizeLineEndings'))
|
||||
|
||||
if (options.select) {
|
||||
this.setBufferRange(newBufferRange, {reversed: wasReversed})
|
||||
} else {
|
||||
if (wasReversed) this.cursor.setBufferPosition(newBufferRange.end)
|
||||
}
|
||||
|
||||
if (autoIndentFirstLine) {
|
||||
this.editor.setIndentationForBufferRow(oldBufferRange.start.row, desiredIndentLevel)
|
||||
}
|
||||
|
||||
if (options.autoIndentNewline && (text === '\n')) {
|
||||
this.editor.autoIndentBufferRow(newBufferRange.end.row, {preserveLeadingWhitespace: true, skipBlankLines: false})
|
||||
} else if (options.autoDecreaseIndent && NonWhitespaceRegExp.test(text)) {
|
||||
this.editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row)
|
||||
}
|
||||
|
||||
const autoscroll = options.autoscroll != null ? options.autoscroll : this.isLastSelection()
|
||||
if (autoscroll) this.autoscroll()
|
||||
|
||||
return newBufferRange
|
||||
}
|
||||
|
||||
// Public: Removes the first character before the selection if the selection
|
||||
// is empty otherwise it deletes the selection.
|
||||
backspace () {
|
||||
if (this.isEmpty()) this.selectLeft()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes the selection or, if nothing is selected, then all
|
||||
// characters from the start of the selection back to the previous word
|
||||
// boundary.
|
||||
deleteToPreviousWordBoundary () {
|
||||
if (this.isEmpty()) this.selectToPreviousWordBoundary()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes the selection or, if nothing is selected, then all
|
||||
// characters from the start of the selection up to the next word
|
||||
// boundary.
|
||||
deleteToNextWordBoundary () {
|
||||
if (this.isEmpty()) this.selectToNextWordBoundary()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes from the start of the selection to the beginning of the
|
||||
// current word if the selection is empty otherwise it deletes the selection.
|
||||
deleteToBeginningOfWord () {
|
||||
if (this.isEmpty()) this.selectToBeginningOfWord()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes from the beginning of the line which the selection begins on
|
||||
// all the way through to the end of the selection.
|
||||
deleteToBeginningOfLine () {
|
||||
if (this.isEmpty() && this.cursor.isAtBeginningOfLine()) {
|
||||
this.selectLeft()
|
||||
} else {
|
||||
this.selectToBeginningOfLine()
|
||||
}
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes the selection or the next character after the start of the
|
||||
// selection if the selection is empty.
|
||||
delete () {
|
||||
if (this.isEmpty()) this.selectRight()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: If the selection is empty, removes all text from the cursor to the
|
||||
// end of the line. If the cursor is already at the end of the line, it
|
||||
// removes the following newline. If the selection isn't empty, only deletes
|
||||
// the contents of the selection.
|
||||
deleteToEndOfLine () {
|
||||
if (this.isEmpty()) {
|
||||
if (this.cursor.isAtEndOfLine()) {
|
||||
this.delete()
|
||||
return
|
||||
}
|
||||
this.selectToEndOfLine()
|
||||
}
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes the selection or all characters from the start of the
|
||||
// selection to the end of the current word if nothing is selected.
|
||||
deleteToEndOfWord () {
|
||||
if (this.isEmpty()) this.selectToEndOfWord()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes the selection or all characters from the start of the
|
||||
// selection to the end of the current word if nothing is selected.
|
||||
deleteToBeginningOfSubword () {
|
||||
if (this.isEmpty()) this.selectToPreviousSubwordBoundary()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes the selection or all characters from the start of the
|
||||
// selection to the end of the current word if nothing is selected.
|
||||
deleteToEndOfSubword () {
|
||||
if (this.isEmpty()) this.selectToNextSubwordBoundary()
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
// Public: Removes only the selected text.
|
||||
deleteSelectedText () {
|
||||
const bufferRange = this.getBufferRange()
|
||||
if (!bufferRange.isEmpty()) this.editor.buffer.delete(bufferRange)
|
||||
if (this.cursor) this.cursor.setBufferPosition(bufferRange.start)
|
||||
}
|
||||
|
||||
// Public: Removes the line at the beginning of the selection if the selection
|
||||
// is empty unless the selection spans multiple lines in which case all lines
|
||||
// are removed.
|
||||
deleteLine () {
|
||||
if (this.isEmpty()) {
|
||||
const start = this.cursor.getScreenRow()
|
||||
const range = this.editor.bufferRowsForScreenRows(start, start + 1)
|
||||
if (range[1] > range[0]) {
|
||||
this.editor.buffer.deleteRows(range[0], range[1] - 1)
|
||||
} else {
|
||||
this.editor.buffer.deleteRow(range[0])
|
||||
}
|
||||
} else {
|
||||
const range = this.getBufferRange()
|
||||
const start = range.start.row
|
||||
let end = range.end.row
|
||||
if (end !== this.editor.buffer.getLastRow() && range.end.column === 0) end--
|
||||
this.editor.buffer.deleteRows(start, end)
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Joins the current line with the one below it. Lines will
|
||||
// be separated by a single space.
|
||||
//
|
||||
// If there selection spans more than one line, all the lines are joined together.
|
||||
joinLines () {
|
||||
let joinMarker
|
||||
const selectedRange = this.getBufferRange()
|
||||
if (selectedRange.isEmpty()) {
|
||||
if (selectedRange.start.row === this.editor.buffer.getLastRow()) return
|
||||
} else {
|
||||
joinMarker = this.editor.markBufferRange(selectedRange, {invalidate: 'never'})
|
||||
}
|
||||
|
||||
const rowCount = Math.max(1, selectedRange.getRowCount() - 1)
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
this.cursor.setBufferPosition([selectedRange.start.row])
|
||||
this.cursor.moveToEndOfLine()
|
||||
|
||||
// Remove trailing whitespace from the current line
|
||||
const scanRange = this.cursor.getCurrentLineBufferRange()
|
||||
let trailingWhitespaceRange = null
|
||||
this.editor.scanInBufferRange(/[ \t]+$/, scanRange, ({range}) => {
|
||||
trailingWhitespaceRange = range
|
||||
})
|
||||
if (trailingWhitespaceRange) {
|
||||
this.setBufferRange(trailingWhitespaceRange)
|
||||
this.deleteSelectedText()
|
||||
}
|
||||
|
||||
const currentRow = selectedRange.start.row
|
||||
const nextRow = currentRow + 1
|
||||
const insertSpace =
|
||||
(nextRow <= this.editor.buffer.getLastRow()) &&
|
||||
(this.editor.buffer.lineLengthForRow(nextRow) > 0) &&
|
||||
(this.editor.buffer.lineLengthForRow(currentRow) > 0)
|
||||
if (insertSpace) this.insertText(' ')
|
||||
|
||||
this.cursor.moveToEndOfLine()
|
||||
|
||||
// Remove leading whitespace from the line below
|
||||
this.modifySelection(() => {
|
||||
this.cursor.moveRight()
|
||||
this.cursor.moveToFirstCharacterOfLine()
|
||||
})
|
||||
this.deleteSelectedText()
|
||||
|
||||
if (insertSpace) this.cursor.moveLeft()
|
||||
}
|
||||
|
||||
if (joinMarker) {
|
||||
const newSelectedRange = joinMarker.getBufferRange()
|
||||
this.setBufferRange(newSelectedRange)
|
||||
joinMarker.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Removes one level of indent from the currently selected rows.
|
||||
outdentSelectedRows () {
|
||||
const [start, end] = this.getBufferRowRange()
|
||||
const {buffer} = this.editor
|
||||
const leadingTabRegex = new RegExp(`^( {1,${this.editor.getTabLength()}}|\t)`)
|
||||
for (let row = start; row <= end; row++) {
|
||||
const match = buffer.lineForRow(row).match(leadingTabRegex)
|
||||
if (match && match[0].length > 0) {
|
||||
buffer.delete([[row, 0], [row, match[0].length]])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Sets the indentation level of all selected rows to values suggested
|
||||
// by the relevant grammars.
|
||||
autoIndentSelectedRows () {
|
||||
const [start, end] = this.getBufferRowRange()
|
||||
return this.editor.autoIndentBufferRows(start, end)
|
||||
}
|
||||
|
||||
// Public: Wraps the selected lines in comments if they aren't currently part
|
||||
// of a comment.
|
||||
//
|
||||
// Removes the comment if they are currently wrapped in a comment.
|
||||
toggleLineComments () {
|
||||
this.editor.toggleLineCommentsForBufferRows(...(this.getBufferRowRange() || []))
|
||||
}
|
||||
|
||||
// Public: Cuts the selection until the end of the screen line.
|
||||
cutToEndOfLine (maintainClipboard) {
|
||||
if (this.isEmpty()) this.selectToEndOfLine()
|
||||
return this.cut(maintainClipboard)
|
||||
}
|
||||
|
||||
// Public: Cuts the selection until the end of the buffer line.
|
||||
cutToEndOfBufferLine (maintainClipboard) {
|
||||
if (this.isEmpty()) this.selectToEndOfBufferLine()
|
||||
this.cut(maintainClipboard)
|
||||
}
|
||||
|
||||
// Public: Copies the selection to the clipboard and then deletes it.
|
||||
//
|
||||
// * `maintainClipboard` {Boolean} (default: false) See {::copy}
|
||||
// * `fullLine` {Boolean} (default: false) See {::copy}
|
||||
cut (maintainClipboard = false, fullLine = false) {
|
||||
this.copy(maintainClipboard, fullLine)
|
||||
this.delete()
|
||||
}
|
||||
|
||||
// Public: Copies the current selection to the clipboard.
|
||||
//
|
||||
// * `maintainClipboard` {Boolean} if `true`, a specific metadata property
|
||||
// is created to store each content copied to the clipboard. The clipboard
|
||||
// `text` still contains the concatenation of the clipboard with the
|
||||
// current selection. (default: false)
|
||||
// * `fullLine` {Boolean} if `true`, the copied text will always be pasted
|
||||
// at the beginning of the line containing the cursor, regardless of the
|
||||
// cursor's horizontal position. (default: false)
|
||||
copy (maintainClipboard = false, fullLine = false) {
|
||||
if (this.isEmpty()) return
|
||||
const {start, end} = this.getBufferRange()
|
||||
const selectionText = this.editor.getTextInRange([start, end])
|
||||
const precedingText = this.editor.getTextInRange([[start.row, 0], start])
|
||||
const startLevel = this.editor.indentLevelForLine(precedingText)
|
||||
|
||||
if (maintainClipboard) {
|
||||
let {text: clipboardText, metadata} = this.editor.constructor.clipboard.readWithMetadata()
|
||||
if (!metadata) metadata = {}
|
||||
if (!metadata.selections) {
|
||||
metadata.selections = [{
|
||||
text: clipboardText,
|
||||
indentBasis: metadata.indentBasis,
|
||||
fullLine: metadata.fullLine
|
||||
}]
|
||||
}
|
||||
metadata.selections.push({
|
||||
text: selectionText,
|
||||
indentBasis: startLevel,
|
||||
fullLine
|
||||
})
|
||||
this.editor.constructor.clipboard.write([clipboardText, selectionText].join('\n'), metadata)
|
||||
} else {
|
||||
this.editor.constructor.clipboard.write(selectionText, {
|
||||
indentBasis: startLevel,
|
||||
fullLine
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Creates a fold containing the current selection.
|
||||
fold () {
|
||||
const range = this.getBufferRange()
|
||||
if (!range.isEmpty()) {
|
||||
this.editor.foldBufferRange(range)
|
||||
this.cursor.setBufferPosition(range.end)
|
||||
}
|
||||
}
|
||||
|
||||
// Private: Increase the indentation level of the given text by given number
|
||||
// of levels. Leaves the first line unchanged.
|
||||
adjustIndent (lines, indentAdjustment) {
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
if (indentAdjustment === 0 || line === '') {
|
||||
continue
|
||||
} else if (indentAdjustment > 0) {
|
||||
lines[i] = this.editor.buildIndentString(indentAdjustment) + line
|
||||
} else {
|
||||
const currentIndentLevel = this.editor.indentLevelForLine(lines[i])
|
||||
const indentLevel = Math.max(0, currentIndentLevel + indentAdjustment)
|
||||
lines[i] = line.replace(/^[\t ]+/, this.editor.buildIndentString(indentLevel))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Indent the current line(s).
|
||||
//
|
||||
// If the selection is empty, indents the current line if the cursor precedes
|
||||
// non-whitespace characters, and otherwise inserts a tab. If the selection is
|
||||
// non empty, calls {::indentSelectedRows}.
|
||||
//
|
||||
// * `options` (optional) {Object} with the keys:
|
||||
// * `autoIndent` If `true`, the line is indented to an automatically-inferred
|
||||
// level. Otherwise, {TextEditor::getTabText} is inserted.
|
||||
indent ({autoIndent} = {}) {
|
||||
const {row} = this.cursor.getBufferPosition()
|
||||
|
||||
if (this.isEmpty()) {
|
||||
this.cursor.skipLeadingWhitespace()
|
||||
const desiredIndent = this.editor.suggestedIndentForBufferRow(row)
|
||||
let delta = desiredIndent - this.cursor.getIndentLevel()
|
||||
|
||||
if (autoIndent && delta > 0) {
|
||||
if (!this.editor.getSoftTabs()) delta = Math.max(delta, 1)
|
||||
this.insertText(this.editor.buildIndentString(delta))
|
||||
} else {
|
||||
this.insertText(this.editor.buildIndentString(1, this.cursor.getBufferColumn()))
|
||||
}
|
||||
} else {
|
||||
this.indentSelectedRows()
|
||||
}
|
||||
}
|
||||
|
||||
// Public: If the selection spans multiple rows, indent all of them.
|
||||
indentSelectedRows () {
|
||||
const [start, end] = this.getBufferRowRange()
|
||||
for (let row = start; row <= end; row++) {
|
||||
if (this.editor.buffer.lineLengthForRow(row) !== 0) {
|
||||
this.editor.buffer.insert([row, 0], this.editor.getTabText())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Managing multiple selections
|
||||
*/
|
||||
|
||||
// Public: Moves the selection down one row.
|
||||
addSelectionBelow () {
|
||||
const range = this.getGoalScreenRange().copy()
|
||||
const nextRow = range.end.row + 1
|
||||
|
||||
for (let row = nextRow, end = this.editor.getLastScreenRow(); row <= end; row++) {
|
||||
range.start.row = row
|
||||
range.end.row = row
|
||||
const clippedRange = this.editor.clipScreenRange(range, {skipSoftWrapIndentation: true})
|
||||
|
||||
if (range.isEmpty()) {
|
||||
if (range.end.column > 0 && clippedRange.end.column === 0) continue
|
||||
} else {
|
||||
if (clippedRange.isEmpty()) continue
|
||||
}
|
||||
|
||||
const selection = this.editor.addSelectionForScreenRange(clippedRange)
|
||||
selection.setGoalScreenRange(range)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Moves the selection up one row.
|
||||
addSelectionAbove () {
|
||||
const range = this.getGoalScreenRange().copy()
|
||||
const previousRow = range.end.row - 1
|
||||
|
||||
for (let row = previousRow; row >= 0; row--) {
|
||||
range.start.row = row
|
||||
range.end.row = row
|
||||
const clippedRange = this.editor.clipScreenRange(range, {skipSoftWrapIndentation: true})
|
||||
|
||||
if (range.isEmpty()) {
|
||||
if (range.end.column > 0 && clippedRange.end.column === 0) continue
|
||||
} else {
|
||||
if (clippedRange.isEmpty()) continue
|
||||
}
|
||||
|
||||
const selection = this.editor.addSelectionForScreenRange(clippedRange)
|
||||
selection.setGoalScreenRange(range)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Combines the given selection into this selection and then destroys
|
||||
// the given selection.
|
||||
//
|
||||
// * `otherSelection` A {Selection} to merge with.
|
||||
// * `options` (optional) {Object} options matching those found in {::setBufferRange}.
|
||||
merge (otherSelection, options = {}) {
|
||||
const myGoalScreenRange = this.getGoalScreenRange()
|
||||
const otherGoalScreenRange = otherSelection.getGoalScreenRange()
|
||||
|
||||
if (myGoalScreenRange && otherGoalScreenRange) {
|
||||
options.goalScreenRange = myGoalScreenRange.union(otherGoalScreenRange)
|
||||
} else {
|
||||
options.goalScreenRange = myGoalScreenRange || otherGoalScreenRange
|
||||
}
|
||||
|
||||
const bufferRange = this.getBufferRange().union(otherSelection.getBufferRange())
|
||||
this.setBufferRange(bufferRange, Object.assign({autoscroll: false}, options))
|
||||
otherSelection.destroy()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Comparing to other selections
|
||||
*/
|
||||
|
||||
// Public: Compare this selection's buffer range to another selection's buffer
|
||||
// range.
|
||||
//
|
||||
// See {Range::compare} for more details.
|
||||
//
|
||||
// * `otherSelection` A {Selection} to compare against
|
||||
compare (otherSelection) {
|
||||
return this.marker.compare(otherSelection.marker)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Private Utilities
|
||||
*/
|
||||
|
||||
setGoalScreenRange (range) {
|
||||
this.goalScreenRange = Range.fromObject(range)
|
||||
}
|
||||
|
||||
getGoalScreenRange () {
|
||||
return this.goalScreenRange || this.getScreenRange()
|
||||
}
|
||||
|
||||
markerDidChange (e) {
|
||||
const {oldHeadBufferPosition, oldTailBufferPosition, newHeadBufferPosition} = e
|
||||
const {oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e
|
||||
const {textChanged} = e
|
||||
|
||||
if (!oldHeadScreenPosition.isEqual(newHeadScreenPosition)) {
|
||||
this.cursor.goalColumn = null
|
||||
const cursorMovedEvent = {
|
||||
oldBufferPosition: oldHeadBufferPosition,
|
||||
oldScreenPosition: oldHeadScreenPosition,
|
||||
newBufferPosition: newHeadBufferPosition,
|
||||
newScreenPosition: newHeadScreenPosition,
|
||||
textChanged,
|
||||
cursor: this.cursor
|
||||
}
|
||||
this.cursor.emitter.emit('did-change-position', cursorMovedEvent)
|
||||
this.editor.cursorMoved(cursorMovedEvent)
|
||||
}
|
||||
|
||||
this.emitter.emit('did-change-range')
|
||||
this.editor.selectionRangeChanged({
|
||||
oldBufferRange: new Range(oldHeadBufferPosition, oldTailBufferPosition),
|
||||
oldScreenRange: new Range(oldHeadScreenPosition, oldTailScreenPosition),
|
||||
newBufferRange: this.getBufferRange(),
|
||||
newScreenRange: this.getScreenRange(),
|
||||
selection: this
|
||||
})
|
||||
}
|
||||
|
||||
markerDidDestroy () {
|
||||
if (this.editor.isDestroyed()) return
|
||||
|
||||
this.destroyed = true
|
||||
this.cursor.destroyed = true
|
||||
|
||||
this.editor.removeSelection(this)
|
||||
|
||||
this.cursor.emitter.emit('did-destroy')
|
||||
this.emitter.emit('did-destroy')
|
||||
|
||||
this.cursor.emitter.dispose()
|
||||
this.emitter.dispose()
|
||||
}
|
||||
|
||||
finalize () {
|
||||
if (!this.initialScreenRange || !this.initialScreenRange.isEqual(this.getScreenRange())) {
|
||||
this.initialScreenRange = null
|
||||
}
|
||||
if (this.isEmpty()) {
|
||||
this.wordwise = false
|
||||
this.linewise = false
|
||||
}
|
||||
}
|
||||
|
||||
autoscroll (options) {
|
||||
if (this.marker.hasTail()) {
|
||||
this.editor.scrollToScreenRange(this.getScreenRange(), Object.assign({reversed: this.isReversed()}, options))
|
||||
} else {
|
||||
this.cursor.autoscroll(options)
|
||||
}
|
||||
}
|
||||
|
||||
clearAutoscroll () {}
|
||||
|
||||
modifySelection (fn) {
|
||||
this.retainSelection = true
|
||||
this.plantTail()
|
||||
fn()
|
||||
this.retainSelection = false
|
||||
}
|
||||
|
||||
// Sets the marker's tail to the same position as the marker's head.
|
||||
//
|
||||
// This only works if there isn't already a tail position.
|
||||
//
|
||||
// Returns a {Point} representing the new tail position.
|
||||
plantTail () {
|
||||
this.marker.plantTail()
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,6 @@ class TextEditorComponent {
|
||||
this.blockDecorationResizeObserver = new ResizeObserver(this.didResizeBlockDecorations.bind(this))
|
||||
this.lineComponentsByScreenLineId = new Map()
|
||||
this.overlayComponents = new Set()
|
||||
this.overlayDimensionsByElement = new WeakMap()
|
||||
this.shouldRenderDummyScrollbars = true
|
||||
this.remeasureScrollbars = false
|
||||
this.pendingAutoscroll = null
|
||||
@@ -803,8 +802,10 @@ class TextEditorComponent {
|
||||
{
|
||||
key: overlayProps.element,
|
||||
overlayComponents: this.overlayComponents,
|
||||
measuredDimensions: this.overlayDimensionsByElement.get(overlayProps.element),
|
||||
didResize: () => { this.updateSync() }
|
||||
didResize: (overlayComponent) => {
|
||||
this.updateOverlayToRender(overlayProps)
|
||||
overlayComponent.update(overlayProps)
|
||||
}
|
||||
},
|
||||
overlayProps
|
||||
))
|
||||
@@ -1339,42 +1340,46 @@ class TextEditorComponent {
|
||||
})
|
||||
}
|
||||
|
||||
updateOverlayToRender (decoration) {
|
||||
const windowInnerHeight = this.getWindowInnerHeight()
|
||||
const windowInnerWidth = this.getWindowInnerWidth()
|
||||
const contentClientRect = this.refs.content.getBoundingClientRect()
|
||||
|
||||
const {element, screenPosition, avoidOverflow} = decoration
|
||||
const {row, column} = screenPosition
|
||||
let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight()
|
||||
let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column)
|
||||
const clientRect = element.getBoundingClientRect()
|
||||
|
||||
if (avoidOverflow !== false) {
|
||||
const computedStyle = window.getComputedStyle(element)
|
||||
const elementTop = wrapperTop + parseInt(computedStyle.marginTop)
|
||||
const elementBottom = elementTop + clientRect.height
|
||||
const flippedElementTop = wrapperTop - this.getLineHeight() - clientRect.height - parseInt(computedStyle.marginBottom)
|
||||
const elementLeft = wrapperLeft + parseInt(computedStyle.marginLeft)
|
||||
const elementRight = elementLeft + clientRect.width
|
||||
|
||||
if (elementBottom > windowInnerHeight && flippedElementTop >= 0) {
|
||||
wrapperTop -= (elementTop - flippedElementTop)
|
||||
}
|
||||
if (elementLeft < 0) {
|
||||
wrapperLeft -= elementLeft
|
||||
} else if (elementRight > windowInnerWidth) {
|
||||
wrapperLeft -= (elementRight - windowInnerWidth)
|
||||
}
|
||||
}
|
||||
|
||||
decoration.pixelTop = Math.round(wrapperTop)
|
||||
decoration.pixelLeft = Math.round(wrapperLeft)
|
||||
}
|
||||
|
||||
updateOverlaysToRender () {
|
||||
const overlayCount = this.decorationsToRender.overlays.length
|
||||
if (overlayCount === 0) return null
|
||||
|
||||
const windowInnerHeight = this.getWindowInnerHeight()
|
||||
const windowInnerWidth = this.getWindowInnerWidth()
|
||||
const contentClientRect = this.refs.content.getBoundingClientRect()
|
||||
for (let i = 0; i < overlayCount; i++) {
|
||||
const decoration = this.decorationsToRender.overlays[i]
|
||||
const {element, screenPosition, avoidOverflow} = decoration
|
||||
const {row, column} = screenPosition
|
||||
let wrapperTop = contentClientRect.top + this.pixelPositionAfterBlocksForRow(row) + this.getLineHeight()
|
||||
let wrapperLeft = contentClientRect.left + this.pixelLeftForRowAndColumn(row, column)
|
||||
const clientRect = element.getBoundingClientRect()
|
||||
this.overlayDimensionsByElement.set(element, clientRect)
|
||||
|
||||
if (avoidOverflow !== false) {
|
||||
const computedStyle = window.getComputedStyle(element)
|
||||
const elementTop = wrapperTop + parseInt(computedStyle.marginTop)
|
||||
const elementBottom = elementTop + clientRect.height
|
||||
const flippedElementTop = wrapperTop - this.getLineHeight() - clientRect.height - parseInt(computedStyle.marginBottom)
|
||||
const elementLeft = wrapperLeft + parseInt(computedStyle.marginLeft)
|
||||
const elementRight = elementLeft + clientRect.width
|
||||
|
||||
if (elementBottom > windowInnerHeight && flippedElementTop >= 0) {
|
||||
wrapperTop -= (elementTop - flippedElementTop)
|
||||
}
|
||||
if (elementLeft < 0) {
|
||||
wrapperLeft -= elementLeft
|
||||
} else if (elementRight > windowInnerWidth) {
|
||||
wrapperLeft -= (elementRight - windowInnerWidth)
|
||||
}
|
||||
}
|
||||
|
||||
decoration.pixelTop = Math.round(wrapperTop)
|
||||
decoration.pixelLeft = Math.round(wrapperLeft)
|
||||
this.updateOverlayToRender(decoration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1603,11 +1608,23 @@ class TextEditorComponent {
|
||||
if (this.isInputEnabled()) {
|
||||
event.stopPropagation()
|
||||
|
||||
// WARNING: If we call preventDefault on the input of a space character,
|
||||
// then the browser interprets the spacebar keypress as a page-down command,
|
||||
// causing spaces to scroll elements containing editors. This is impossible
|
||||
// to test.
|
||||
if (event.data !== ' ') event.preventDefault()
|
||||
// WARNING: If we call preventDefault on the input of a space
|
||||
// character, then the browser interprets the spacebar keypress as a
|
||||
// page-down command, causing spaces to scroll elements containing
|
||||
// editors. This means typing space will actually change the contents
|
||||
// of the hidden input, which will cause the browser to autoscroll the
|
||||
// scroll container to reveal the input if it is off screen (See
|
||||
// https://github.com/atom/atom/issues/16046). To correct for this
|
||||
// situation, we automatically reset the scroll position to 0,0 after
|
||||
// typing a space. None of this can really be tested.
|
||||
if (event.data === ' ') {
|
||||
window.setImmediate(() => {
|
||||
this.refs.scrollContainer.scrollTop = 0
|
||||
this.refs.scrollContainer.scrollLeft = 0
|
||||
})
|
||||
} else {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
// If the input event is fired while the accented character menu is open it
|
||||
// means that the user has chosen one of the accented alternatives. Thus, we
|
||||
@@ -1640,8 +1657,11 @@ class TextEditorComponent {
|
||||
didKeydown (event) {
|
||||
// Stop dragging when user interacts with the keyboard. This prevents
|
||||
// unwanted selections in the case edits are performed while selecting text
|
||||
// at the same time.
|
||||
if (this.stopDragging) this.stopDragging()
|
||||
// at the same time. Modifier keys are exempt to preserve the ability to
|
||||
// add selections, shift-scroll horizontally while selecting.
|
||||
if (this.stopDragging && event.key !== 'Control' && event.key !== 'Alt' && event.key !== 'Meta' && event.key !== 'Shift') {
|
||||
this.stopDragging()
|
||||
}
|
||||
|
||||
if (this.lastKeydownBeforeKeypress != null) {
|
||||
if (this.lastKeydownBeforeKeypress.code === event.code) {
|
||||
@@ -1758,7 +1778,7 @@ class TextEditorComponent {
|
||||
|
||||
if (target && target.matches('.fold-marker')) {
|
||||
const bufferPosition = model.bufferPositionForScreenPosition(screenPosition)
|
||||
model.destroyFoldsIntersectingBufferRange(Range(bufferPosition, bufferPosition))
|
||||
model.destroyFoldsContainingBufferPositions([bufferPosition], false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2443,8 +2463,12 @@ class TextEditorComponent {
|
||||
|
||||
didChangeDisplayLayer (changes) {
|
||||
for (let i = 0; i < changes.length; i++) {
|
||||
const {start, oldExtent, newExtent} = changes[i]
|
||||
this.spliceLineTopIndex(start.row, oldExtent.row, newExtent.row)
|
||||
const {oldRange, newRange} = changes[i]
|
||||
this.spliceLineTopIndex(
|
||||
newRange.start.row,
|
||||
oldRange.end.row - oldRange.start.row,
|
||||
newRange.end.row - newRange.start.row
|
||||
)
|
||||
}
|
||||
|
||||
this.scheduleUpdate()
|
||||
@@ -4194,17 +4218,26 @@ class OverlayComponent {
|
||||
this.element.style.zIndex = 4
|
||||
this.element.style.top = (this.props.pixelTop || 0) + 'px'
|
||||
this.element.style.left = (this.props.pixelLeft || 0) + 'px'
|
||||
this.currentContentRect = null
|
||||
|
||||
// Synchronous DOM updates in response to resize events might trigger a
|
||||
// "loop limit exceeded" error. We disconnect the observer before
|
||||
// potentially mutating the DOM, and then reconnect it on the next tick.
|
||||
// Note: ResizeObserver calls its callback when .observe is called
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
const {contentRect} = entries[0]
|
||||
if (contentRect.width !== this.props.measuredDimensions.width || contentRect.height !== this.props.measuredDimensions.height) {
|
||||
|
||||
if (
|
||||
this.currentContentRect &&
|
||||
(this.currentContentRect.width !== contentRect.width ||
|
||||
this.currentContentRect.height !== contentRect.height)
|
||||
) {
|
||||
this.resizeObserver.disconnect()
|
||||
this.props.didResize()
|
||||
this.props.didResize(this)
|
||||
process.nextTick(() => { this.resizeObserver.observe(this.props.element) })
|
||||
}
|
||||
|
||||
this.currentContentRect = contentRect
|
||||
})
|
||||
this.didAttach()
|
||||
this.props.overlayComponents.add(this)
|
||||
@@ -4215,15 +4248,30 @@ class OverlayComponent {
|
||||
this.didDetach()
|
||||
}
|
||||
|
||||
getNextUpdatePromise () {
|
||||
if (!this.nextUpdatePromise) {
|
||||
this.nextUpdatePromise = new Promise((resolve) => {
|
||||
this.resolveNextUpdatePromise = () => {
|
||||
this.nextUpdatePromise = null
|
||||
this.resolveNextUpdatePromise = null
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
return this.nextUpdatePromise
|
||||
}
|
||||
|
||||
update (newProps) {
|
||||
const oldProps = this.props
|
||||
this.props = newProps
|
||||
this.props = Object.assign({}, oldProps, newProps)
|
||||
if (this.props.pixelTop != null) this.element.style.top = this.props.pixelTop + 'px'
|
||||
if (this.props.pixelLeft != null) this.element.style.left = this.props.pixelLeft + 'px'
|
||||
if (newProps.className !== oldProps.className) {
|
||||
if (oldProps.className != null) this.element.classList.remove(oldProps.className)
|
||||
if (newProps.className != null) this.element.classList.add(newProps.className)
|
||||
}
|
||||
|
||||
if (this.resolveNextUpdatePromise) this.resolveNextUpdatePromise()
|
||||
}
|
||||
|
||||
didAttach () {
|
||||
|
||||
@@ -288,7 +288,7 @@ export default class TextEditorRegistry {
|
||||
|
||||
let currentScore = this.editorGrammarScores.get(editor)
|
||||
if (currentScore == null || score > currentScore) {
|
||||
editor.setGrammar(grammar, score)
|
||||
editor.setGrammar(grammar)
|
||||
this.editorGrammarScores.set(editor, score)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
4587
src/text-editor.js
Normal file
4587
src/text-editor.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,322 +0,0 @@
|
||||
path = require 'path'
|
||||
_ = require 'underscore-plus'
|
||||
{Emitter, CompositeDisposable} = require 'event-kit'
|
||||
{File} = require 'pathwatcher'
|
||||
fs = require 'fs-plus'
|
||||
LessCompileCache = require './less-compile-cache'
|
||||
|
||||
# Extended: Handles loading and activating available themes.
|
||||
#
|
||||
# An instance of this class is always available as the `atom.themes` global.
|
||||
module.exports =
|
||||
class ThemeManager
|
||||
constructor: ({@packageManager, @config, @styleManager, @notificationManager, @viewRegistry}) ->
|
||||
@emitter = new Emitter
|
||||
@styleSheetDisposablesBySourcePath = {}
|
||||
@lessCache = null
|
||||
@initialLoadComplete = false
|
||||
@packageManager.registerPackageActivator(this, ['theme'])
|
||||
@packageManager.onDidActivateInitialPackages =>
|
||||
@onDidChangeActiveThemes => @packageManager.reloadActivePackageStyleSheets()
|
||||
|
||||
initialize: ({@resourcePath, @configDirPath, @safeMode, devMode}) ->
|
||||
@lessSourcesByRelativeFilePath = null
|
||||
if devMode or typeof snapshotAuxiliaryData is 'undefined'
|
||||
@lessSourcesByRelativeFilePath = {}
|
||||
@importedFilePathsByRelativeImportPath = {}
|
||||
else
|
||||
@lessSourcesByRelativeFilePath = snapshotAuxiliaryData.lessSourcesByRelativeFilePath
|
||||
@importedFilePathsByRelativeImportPath = snapshotAuxiliaryData.importedFilePathsByRelativeImportPath
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
|
||||
# Essential: Invoke `callback` when style sheet changes associated with
|
||||
# updating the list of active themes have completed.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
onDidChangeActiveThemes: (callback) ->
|
||||
@emitter.on 'did-change-active-themes', callback
|
||||
|
||||
###
|
||||
Section: Accessing Available Themes
|
||||
###
|
||||
|
||||
getAvailableNames: ->
|
||||
# TODO: Maybe should change to list all the available themes out there?
|
||||
@getLoadedNames()
|
||||
|
||||
###
|
||||
Section: Accessing Loaded Themes
|
||||
###
|
||||
|
||||
# Public: Returns an {Array} of {String}s of all the loaded theme names.
|
||||
getLoadedThemeNames: ->
|
||||
theme.name for theme in @getLoadedThemes()
|
||||
|
||||
# Public: Returns an {Array} of all the loaded themes.
|
||||
getLoadedThemes: ->
|
||||
pack for pack in @packageManager.getLoadedPackages() when pack.isTheme()
|
||||
|
||||
###
|
||||
Section: Accessing Active Themes
|
||||
###
|
||||
|
||||
# Public: Returns an {Array} of {String}s all the active theme names.
|
||||
getActiveThemeNames: ->
|
||||
theme.name for theme in @getActiveThemes()
|
||||
|
||||
# Public: Returns an {Array} of all the active themes.
|
||||
getActiveThemes: ->
|
||||
pack for pack in @packageManager.getActivePackages() when pack.isTheme()
|
||||
|
||||
activatePackages: -> @activateThemes()
|
||||
|
||||
###
|
||||
Section: Managing Enabled Themes
|
||||
###
|
||||
|
||||
warnForNonExistentThemes: ->
|
||||
themeNames = @config.get('core.themes') ? []
|
||||
themeNames = [themeNames] unless _.isArray(themeNames)
|
||||
for themeName in themeNames
|
||||
unless themeName and typeof themeName is 'string' and @packageManager.resolvePackagePath(themeName)
|
||||
console.warn("Enabled theme '#{themeName}' is not installed.")
|
||||
|
||||
# Public: Get the enabled theme names from the config.
|
||||
#
|
||||
# Returns an array of theme names in the order that they should be activated.
|
||||
getEnabledThemeNames: ->
|
||||
themeNames = @config.get('core.themes') ? []
|
||||
themeNames = [themeNames] unless _.isArray(themeNames)
|
||||
themeNames = themeNames.filter (themeName) =>
|
||||
if themeName and typeof themeName is 'string'
|
||||
return true if @packageManager.resolvePackagePath(themeName)
|
||||
false
|
||||
|
||||
# Use a built-in syntax and UI theme any time the configured themes are not
|
||||
# available.
|
||||
if themeNames.length < 2
|
||||
builtInThemeNames = [
|
||||
'atom-dark-syntax'
|
||||
'atom-dark-ui'
|
||||
'atom-light-syntax'
|
||||
'atom-light-ui'
|
||||
'base16-tomorrow-dark-theme'
|
||||
'base16-tomorrow-light-theme'
|
||||
'solarized-dark-syntax'
|
||||
'solarized-light-syntax'
|
||||
]
|
||||
themeNames = _.intersection(themeNames, builtInThemeNames)
|
||||
if themeNames.length is 0
|
||||
themeNames = ['atom-dark-syntax', 'atom-dark-ui']
|
||||
else if themeNames.length is 1
|
||||
if _.endsWith(themeNames[0], '-ui')
|
||||
themeNames.unshift('atom-dark-syntax')
|
||||
else
|
||||
themeNames.push('atom-dark-ui')
|
||||
|
||||
# Reverse so the first (top) theme is loaded after the others. We want
|
||||
# the first/top theme to override later themes in the stack.
|
||||
themeNames.reverse()
|
||||
|
||||
###
|
||||
Section: Private
|
||||
###
|
||||
|
||||
# Resolve and apply the stylesheet specified by the path.
|
||||
#
|
||||
# This supports both CSS and Less stylesheets.
|
||||
#
|
||||
# * `stylesheetPath` A {String} path to the stylesheet that can be an absolute
|
||||
# path or a relative path that will be resolved against the load path.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to remove the
|
||||
# required stylesheet.
|
||||
requireStylesheet: (stylesheetPath, priority, skipDeprecatedSelectorsTransformation) ->
|
||||
if fullPath = @resolveStylesheet(stylesheetPath)
|
||||
content = @loadStylesheet(fullPath)
|
||||
@applyStylesheet(fullPath, content, priority, skipDeprecatedSelectorsTransformation)
|
||||
else
|
||||
throw new Error("Could not find a file at path '#{stylesheetPath}'")
|
||||
|
||||
unwatchUserStylesheet: ->
|
||||
@userStylesheetSubscriptions?.dispose()
|
||||
@userStylesheetSubscriptions = null
|
||||
@userStylesheetFile = null
|
||||
@userStyleSheetDisposable?.dispose()
|
||||
@userStyleSheetDisposable = null
|
||||
|
||||
loadUserStylesheet: ->
|
||||
@unwatchUserStylesheet()
|
||||
|
||||
userStylesheetPath = @styleManager.getUserStyleSheetPath()
|
||||
return unless fs.isFileSync(userStylesheetPath)
|
||||
|
||||
try
|
||||
@userStylesheetFile = new File(userStylesheetPath)
|
||||
@userStylesheetSubscriptions = new CompositeDisposable()
|
||||
reloadStylesheet = => @loadUserStylesheet()
|
||||
@userStylesheetSubscriptions.add(@userStylesheetFile.onDidChange(reloadStylesheet))
|
||||
@userStylesheetSubscriptions.add(@userStylesheetFile.onDidRename(reloadStylesheet))
|
||||
@userStylesheetSubscriptions.add(@userStylesheetFile.onDidDelete(reloadStylesheet))
|
||||
catch error
|
||||
message = """
|
||||
Unable to watch path: `#{path.basename(userStylesheetPath)}`. Make sure
|
||||
you have permissions to `#{userStylesheetPath}`.
|
||||
|
||||
On linux there are currently problems with watch sizes. See
|
||||
[this document][watches] for more info.
|
||||
[watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path
|
||||
"""
|
||||
@notificationManager.addError(message, dismissable: true)
|
||||
|
||||
try
|
||||
userStylesheetContents = @loadStylesheet(userStylesheetPath, true)
|
||||
catch
|
||||
return
|
||||
|
||||
@userStyleSheetDisposable = @styleManager.addStyleSheet(userStylesheetContents, sourcePath: userStylesheetPath, priority: 2)
|
||||
|
||||
loadBaseStylesheets: ->
|
||||
@reloadBaseStylesheets()
|
||||
|
||||
reloadBaseStylesheets: ->
|
||||
@requireStylesheet('../static/atom', -2, true)
|
||||
|
||||
stylesheetElementForId: (id) ->
|
||||
escapedId = id.replace(/\\/g, '\\\\')
|
||||
document.head.querySelector("atom-styles style[source-path=\"#{escapedId}\"]")
|
||||
|
||||
resolveStylesheet: (stylesheetPath) ->
|
||||
if path.extname(stylesheetPath).length > 0
|
||||
fs.resolveOnLoadPath(stylesheetPath)
|
||||
else
|
||||
fs.resolveOnLoadPath(stylesheetPath, ['css', 'less'])
|
||||
|
||||
loadStylesheet: (stylesheetPath, importFallbackVariables) ->
|
||||
if path.extname(stylesheetPath) is '.less'
|
||||
@loadLessStylesheet(stylesheetPath, importFallbackVariables)
|
||||
else
|
||||
fs.readFileSync(stylesheetPath, 'utf8')
|
||||
|
||||
loadLessStylesheet: (lessStylesheetPath, importFallbackVariables=false) ->
|
||||
@lessCache ?= new LessCompileCache({
|
||||
@resourcePath,
|
||||
@lessSourcesByRelativeFilePath,
|
||||
@importedFilePathsByRelativeImportPath,
|
||||
importPaths: @getImportPaths()
|
||||
})
|
||||
|
||||
try
|
||||
if importFallbackVariables
|
||||
baseVarImports = """
|
||||
@import "variables/ui-variables";
|
||||
@import "variables/syntax-variables";
|
||||
"""
|
||||
relativeFilePath = path.relative(@resourcePath, lessStylesheetPath)
|
||||
lessSource = @lessSourcesByRelativeFilePath[relativeFilePath]
|
||||
if lessSource?
|
||||
content = lessSource.content
|
||||
digest = lessSource.digest
|
||||
else
|
||||
content = baseVarImports + '\n' + fs.readFileSync(lessStylesheetPath, 'utf8')
|
||||
digest = null
|
||||
|
||||
@lessCache.cssForFile(lessStylesheetPath, content, digest)
|
||||
else
|
||||
@lessCache.read(lessStylesheetPath)
|
||||
catch error
|
||||
error.less = true
|
||||
if error.line?
|
||||
# Adjust line numbers for import fallbacks
|
||||
error.line -= 2 if importFallbackVariables
|
||||
|
||||
message = "Error compiling Less stylesheet: `#{lessStylesheetPath}`"
|
||||
detail = """
|
||||
Line number: #{error.line}
|
||||
#{error.message}
|
||||
"""
|
||||
else
|
||||
message = "Error loading Less stylesheet: `#{lessStylesheetPath}`"
|
||||
detail = error.message
|
||||
|
||||
@notificationManager.addError(message, {detail, dismissable: true})
|
||||
throw error
|
||||
|
||||
removeStylesheet: (stylesheetPath) ->
|
||||
@styleSheetDisposablesBySourcePath[stylesheetPath]?.dispose()
|
||||
|
||||
applyStylesheet: (path, text, priority, skipDeprecatedSelectorsTransformation) ->
|
||||
@styleSheetDisposablesBySourcePath[path] = @styleManager.addStyleSheet(
|
||||
text,
|
||||
{
|
||||
priority,
|
||||
skipDeprecatedSelectorsTransformation,
|
||||
sourcePath: path
|
||||
}
|
||||
)
|
||||
|
||||
activateThemes: ->
|
||||
new Promise (resolve) =>
|
||||
# @config.observe runs the callback once, then on subsequent changes.
|
||||
@config.observe 'core.themes', =>
|
||||
@deactivateThemes().then =>
|
||||
@warnForNonExistentThemes()
|
||||
@refreshLessCache() # Update cache for packages in core.themes config
|
||||
|
||||
promises = []
|
||||
for themeName in @getEnabledThemeNames()
|
||||
if @packageManager.resolvePackagePath(themeName)
|
||||
promises.push(@packageManager.activatePackage(themeName))
|
||||
else
|
||||
console.warn("Failed to activate theme '#{themeName}' because it isn't installed.")
|
||||
|
||||
Promise.all(promises).then =>
|
||||
@addActiveThemeClasses()
|
||||
@refreshLessCache() # Update cache again now that @getActiveThemes() is populated
|
||||
@loadUserStylesheet()
|
||||
@reloadBaseStylesheets()
|
||||
@initialLoadComplete = true
|
||||
@emitter.emit 'did-change-active-themes'
|
||||
resolve()
|
||||
|
||||
deactivateThemes: ->
|
||||
@removeActiveThemeClasses()
|
||||
@unwatchUserStylesheet()
|
||||
results = @getActiveThemes().map((pack) => @packageManager.deactivatePackage(pack.name))
|
||||
Promise.all(results.filter((r) -> typeof r?.then is 'function'))
|
||||
|
||||
isInitialLoadComplete: -> @initialLoadComplete
|
||||
|
||||
addActiveThemeClasses: ->
|
||||
if workspaceElement = @viewRegistry.getView(@workspace)
|
||||
for pack in @getActiveThemes()
|
||||
workspaceElement.classList.add("theme-#{pack.name}")
|
||||
return
|
||||
|
||||
removeActiveThemeClasses: ->
|
||||
workspaceElement = @viewRegistry.getView(@workspace)
|
||||
for pack in @getActiveThemes()
|
||||
workspaceElement.classList.remove("theme-#{pack.name}")
|
||||
return
|
||||
|
||||
refreshLessCache: ->
|
||||
@lessCache?.setImportPaths(@getImportPaths())
|
||||
|
||||
getImportPaths: ->
|
||||
activeThemes = @getActiveThemes()
|
||||
if activeThemes.length > 0
|
||||
themePaths = (theme.getStylesheetsPath() for theme in activeThemes when theme)
|
||||
else
|
||||
themePaths = []
|
||||
for themeName in @getEnabledThemeNames()
|
||||
if themePath = @packageManager.resolvePackagePath(themeName)
|
||||
deprecatedPath = path.join(themePath, 'stylesheets')
|
||||
if fs.isDirectorySync(deprecatedPath)
|
||||
themePaths.push(deprecatedPath)
|
||||
else
|
||||
themePaths.push(path.join(themePath, 'styles'))
|
||||
|
||||
themePaths.filter (themePath) -> fs.isDirectorySync(themePath)
|
||||
401
src/theme-manager.js
Normal file
401
src/theme-manager.js
Normal file
@@ -0,0 +1,401 @@
|
||||
/* global snapshotAuxiliaryData */
|
||||
|
||||
const path = require('path')
|
||||
const _ = require('underscore-plus')
|
||||
const {Emitter, CompositeDisposable} = require('event-kit')
|
||||
const {File} = require('pathwatcher')
|
||||
const fs = require('fs-plus')
|
||||
const LessCompileCache = require('./less-compile-cache')
|
||||
|
||||
// Extended: Handles loading and activating available themes.
|
||||
//
|
||||
// An instance of this class is always available as the `atom.themes` global.
|
||||
module.exports =
|
||||
class ThemeManager {
|
||||
constructor ({packageManager, config, styleManager, notificationManager, viewRegistry}) {
|
||||
this.packageManager = packageManager
|
||||
this.config = config
|
||||
this.styleManager = styleManager
|
||||
this.notificationManager = notificationManager
|
||||
this.viewRegistry = viewRegistry
|
||||
this.emitter = new Emitter()
|
||||
this.styleSheetDisposablesBySourcePath = {}
|
||||
this.lessCache = null
|
||||
this.initialLoadComplete = false
|
||||
this.packageManager.registerPackageActivator(this, ['theme'])
|
||||
this.packageManager.onDidActivateInitialPackages(() => {
|
||||
this.onDidChangeActiveThemes(() => this.packageManager.reloadActivePackageStyleSheets())
|
||||
})
|
||||
}
|
||||
|
||||
initialize ({resourcePath, configDirPath, safeMode, devMode}) {
|
||||
this.resourcePath = resourcePath
|
||||
this.configDirPath = configDirPath
|
||||
this.safeMode = safeMode
|
||||
this.lessSourcesByRelativeFilePath = null
|
||||
if (devMode || (typeof snapshotAuxiliaryData === 'undefined')) {
|
||||
this.lessSourcesByRelativeFilePath = {}
|
||||
this.importedFilePathsByRelativeImportPath = {}
|
||||
} else {
|
||||
this.lessSourcesByRelativeFilePath = snapshotAuxiliaryData.lessSourcesByRelativeFilePath
|
||||
this.importedFilePathsByRelativeImportPath = snapshotAuxiliaryData.importedFilePathsByRelativeImportPath
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Event Subscription
|
||||
*/
|
||||
|
||||
// Essential: Invoke `callback` when style sheet changes associated with
|
||||
// updating the list of active themes have completed.
|
||||
//
|
||||
// * `callback` {Function}
|
||||
onDidChangeActiveThemes (callback) {
|
||||
return this.emitter.on('did-change-active-themes', callback)
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Accessing Available Themes
|
||||
*/
|
||||
|
||||
getAvailableNames () {
|
||||
// TODO: Maybe should change to list all the available themes out there?
|
||||
return this.getLoadedNames()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Accessing Loaded Themes
|
||||
*/
|
||||
|
||||
// Public: Returns an {Array} of {String}s of all the loaded theme names.
|
||||
getLoadedThemeNames () {
|
||||
return this.getLoadedThemes().map((theme) => theme.name)
|
||||
}
|
||||
|
||||
// Public: Returns an {Array} of all the loaded themes.
|
||||
getLoadedThemes () {
|
||||
return this.packageManager.getLoadedPackages().filter((pack) => pack.isTheme())
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Accessing Active Themes
|
||||
*/
|
||||
|
||||
// Public: Returns an {Array} of {String}s of all the active theme names.
|
||||
getActiveThemeNames () {
|
||||
return this.getActiveThemes().map((theme) => theme.name)
|
||||
}
|
||||
|
||||
// Public: Returns an {Array} of all the active themes.
|
||||
getActiveThemes () {
|
||||
return this.packageManager.getActivePackages().filter((pack) => pack.isTheme())
|
||||
}
|
||||
|
||||
activatePackages () {
|
||||
return this.activateThemes()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Managing Enabled Themes
|
||||
*/
|
||||
|
||||
warnForNonExistentThemes () {
|
||||
let themeNames = this.config.get('core.themes') || []
|
||||
if (!_.isArray(themeNames)) { themeNames = [themeNames] }
|
||||
for (let themeName of themeNames) {
|
||||
if (!themeName || (typeof themeName !== 'string') || !this.packageManager.resolvePackagePath(themeName)) {
|
||||
console.warn(`Enabled theme '${themeName}' is not installed.`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public: Get the enabled theme names from the config.
|
||||
//
|
||||
// Returns an array of theme names in the order that they should be activated.
|
||||
getEnabledThemeNames () {
|
||||
let themeNames = this.config.get('core.themes') || []
|
||||
if (!_.isArray(themeNames)) { themeNames = [themeNames] }
|
||||
themeNames = themeNames.filter((themeName) =>
|
||||
(typeof themeName === 'string') && this.packageManager.resolvePackagePath(themeName)
|
||||
)
|
||||
|
||||
// Use a built-in syntax and UI theme any time the configured themes are not
|
||||
// available.
|
||||
if (themeNames.length < 2) {
|
||||
const builtInThemeNames = [
|
||||
'atom-dark-syntax',
|
||||
'atom-dark-ui',
|
||||
'atom-light-syntax',
|
||||
'atom-light-ui',
|
||||
'base16-tomorrow-dark-theme',
|
||||
'base16-tomorrow-light-theme',
|
||||
'solarized-dark-syntax',
|
||||
'solarized-light-syntax'
|
||||
]
|
||||
themeNames = _.intersection(themeNames, builtInThemeNames)
|
||||
if (themeNames.length === 0) {
|
||||
themeNames = ['atom-dark-syntax', 'atom-dark-ui']
|
||||
} else if (themeNames.length === 1) {
|
||||
if (_.endsWith(themeNames[0], '-ui')) {
|
||||
themeNames.unshift('atom-dark-syntax')
|
||||
} else {
|
||||
themeNames.push('atom-dark-ui')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse so the first (top) theme is loaded after the others. We want
|
||||
// the first/top theme to override later themes in the stack.
|
||||
return themeNames.reverse()
|
||||
}
|
||||
|
||||
/*
|
||||
Section: Private
|
||||
*/
|
||||
|
||||
// Resolve and apply the stylesheet specified by the path.
|
||||
//
|
||||
// This supports both CSS and Less stylesheets.
|
||||
//
|
||||
// * `stylesheetPath` A {String} path to the stylesheet that can be an absolute
|
||||
// path or a relative path that will be resolved against the load path.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to remove the
|
||||
// required stylesheet.
|
||||
requireStylesheet (stylesheetPath, priority, skipDeprecatedSelectorsTransformation) {
|
||||
let fullPath = this.resolveStylesheet(stylesheetPath)
|
||||
if (fullPath) {
|
||||
const content = this.loadStylesheet(fullPath)
|
||||
return this.applyStylesheet(fullPath, content, priority, skipDeprecatedSelectorsTransformation)
|
||||
} else {
|
||||
throw new Error(`Could not find a file at path '${stylesheetPath}'`)
|
||||
}
|
||||
}
|
||||
|
||||
unwatchUserStylesheet () {
|
||||
if (this.userStylesheetSubscriptions != null) this.userStylesheetSubscriptions.dispose()
|
||||
this.userStylesheetSubscriptions = null
|
||||
this.userStylesheetFile = null
|
||||
if (this.userStyleSheetDisposable != null) this.userStyleSheetDisposable.dispose()
|
||||
this.userStyleSheetDisposable = null
|
||||
}
|
||||
|
||||
loadUserStylesheet () {
|
||||
this.unwatchUserStylesheet()
|
||||
|
||||
const userStylesheetPath = this.styleManager.getUserStyleSheetPath()
|
||||
if (!fs.isFileSync(userStylesheetPath)) { return }
|
||||
|
||||
try {
|
||||
this.userStylesheetFile = new File(userStylesheetPath)
|
||||
this.userStylesheetSubscriptions = new CompositeDisposable()
|
||||
const reloadStylesheet = () => this.loadUserStylesheet()
|
||||
this.userStylesheetSubscriptions.add(this.userStylesheetFile.onDidChange(reloadStylesheet))
|
||||
this.userStylesheetSubscriptions.add(this.userStylesheetFile.onDidRename(reloadStylesheet))
|
||||
this.userStylesheetSubscriptions.add(this.userStylesheetFile.onDidDelete(reloadStylesheet))
|
||||
} catch (error) {
|
||||
const message = `\
|
||||
Unable to watch path: \`${path.basename(userStylesheetPath)}\`. Make sure
|
||||
you have permissions to \`${userStylesheetPath}\`.
|
||||
|
||||
On linux there are currently problems with watch sizes. See
|
||||
[this document][watches] for more info.
|
||||
[watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path\
|
||||
`
|
||||
this.notificationManager.addError(message, {dismissable: true})
|
||||
}
|
||||
|
||||
let userStylesheetContents
|
||||
try {
|
||||
userStylesheetContents = this.loadStylesheet(userStylesheetPath, true)
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.userStyleSheetDisposable = this.styleManager.addStyleSheet(userStylesheetContents, {sourcePath: userStylesheetPath, priority: 2})
|
||||
}
|
||||
|
||||
loadBaseStylesheets () {
|
||||
this.reloadBaseStylesheets()
|
||||
}
|
||||
|
||||
reloadBaseStylesheets () {
|
||||
this.requireStylesheet('../static/atom', -2, true)
|
||||
}
|
||||
|
||||
stylesheetElementForId (id) {
|
||||
const escapedId = id.replace(/\\/g, '\\\\')
|
||||
return document.head.querySelector(`atom-styles style[source-path="${escapedId}"]`)
|
||||
}
|
||||
|
||||
resolveStylesheet (stylesheetPath) {
|
||||
if (path.extname(stylesheetPath).length > 0) {
|
||||
return fs.resolveOnLoadPath(stylesheetPath)
|
||||
} else {
|
||||
return fs.resolveOnLoadPath(stylesheetPath, ['css', 'less'])
|
||||
}
|
||||
}
|
||||
|
||||
loadStylesheet (stylesheetPath, importFallbackVariables) {
|
||||
if (path.extname(stylesheetPath) === '.less') {
|
||||
return this.loadLessStylesheet(stylesheetPath, importFallbackVariables)
|
||||
} else {
|
||||
return fs.readFileSync(stylesheetPath, 'utf8')
|
||||
}
|
||||
}
|
||||
|
||||
loadLessStylesheet (lessStylesheetPath, importFallbackVariables = false) {
|
||||
if (this.lessCache == null) {
|
||||
this.lessCache = new LessCompileCache({
|
||||
resourcePath: this.resourcePath,
|
||||
lessSourcesByRelativeFilePath: this.lessSourcesByRelativeFilePath,
|
||||
importedFilePathsByRelativeImportPath: this.importedFilePathsByRelativeImportPath,
|
||||
importPaths: this.getImportPaths()
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
if (importFallbackVariables) {
|
||||
const baseVarImports = `\
|
||||
@import "variables/ui-variables";
|
||||
@import "variables/syntax-variables";\
|
||||
`
|
||||
const relativeFilePath = path.relative(this.resourcePath, lessStylesheetPath)
|
||||
const lessSource = this.lessSourcesByRelativeFilePath[relativeFilePath]
|
||||
|
||||
let content, digest
|
||||
if (lessSource != null) {
|
||||
({ content } = lessSource);
|
||||
({ digest } = lessSource)
|
||||
} else {
|
||||
content = baseVarImports + '\n' + fs.readFileSync(lessStylesheetPath, 'utf8')
|
||||
digest = null
|
||||
}
|
||||
|
||||
return this.lessCache.cssForFile(lessStylesheetPath, content, digest)
|
||||
} else {
|
||||
return this.lessCache.read(lessStylesheetPath)
|
||||
}
|
||||
} catch (error) {
|
||||
let detail, message
|
||||
error.less = true
|
||||
if (error.line != null) {
|
||||
// Adjust line numbers for import fallbacks
|
||||
if (importFallbackVariables) { error.line -= 2 }
|
||||
|
||||
message = `Error compiling Less stylesheet: \`${lessStylesheetPath}\``
|
||||
detail = `Line number: ${error.line}\n${error.message}`
|
||||
} else {
|
||||
message = `Error loading Less stylesheet: \`${lessStylesheetPath}\``
|
||||
detail = error.message
|
||||
}
|
||||
|
||||
this.notificationManager.addError(message, {detail, dismissable: true})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
removeStylesheet (stylesheetPath) {
|
||||
if (this.styleSheetDisposablesBySourcePath[stylesheetPath] != null) {
|
||||
this.styleSheetDisposablesBySourcePath[stylesheetPath].dispose()
|
||||
}
|
||||
}
|
||||
|
||||
applyStylesheet (path, text, priority, skipDeprecatedSelectorsTransformation) {
|
||||
this.styleSheetDisposablesBySourcePath[path] = this.styleManager.addStyleSheet(
|
||||
text,
|
||||
{
|
||||
priority,
|
||||
skipDeprecatedSelectorsTransformation,
|
||||
sourcePath: path
|
||||
}
|
||||
)
|
||||
|
||||
return this.styleSheetDisposablesBySourcePath[path]
|
||||
}
|
||||
|
||||
activateThemes () {
|
||||
return new Promise(resolve => {
|
||||
// @config.observe runs the callback once, then on subsequent changes.
|
||||
this.config.observe('core.themes', () => {
|
||||
this.deactivateThemes().then(() => {
|
||||
this.warnForNonExistentThemes()
|
||||
this.refreshLessCache() // Update cache for packages in core.themes config
|
||||
|
||||
const promises = []
|
||||
for (const themeName of this.getEnabledThemeNames()) {
|
||||
if (this.packageManager.resolvePackagePath(themeName)) {
|
||||
promises.push(this.packageManager.activatePackage(themeName))
|
||||
} else {
|
||||
console.warn(`Failed to activate theme '${themeName}' because it isn't installed.`)
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
this.addActiveThemeClasses()
|
||||
this.refreshLessCache() // Update cache again now that @getActiveThemes() is populated
|
||||
this.loadUserStylesheet()
|
||||
this.reloadBaseStylesheets()
|
||||
this.initialLoadComplete = true
|
||||
this.emitter.emit('did-change-active-themes')
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
deactivateThemes () {
|
||||
this.removeActiveThemeClasses()
|
||||
this.unwatchUserStylesheet()
|
||||
const results = this.getActiveThemes().map(pack => this.packageManager.deactivatePackage(pack.name))
|
||||
return Promise.all(results.filter((r) => (r != null) && (typeof r.then === 'function')))
|
||||
}
|
||||
|
||||
isInitialLoadComplete () {
|
||||
return this.initialLoadComplete
|
||||
}
|
||||
|
||||
addActiveThemeClasses () {
|
||||
const workspaceElement = this.viewRegistry.getView(this.workspace)
|
||||
if (workspaceElement) {
|
||||
for (const pack of this.getActiveThemes()) {
|
||||
workspaceElement.classList.add(`theme-${pack.name}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeActiveThemeClasses () {
|
||||
const workspaceElement = this.viewRegistry.getView(this.workspace)
|
||||
for (const pack of this.getActiveThemes()) {
|
||||
workspaceElement.classList.remove(`theme-${pack.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
refreshLessCache () {
|
||||
if (this.lessCache) this.lessCache.setImportPaths(this.getImportPaths())
|
||||
}
|
||||
|
||||
getImportPaths () {
|
||||
let themePaths
|
||||
const activeThemes = this.getActiveThemes()
|
||||
if (activeThemes.length > 0) {
|
||||
themePaths = (activeThemes.filter((theme) => theme).map((theme) => theme.getStylesheetsPath()))
|
||||
} else {
|
||||
themePaths = []
|
||||
for (const themeName of this.getEnabledThemeNames()) {
|
||||
const themePath = this.packageManager.resolvePackagePath(themeName)
|
||||
if (themePath) {
|
||||
const deprecatedPath = path.join(themePath, 'stylesheets')
|
||||
if (fs.isDirectorySync(deprecatedPath)) {
|
||||
themePaths.push(deprecatedPath)
|
||||
} else {
|
||||
themePaths.push(path.join(themePath, 'styles'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return themePaths.filter(themePath => fs.isDirectorySync(themePath))
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
path = require 'path'
|
||||
Package = require './package'
|
||||
|
||||
module.exports =
|
||||
class ThemePackage extends Package
|
||||
getType: -> 'theme'
|
||||
|
||||
getStyleSheetPriority: -> 1
|
||||
|
||||
enable: ->
|
||||
@config.unshiftAtKeyPath('core.themes', @name)
|
||||
|
||||
disable: ->
|
||||
@config.removeAtKeyPath('core.themes', @name)
|
||||
|
||||
preload: ->
|
||||
@loadTime = 0
|
||||
@configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata()
|
||||
|
||||
finishLoading: ->
|
||||
@path = path.join(@packageManager.resourcePath, @path)
|
||||
|
||||
load: ->
|
||||
@loadTime = 0
|
||||
@configSchemaRegisteredOnLoad = @registerConfigSchemaFromMetadata()
|
||||
this
|
||||
|
||||
activate: ->
|
||||
@activationPromise ?= new Promise (resolve, reject) =>
|
||||
@resolveActivationPromise = resolve
|
||||
@rejectActivationPromise = reject
|
||||
@measure 'activateTime', =>
|
||||
try
|
||||
@loadStylesheets()
|
||||
@activateNow()
|
||||
catch error
|
||||
@handleError("Failed to activate the #{@name} theme", error)
|
||||
55
src/theme-package.js
Normal file
55
src/theme-package.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const path = require('path')
|
||||
const Package = require('./package')
|
||||
|
||||
module.exports =
|
||||
class ThemePackage extends Package {
|
||||
getType () {
|
||||
return 'theme'
|
||||
}
|
||||
|
||||
getStyleSheetPriority () {
|
||||
return 1
|
||||
}
|
||||
|
||||
enable () {
|
||||
this.config.unshiftAtKeyPath('core.themes', this.name)
|
||||
}
|
||||
|
||||
disable () {
|
||||
this.config.removeAtKeyPath('core.themes', this.name)
|
||||
}
|
||||
|
||||
preload () {
|
||||
this.loadTime = 0
|
||||
this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata()
|
||||
}
|
||||
|
||||
finishLoading () {
|
||||
this.path = path.join(this.packageManager.resourcePath, this.path)
|
||||
}
|
||||
|
||||
load () {
|
||||
this.loadTime = 0
|
||||
this.configSchemaRegisteredOnLoad = this.registerConfigSchemaFromMetadata()
|
||||
return this
|
||||
}
|
||||
|
||||
activate () {
|
||||
if (this.activationPromise == null) {
|
||||
this.activationPromise = new Promise((resolve, reject) => {
|
||||
this.resolveActivationPromise = resolve
|
||||
this.rejectActivationPromise = reject
|
||||
this.measure('activateTime', () => {
|
||||
try {
|
||||
this.loadStylesheets()
|
||||
this.activateNow()
|
||||
} catch (error) {
|
||||
this.handleError(`Failed to activate the ${this.name} theme`, error)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return this.activationPromise
|
||||
}
|
||||
}
|
||||
@@ -163,99 +163,12 @@ class TokenizedBuffer {
|
||||
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)
|
||||
})
|
||||
}
|
||||
commentStringsForPosition (position) {
|
||||
if (this.scopedSettingsDelegate) {
|
||||
const scope = this.scopeDescriptorForPosition(position)
|
||||
return this.scopedSettingsDelegate.getCommentStrings(scope)
|
||||
} 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,24 +507,6 @@ class TokenizedBuffer {
|
||||
return scopes
|
||||
}
|
||||
|
||||
columnForIndentLevel (line, indentLevel, tabLength = this.tabLength) {
|
||||
let column = 0
|
||||
let indentLength = 0
|
||||
const goalIndentLength = indentLevel * tabLength
|
||||
while (indentLength < goalIndentLength) {
|
||||
const char = line[column]
|
||||
if (char === '\t') {
|
||||
indentLength += tabLength - (indentLength % tabLength)
|
||||
} else if (char === ' ') {
|
||||
indentLength++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
column++
|
||||
}
|
||||
return column
|
||||
}
|
||||
|
||||
indentLevelForLine (line, tabLength = this.tabLength) {
|
||||
let indentLength = 0
|
||||
for (let i = 0, {length} = line; i < length; i++) {
|
||||
@@ -841,12 +736,6 @@ class TokenizedBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
commentStringsForScopeDescriptor (scopes) {
|
||||
if (this.scopedSettingsDelegate) {
|
||||
return this.scopedSettingsDelegate.getCommentStrings(scopes)
|
||||
}
|
||||
}
|
||||
|
||||
regexForPattern (pattern) {
|
||||
if (pattern) {
|
||||
if (!this.regexesByPattern[pattern]) {
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
{Disposable, CompositeDisposable} = require 'event-kit'
|
||||
Tooltip = null
|
||||
|
||||
# Essential: Associates tooltips with HTML elements.
|
||||
#
|
||||
# You can get the `TooltipManager` via `atom.tooltips`.
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# The essence of displaying a tooltip
|
||||
#
|
||||
# ```coffee
|
||||
# # display it
|
||||
# disposable = atom.tooltips.add(div, {title: 'This is a tooltip'})
|
||||
#
|
||||
# # remove it
|
||||
# disposable.dispose()
|
||||
# ```
|
||||
#
|
||||
# In practice there are usually multiple tooltips. So we add them to a
|
||||
# CompositeDisposable
|
||||
#
|
||||
# ```coffee
|
||||
# {CompositeDisposable} = require 'atom'
|
||||
# subscriptions = new CompositeDisposable
|
||||
#
|
||||
# div1 = document.createElement('div')
|
||||
# div2 = document.createElement('div')
|
||||
# subscriptions.add atom.tooltips.add(div1, {title: 'This is a tooltip'})
|
||||
# subscriptions.add atom.tooltips.add(div2, {title: 'Another tooltip'})
|
||||
#
|
||||
# # remove them all
|
||||
# subscriptions.dispose()
|
||||
# ```
|
||||
#
|
||||
# You can display a key binding in the tooltip as well with the
|
||||
# `keyBindingCommand` option.
|
||||
#
|
||||
# ```coffee
|
||||
# disposable = atom.tooltips.add @caseOptionButton,
|
||||
# title: "Match Case"
|
||||
# keyBindingCommand: 'find-and-replace:toggle-case-option'
|
||||
# keyBindingTarget: @findEditor.element
|
||||
# ```
|
||||
module.exports =
|
||||
class TooltipManager
|
||||
defaults:
|
||||
trigger: 'hover'
|
||||
container: 'body'
|
||||
html: true
|
||||
placement: 'auto top'
|
||||
viewportPadding: 2
|
||||
|
||||
hoverDefaults:
|
||||
{delay: {show: 1000, hide: 100}}
|
||||
|
||||
constructor: ({@keymapManager, @viewRegistry}) ->
|
||||
@tooltips = new Map()
|
||||
|
||||
# Essential: Add a tooltip to the given element.
|
||||
#
|
||||
# * `target` An `HTMLElement`
|
||||
# * `options` An object with one or more of the following options:
|
||||
# * `title` A {String} or {Function} to use for the text in the tip. If
|
||||
# a function is passed, `this` will be set to the `target` element. This
|
||||
# option is mutually exclusive with the `item` option.
|
||||
# * `html` A {Boolean} affecting the interpretation of the `title` option.
|
||||
# If `true` (the default), the `title` string will be interpreted as HTML.
|
||||
# Otherwise it will be interpreted as plain text.
|
||||
# * `item` A view (object with an `.element` property) or a DOM element
|
||||
# containing custom content for the tooltip. This option is mutually
|
||||
# exclusive with the `title` option.
|
||||
# * `class` A {String} with a class to apply to the tooltip element to
|
||||
# enable custom styling.
|
||||
# * `placement` A {String} or {Function} returning a string to indicate
|
||||
# the position of the tooltip relative to `element`. Can be `'top'`,
|
||||
# `'bottom'`, `'left'`, `'right'`, or `'auto'`. When `'auto'` is
|
||||
# specified, it will dynamically reorient the tooltip. For example, if
|
||||
# placement is `'auto left'`, the tooltip will display to the left when
|
||||
# possible, otherwise it will display right.
|
||||
# When a function is used to determine the placement, it is called with
|
||||
# the tooltip DOM node as its first argument and the triggering element
|
||||
# DOM node as its second. The `this` context is set to the tooltip
|
||||
# instance.
|
||||
# * `trigger` A {String} indicating how the tooltip should be displayed.
|
||||
# Choose from one of the following options:
|
||||
# * `'hover'` Show the tooltip when the mouse hovers over the element.
|
||||
# This is the default.
|
||||
# * `'click'` Show the tooltip when the element is clicked. The tooltip
|
||||
# will be hidden after clicking the element again or anywhere else
|
||||
# outside of the tooltip itself.
|
||||
# * `'focus'` Show the tooltip when the element is focused.
|
||||
# * `'manual'` Show the tooltip immediately and only hide it when the
|
||||
# returned disposable is disposed.
|
||||
# * `delay` An object specifying the show and hide delay in milliseconds.
|
||||
# Defaults to `{show: 1000, hide: 100}` if the `trigger` is `hover` and
|
||||
# otherwise defaults to `0` for both values.
|
||||
# * `keyBindingCommand` A {String} containing a command name. If you specify
|
||||
# this option and a key binding exists that matches the command, it will
|
||||
# be appended to the title or rendered alone if no title is specified.
|
||||
# * `keyBindingTarget` An `HTMLElement` on which to look up the key binding.
|
||||
# If this option is not supplied, the first of all matching key bindings
|
||||
# for the given command will be rendered.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to remove the
|
||||
# tooltip.
|
||||
add: (target, options) ->
|
||||
if target.jquery
|
||||
disposable = new CompositeDisposable
|
||||
disposable.add @add(element, options) for element in target
|
||||
return disposable
|
||||
|
||||
Tooltip ?= require './tooltip'
|
||||
|
||||
{keyBindingCommand, keyBindingTarget} = options
|
||||
|
||||
if keyBindingCommand?
|
||||
bindings = @keymapManager.findKeyBindings(command: keyBindingCommand, target: keyBindingTarget)
|
||||
keystroke = getKeystroke(bindings)
|
||||
if options.title? and keystroke?
|
||||
options.title += " " + getKeystroke(bindings)
|
||||
else if keystroke?
|
||||
options.title = getKeystroke(bindings)
|
||||
|
||||
delete options.selector
|
||||
options = _.defaults(options, @defaults)
|
||||
if options.trigger is 'hover'
|
||||
options = _.defaults(options, @hoverDefaults)
|
||||
|
||||
tooltip = new Tooltip(target, options, @viewRegistry)
|
||||
|
||||
if not @tooltips.has(target)
|
||||
@tooltips.set(target, [])
|
||||
@tooltips.get(target).push(tooltip)
|
||||
|
||||
hideTooltip = ->
|
||||
tooltip.leave(currentTarget: target)
|
||||
tooltip.hide()
|
||||
|
||||
window.addEventListener('resize', hideTooltip)
|
||||
|
||||
disposable = new Disposable =>
|
||||
window.removeEventListener('resize', hideTooltip)
|
||||
hideTooltip()
|
||||
tooltip.destroy()
|
||||
|
||||
if @tooltips.has(target)
|
||||
tooltipsForTarget = @tooltips.get(target)
|
||||
index = tooltipsForTarget.indexOf(tooltip)
|
||||
if index isnt -1
|
||||
tooltipsForTarget.splice(index, 1)
|
||||
if tooltipsForTarget.length is 0
|
||||
@tooltips.delete(target)
|
||||
|
||||
disposable
|
||||
|
||||
# Extended: Find the tooltips that have been applied to the given element.
|
||||
#
|
||||
# * `target` The `HTMLElement` to find tooltips on.
|
||||
#
|
||||
# Returns an {Array} of `Tooltip` objects that match the `target`.
|
||||
findTooltips: (target) ->
|
||||
if @tooltips.has(target)
|
||||
@tooltips.get(target).slice()
|
||||
else
|
||||
[]
|
||||
|
||||
humanizeKeystrokes = (keystroke) ->
|
||||
keystrokes = keystroke.split(' ')
|
||||
keystrokes = (_.humanizeKeystroke(stroke) for stroke in keystrokes)
|
||||
keystrokes.join(' ')
|
||||
|
||||
getKeystroke = (bindings) ->
|
||||
if bindings?.length
|
||||
"<span class=\"keystroke\">#{humanizeKeystrokes(bindings[0].keystrokes)}</span>"
|
||||
199
src/tooltip-manager.js
Normal file
199
src/tooltip-manager.js
Normal file
@@ -0,0 +1,199 @@
|
||||
const _ = require('underscore-plus')
|
||||
const {Disposable, CompositeDisposable} = require('event-kit')
|
||||
let Tooltip = null
|
||||
|
||||
// Essential: Associates tooltips with HTML elements.
|
||||
//
|
||||
// You can get the `TooltipManager` via `atom.tooltips`.
|
||||
//
|
||||
// ## Examples
|
||||
//
|
||||
// The essence of displaying a tooltip
|
||||
//
|
||||
// ```javascript
|
||||
// // display it
|
||||
// const disposable = atom.tooltips.add(div, {title: 'This is a tooltip'})
|
||||
//
|
||||
// // remove it
|
||||
// disposable.dispose()
|
||||
// ```
|
||||
//
|
||||
// In practice there are usually multiple tooltips. So we add them to a
|
||||
// CompositeDisposable
|
||||
//
|
||||
// ```javascript
|
||||
// const {CompositeDisposable} = require('atom')
|
||||
// const subscriptions = new CompositeDisposable()
|
||||
//
|
||||
// const div1 = document.createElement('div')
|
||||
// const div2 = document.createElement('div')
|
||||
// subscriptions.add(atom.tooltips.add(div1, {title: 'This is a tooltip'}))
|
||||
// subscriptions.add(atom.tooltips.add(div2, {title: 'Another tooltip'}))
|
||||
//
|
||||
// // remove them all
|
||||
// subscriptions.dispose()
|
||||
// ```
|
||||
//
|
||||
// You can display a key binding in the tooltip as well with the
|
||||
// `keyBindingCommand` option.
|
||||
//
|
||||
// ```javascript
|
||||
// disposable = atom.tooltips.add(this.caseOptionButton, {
|
||||
// title: 'Match Case',
|
||||
// keyBindingCommand: 'find-and-replace:toggle-case-option',
|
||||
// keyBindingTarget: this.findEditor.element
|
||||
// })
|
||||
// ```
|
||||
module.exports =
|
||||
class TooltipManager {
|
||||
constructor ({keymapManager, viewRegistry}) {
|
||||
this.defaults = {
|
||||
trigger: 'hover',
|
||||
container: 'body',
|
||||
html: true,
|
||||
placement: 'auto top',
|
||||
viewportPadding: 2
|
||||
}
|
||||
|
||||
this.hoverDefaults = {
|
||||
delay: {show: 1000, hide: 100}
|
||||
}
|
||||
|
||||
this.keymapManager = keymapManager
|
||||
this.viewRegistry = viewRegistry
|
||||
this.tooltips = new Map()
|
||||
}
|
||||
|
||||
// Essential: Add a tooltip to the given element.
|
||||
//
|
||||
// * `target` An `HTMLElement`
|
||||
// * `options` An object with one or more of the following options:
|
||||
// * `title` A {String} or {Function} to use for the text in the tip. If
|
||||
// a function is passed, `this` will be set to the `target` element. This
|
||||
// option is mutually exclusive with the `item` option.
|
||||
// * `html` A {Boolean} affecting the interpretation of the `title` option.
|
||||
// If `true` (the default), the `title` string will be interpreted as HTML.
|
||||
// Otherwise it will be interpreted as plain text.
|
||||
// * `item` A view (object with an `.element` property) or a DOM element
|
||||
// containing custom content for the tooltip. This option is mutually
|
||||
// exclusive with the `title` option.
|
||||
// * `class` A {String} with a class to apply to the tooltip element to
|
||||
// enable custom styling.
|
||||
// * `placement` A {String} or {Function} returning a string to indicate
|
||||
// the position of the tooltip relative to `element`. Can be `'top'`,
|
||||
// `'bottom'`, `'left'`, `'right'`, or `'auto'`. When `'auto'` is
|
||||
// specified, it will dynamically reorient the tooltip. For example, if
|
||||
// placement is `'auto left'`, the tooltip will display to the left when
|
||||
// possible, otherwise it will display right.
|
||||
// When a function is used to determine the placement, it is called with
|
||||
// the tooltip DOM node as its first argument and the triggering element
|
||||
// DOM node as its second. The `this` context is set to the tooltip
|
||||
// instance.
|
||||
// * `trigger` A {String} indicating how the tooltip should be displayed.
|
||||
// Choose from one of the following options:
|
||||
// * `'hover'` Show the tooltip when the mouse hovers over the element.
|
||||
// This is the default.
|
||||
// * `'click'` Show the tooltip when the element is clicked. The tooltip
|
||||
// will be hidden after clicking the element again or anywhere else
|
||||
// outside of the tooltip itself.
|
||||
// * `'focus'` Show the tooltip when the element is focused.
|
||||
// * `'manual'` Show the tooltip immediately and only hide it when the
|
||||
// returned disposable is disposed.
|
||||
// * `delay` An object specifying the show and hide delay in milliseconds.
|
||||
// Defaults to `{show: 1000, hide: 100}` if the `trigger` is `hover` and
|
||||
// otherwise defaults to `0` for both values.
|
||||
// * `keyBindingCommand` A {String} containing a command name. If you specify
|
||||
// this option and a key binding exists that matches the command, it will
|
||||
// be appended to the title or rendered alone if no title is specified.
|
||||
// * `keyBindingTarget` An `HTMLElement` on which to look up the key binding.
|
||||
// If this option is not supplied, the first of all matching key bindings
|
||||
// for the given command will be rendered.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to remove the
|
||||
// tooltip.
|
||||
add (target, options) {
|
||||
if (target.jquery) {
|
||||
const disposable = new CompositeDisposable()
|
||||
for (const element of target) { disposable.add(this.add(element, options)) }
|
||||
return disposable
|
||||
}
|
||||
|
||||
if (Tooltip == null) { Tooltip = require('./tooltip') }
|
||||
|
||||
const {keyBindingCommand, keyBindingTarget} = options
|
||||
|
||||
if (keyBindingCommand != null) {
|
||||
const bindings = this.keymapManager.findKeyBindings({command: keyBindingCommand, target: keyBindingTarget})
|
||||
const keystroke = getKeystroke(bindings)
|
||||
if ((options.title != null) && (keystroke != null)) {
|
||||
options.title += ` ${getKeystroke(bindings)}`
|
||||
} else if (keystroke != null) {
|
||||
options.title = getKeystroke(bindings)
|
||||
}
|
||||
}
|
||||
|
||||
delete options.selector
|
||||
options = _.defaults(options, this.defaults)
|
||||
if (options.trigger === 'hover') {
|
||||
options = _.defaults(options, this.hoverDefaults)
|
||||
}
|
||||
|
||||
const tooltip = new Tooltip(target, options, this.viewRegistry)
|
||||
|
||||
if (!this.tooltips.has(target)) {
|
||||
this.tooltips.set(target, [])
|
||||
}
|
||||
this.tooltips.get(target).push(tooltip)
|
||||
|
||||
const hideTooltip = function () {
|
||||
tooltip.leave({currentTarget: target})
|
||||
tooltip.hide()
|
||||
}
|
||||
|
||||
window.addEventListener('resize', hideTooltip)
|
||||
|
||||
const disposable = new Disposable(() => {
|
||||
window.removeEventListener('resize', hideTooltip)
|
||||
hideTooltip()
|
||||
tooltip.destroy()
|
||||
|
||||
if (this.tooltips.has(target)) {
|
||||
const tooltipsForTarget = this.tooltips.get(target)
|
||||
const index = tooltipsForTarget.indexOf(tooltip)
|
||||
if (index !== -1) {
|
||||
tooltipsForTarget.splice(index, 1)
|
||||
}
|
||||
if (tooltipsForTarget.length === 0) {
|
||||
this.tooltips.delete(target)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return disposable
|
||||
}
|
||||
|
||||
// Extended: Find the tooltips that have been applied to the given element.
|
||||
//
|
||||
// * `target` The `HTMLElement` to find tooltips on.
|
||||
//
|
||||
// Returns an {Array} of `Tooltip` objects that match the `target`.
|
||||
findTooltips (target) {
|
||||
if (this.tooltips.has(target)) {
|
||||
return this.tooltips.get(target).slice()
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function humanizeKeystrokes (keystroke) {
|
||||
let keystrokes = keystroke.split(' ')
|
||||
keystrokes = (keystrokes.map((stroke) => _.humanizeKeystroke(stroke)))
|
||||
return keystrokes.join(' ')
|
||||
}
|
||||
|
||||
function getKeystroke (bindings) {
|
||||
if (bindings && bindings.length) {
|
||||
return `<span class="keystroke">${humanizeKeystrokes(bindings[0].keystrokes)}</span>`
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
Grim = require 'grim'
|
||||
{Disposable} = require 'event-kit'
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
AnyConstructor = Symbol('any-constructor')
|
||||
|
||||
# Essential: `ViewRegistry` handles the association between model and view
|
||||
# types in Atom. We call this association a View Provider. As in, for a given
|
||||
# model, this class can provide a view via {::getView}, as long as the
|
||||
# model/view association was registered via {::addViewProvider}
|
||||
#
|
||||
# If you're adding your own kind of pane item, a good strategy for all but the
|
||||
# simplest items is to separate the model and the view. The model handles
|
||||
# application logic and is the primary point of API interaction. The view
|
||||
# just handles presentation.
|
||||
#
|
||||
# Note: Models can be any object, but must implement a `getTitle()` function
|
||||
# if they are to be displayed in a {Pane}
|
||||
#
|
||||
# View providers inform the workspace how your model objects should be
|
||||
# presented in the DOM. A view provider must always return a DOM node, which
|
||||
# makes [HTML 5 custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/)
|
||||
# an ideal tool for implementing views in Atom.
|
||||
#
|
||||
# You can access the `ViewRegistry` object via `atom.views`.
|
||||
module.exports =
|
||||
class ViewRegistry
|
||||
animationFrameRequest: null
|
||||
documentReadInProgress: false
|
||||
|
||||
constructor: (@atomEnvironment) ->
|
||||
@clear()
|
||||
|
||||
clear: ->
|
||||
@views = new WeakMap
|
||||
@providers = []
|
||||
@clearDocumentRequests()
|
||||
|
||||
# Essential: Add a provider that will be used to construct views in the
|
||||
# workspace's view layer based on model objects in its model layer.
|
||||
#
|
||||
# ## Examples
|
||||
#
|
||||
# Text editors are divided into a model and a view layer, so when you interact
|
||||
# with methods like `atom.workspace.getActiveTextEditor()` you're only going
|
||||
# to get the model object. We display text editors on screen by teaching the
|
||||
# workspace what view constructor it should use to represent them:
|
||||
#
|
||||
# ```coffee
|
||||
# atom.views.addViewProvider TextEditor, (textEditor) ->
|
||||
# textEditorElement = new TextEditorElement
|
||||
# textEditorElement.initialize(textEditor)
|
||||
# textEditorElement
|
||||
# ```
|
||||
#
|
||||
# * `modelConstructor` (optional) Constructor {Function} for your model. If
|
||||
# a constructor is given, the `createView` function will only be used
|
||||
# for model objects inheriting from that constructor. Otherwise, it will
|
||||
# will be called for any object.
|
||||
# * `createView` Factory {Function} that is passed an instance of your model
|
||||
# and must return a subclass of `HTMLElement` or `undefined`. If it returns
|
||||
# `undefined`, then the registry will continue to search for other view
|
||||
# providers.
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to remove the
|
||||
# added provider.
|
||||
addViewProvider: (modelConstructor, createView) ->
|
||||
if arguments.length is 1
|
||||
switch typeof modelConstructor
|
||||
when 'function'
|
||||
provider = {createView: modelConstructor, modelConstructor: AnyConstructor}
|
||||
when 'object'
|
||||
Grim.deprecate("atom.views.addViewProvider now takes 2 arguments: a model constructor and a createView function. See docs for details.")
|
||||
provider = modelConstructor
|
||||
else
|
||||
throw new TypeError("Arguments to addViewProvider must be functions")
|
||||
else
|
||||
provider = {modelConstructor, createView}
|
||||
|
||||
@providers.push(provider)
|
||||
new Disposable =>
|
||||
@providers = @providers.filter (p) -> p isnt provider
|
||||
|
||||
getViewProviderCount: ->
|
||||
@providers.length
|
||||
|
||||
# Essential: Get the view associated with an object in the workspace.
|
||||
#
|
||||
# If you're just *using* the workspace, you shouldn't need to access the view
|
||||
# layer, but view layer access may be necessary if you want to perform DOM
|
||||
# manipulation that isn't supported via the model API.
|
||||
#
|
||||
# ## View Resolution Algorithm
|
||||
#
|
||||
# The view associated with the object is resolved using the following
|
||||
# sequence
|
||||
#
|
||||
# 1. Is the object an instance of `HTMLElement`? If true, return the object.
|
||||
# 2. Does the object have a method named `getElement` that returns an
|
||||
# instance of `HTMLElement`? If true, return that value.
|
||||
# 3. Does the object have a property named `element` with a value which is
|
||||
# an instance of `HTMLElement`? If true, return the property value.
|
||||
# 4. Is the object a jQuery object, indicated by the presence of a `jquery`
|
||||
# property? If true, return the root DOM element (i.e. `object[0]`).
|
||||
# 5. Has a view provider been registered for the object? If true, use the
|
||||
# provider to create a view associated with the object, and return the
|
||||
# view.
|
||||
#
|
||||
# If no associated view is returned by the sequence an error is thrown.
|
||||
#
|
||||
# Returns a DOM element.
|
||||
getView: (object) ->
|
||||
return unless object?
|
||||
|
||||
if view = @views.get(object)
|
||||
view
|
||||
else
|
||||
view = @createView(object)
|
||||
@views.set(object, view)
|
||||
view
|
||||
|
||||
createView: (object) ->
|
||||
if object instanceof HTMLElement
|
||||
return object
|
||||
|
||||
if typeof object?.getElement is 'function'
|
||||
element = object.getElement()
|
||||
if element instanceof HTMLElement
|
||||
return element
|
||||
|
||||
if object?.element instanceof HTMLElement
|
||||
return object.element
|
||||
|
||||
if object?.jquery
|
||||
return object[0]
|
||||
|
||||
for provider in @providers
|
||||
if provider.modelConstructor is AnyConstructor
|
||||
if element = provider.createView(object, @atomEnvironment)
|
||||
return element
|
||||
continue
|
||||
|
||||
if object instanceof provider.modelConstructor
|
||||
if element = provider.createView?(object, @atomEnvironment)
|
||||
return element
|
||||
|
||||
if viewConstructor = provider.viewConstructor
|
||||
element = new viewConstructor
|
||||
element.initialize?(object) ? element.setModel?(object)
|
||||
return element
|
||||
|
||||
if viewConstructor = object?.getViewClass?()
|
||||
view = new viewConstructor(object)
|
||||
return view[0]
|
||||
|
||||
throw new Error("Can't create a view for #{object.constructor.name} instance. Please register a view provider.")
|
||||
|
||||
updateDocument: (fn) ->
|
||||
@documentWriters.push(fn)
|
||||
@requestDocumentUpdate() unless @documentReadInProgress
|
||||
new Disposable =>
|
||||
@documentWriters = @documentWriters.filter (writer) -> writer isnt fn
|
||||
|
||||
readDocument: (fn) ->
|
||||
@documentReaders.push(fn)
|
||||
@requestDocumentUpdate()
|
||||
new Disposable =>
|
||||
@documentReaders = @documentReaders.filter (reader) -> reader isnt fn
|
||||
|
||||
getNextUpdatePromise: ->
|
||||
@nextUpdatePromise ?= new Promise (resolve) =>
|
||||
@resolveNextUpdatePromise = resolve
|
||||
|
||||
clearDocumentRequests: ->
|
||||
@documentReaders = []
|
||||
@documentWriters = []
|
||||
@nextUpdatePromise = null
|
||||
@resolveNextUpdatePromise = null
|
||||
if @animationFrameRequest?
|
||||
cancelAnimationFrame(@animationFrameRequest)
|
||||
@animationFrameRequest = null
|
||||
|
||||
requestDocumentUpdate: ->
|
||||
@animationFrameRequest ?= requestAnimationFrame(@performDocumentUpdate)
|
||||
|
||||
performDocumentUpdate: =>
|
||||
resolveNextUpdatePromise = @resolveNextUpdatePromise
|
||||
@animationFrameRequest = null
|
||||
@nextUpdatePromise = null
|
||||
@resolveNextUpdatePromise = null
|
||||
|
||||
writer() while writer = @documentWriters.shift()
|
||||
|
||||
@documentReadInProgress = true
|
||||
reader() while reader = @documentReaders.shift()
|
||||
@documentReadInProgress = false
|
||||
|
||||
# process updates requested as a result of reads
|
||||
writer() while writer = @documentWriters.shift()
|
||||
|
||||
resolveNextUpdatePromise?()
|
||||
259
src/view-registry.js
Normal file
259
src/view-registry.js
Normal file
@@ -0,0 +1,259 @@
|
||||
const Grim = require('grim')
|
||||
const {Disposable} = require('event-kit')
|
||||
|
||||
const AnyConstructor = Symbol('any-constructor')
|
||||
|
||||
// Essential: `ViewRegistry` handles the association between model and view
|
||||
// types in Atom. We call this association a View Provider. As in, for a given
|
||||
// model, this class can provide a view via {::getView}, as long as the
|
||||
// model/view association was registered via {::addViewProvider}
|
||||
//
|
||||
// If you're adding your own kind of pane item, a good strategy for all but the
|
||||
// simplest items is to separate the model and the view. The model handles
|
||||
// application logic and is the primary point of API interaction. The view
|
||||
// just handles presentation.
|
||||
//
|
||||
// Note: Models can be any object, but must implement a `getTitle()` function
|
||||
// if they are to be displayed in a {Pane}
|
||||
//
|
||||
// View providers inform the workspace how your model objects should be
|
||||
// presented in the DOM. A view provider must always return a DOM node, which
|
||||
// makes [HTML 5 custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/)
|
||||
// an ideal tool for implementing views in Atom.
|
||||
//
|
||||
// You can access the `ViewRegistry` object via `atom.views`.
|
||||
module.exports =
|
||||
class ViewRegistry {
|
||||
constructor (atomEnvironment) {
|
||||
this.animationFrameRequest = null
|
||||
this.documentReadInProgress = false
|
||||
this.performDocumentUpdate = this.performDocumentUpdate.bind(this)
|
||||
this.atomEnvironment = atomEnvironment
|
||||
this.clear()
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.views = new WeakMap()
|
||||
this.providers = []
|
||||
this.clearDocumentRequests()
|
||||
}
|
||||
|
||||
// Essential: Add a provider that will be used to construct views in the
|
||||
// workspace's view layer based on model objects in its model layer.
|
||||
//
|
||||
// ## Examples
|
||||
//
|
||||
// Text editors are divided into a model and a view layer, so when you interact
|
||||
// with methods like `atom.workspace.getActiveTextEditor()` you're only going
|
||||
// to get the model object. We display text editors on screen by teaching the
|
||||
// workspace what view constructor it should use to represent them:
|
||||
//
|
||||
// ```coffee
|
||||
// atom.views.addViewProvider TextEditor, (textEditor) ->
|
||||
// textEditorElement = new TextEditorElement
|
||||
// textEditorElement.initialize(textEditor)
|
||||
// textEditorElement
|
||||
// ```
|
||||
//
|
||||
// * `modelConstructor` (optional) Constructor {Function} for your model. If
|
||||
// a constructor is given, the `createView` function will only be used
|
||||
// for model objects inheriting from that constructor. Otherwise, it will
|
||||
// will be called for any object.
|
||||
// * `createView` Factory {Function} that is passed an instance of your model
|
||||
// and must return a subclass of `HTMLElement` or `undefined`. If it returns
|
||||
// `undefined`, then the registry will continue to search for other view
|
||||
// providers.
|
||||
//
|
||||
// Returns a {Disposable} on which `.dispose()` can be called to remove the
|
||||
// added provider.
|
||||
addViewProvider (modelConstructor, createView) {
|
||||
let provider
|
||||
if (arguments.length === 1) {
|
||||
switch (typeof modelConstructor) {
|
||||
case 'function':
|
||||
provider = {createView: modelConstructor, modelConstructor: AnyConstructor}
|
||||
break
|
||||
case 'object':
|
||||
Grim.deprecate('atom.views.addViewProvider now takes 2 arguments: a model constructor and a createView function. See docs for details.')
|
||||
provider = modelConstructor
|
||||
break
|
||||
default:
|
||||
throw new TypeError('Arguments to addViewProvider must be functions')
|
||||
}
|
||||
} else {
|
||||
provider = {modelConstructor, createView}
|
||||
}
|
||||
|
||||
this.providers.push(provider)
|
||||
return new Disposable(() => {
|
||||
this.providers = this.providers.filter(p => p !== provider)
|
||||
})
|
||||
}
|
||||
|
||||
getViewProviderCount () {
|
||||
return this.providers.length
|
||||
}
|
||||
|
||||
// Essential: Get the view associated with an object in the workspace.
|
||||
//
|
||||
// If you're just *using* the workspace, you shouldn't need to access the view
|
||||
// layer, but view layer access may be necessary if you want to perform DOM
|
||||
// manipulation that isn't supported via the model API.
|
||||
//
|
||||
// ## View Resolution Algorithm
|
||||
//
|
||||
// The view associated with the object is resolved using the following
|
||||
// sequence
|
||||
//
|
||||
// 1. Is the object an instance of `HTMLElement`? If true, return the object.
|
||||
// 2. Does the object have a method named `getElement` that returns an
|
||||
// instance of `HTMLElement`? If true, return that value.
|
||||
// 3. Does the object have a property named `element` with a value which is
|
||||
// an instance of `HTMLElement`? If true, return the property value.
|
||||
// 4. Is the object a jQuery object, indicated by the presence of a `jquery`
|
||||
// property? If true, return the root DOM element (i.e. `object[0]`).
|
||||
// 5. Has a view provider been registered for the object? If true, use the
|
||||
// provider to create a view associated with the object, and return the
|
||||
// view.
|
||||
//
|
||||
// If no associated view is returned by the sequence an error is thrown.
|
||||
//
|
||||
// Returns a DOM element.
|
||||
getView (object) {
|
||||
if (object == null) { return }
|
||||
|
||||
let view = this.views.get(object)
|
||||
if (!view) {
|
||||
view = this.createView(object)
|
||||
this.views.set(object, view)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
createView (object) {
|
||||
if (object instanceof HTMLElement) { return object }
|
||||
|
||||
let element
|
||||
if (object && (typeof object.getElement === 'function')) {
|
||||
element = object.getElement()
|
||||
if (element instanceof HTMLElement) {
|
||||
return element
|
||||
}
|
||||
}
|
||||
|
||||
if (object && object.element instanceof HTMLElement) {
|
||||
return object.element
|
||||
}
|
||||
|
||||
if (object && object.jquery) {
|
||||
return object[0]
|
||||
}
|
||||
|
||||
for (let provider of this.providers) {
|
||||
if (provider.modelConstructor === AnyConstructor) {
|
||||
element = provider.createView(object, this.atomEnvironment)
|
||||
if (element) { return element }
|
||||
continue
|
||||
}
|
||||
|
||||
if (object instanceof provider.modelConstructor) {
|
||||
element = provider.createView && provider.createView(object, this.atomEnvironment)
|
||||
if (element) { return element }
|
||||
|
||||
let ViewConstructor = provider.viewConstructor
|
||||
if (ViewConstructor) {
|
||||
element = new ViewConstructor()
|
||||
if (element.initialize) {
|
||||
element.initialize(object)
|
||||
} else if (element.setModel) {
|
||||
element.setModel(object)
|
||||
}
|
||||
return element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (object && object.getViewClass) {
|
||||
let ViewConstructor = object.getViewClass()
|
||||
if (ViewConstructor) {
|
||||
const view = new ViewConstructor(object)
|
||||
return view[0]
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Can't create a view for ${object.constructor.name} instance. Please register a view provider.`)
|
||||
}
|
||||
|
||||
updateDocument (fn) {
|
||||
this.documentWriters.push(fn)
|
||||
if (!this.documentReadInProgress) { this.requestDocumentUpdate() }
|
||||
return new Disposable(() => {
|
||||
this.documentWriters = this.documentWriters.filter(writer => writer !== fn)
|
||||
})
|
||||
}
|
||||
|
||||
readDocument (fn) {
|
||||
this.documentReaders.push(fn)
|
||||
this.requestDocumentUpdate()
|
||||
return new Disposable(() => {
|
||||
this.documentReaders = this.documentReaders.filter(reader => reader !== fn)
|
||||
})
|
||||
}
|
||||
|
||||
getNextUpdatePromise () {
|
||||
if (this.nextUpdatePromise == null) {
|
||||
this.nextUpdatePromise = new Promise(resolve => {
|
||||
this.resolveNextUpdatePromise = resolve
|
||||
})
|
||||
}
|
||||
|
||||
return this.nextUpdatePromise
|
||||
}
|
||||
|
||||
clearDocumentRequests () {
|
||||
this.documentReaders = []
|
||||
this.documentWriters = []
|
||||
this.nextUpdatePromise = null
|
||||
this.resolveNextUpdatePromise = null
|
||||
if (this.animationFrameRequest != null) {
|
||||
cancelAnimationFrame(this.animationFrameRequest)
|
||||
this.animationFrameRequest = null
|
||||
}
|
||||
}
|
||||
|
||||
requestDocumentUpdate () {
|
||||
if (this.animationFrameRequest == null) {
|
||||
this.animationFrameRequest = requestAnimationFrame(this.performDocumentUpdate)
|
||||
}
|
||||
}
|
||||
|
||||
performDocumentUpdate () {
|
||||
const { resolveNextUpdatePromise } = this
|
||||
this.animationFrameRequest = null
|
||||
this.nextUpdatePromise = null
|
||||
this.resolveNextUpdatePromise = null
|
||||
|
||||
var writer = this.documentWriters.shift()
|
||||
while (writer) {
|
||||
writer()
|
||||
writer = this.documentWriters.shift()
|
||||
}
|
||||
|
||||
var reader = this.documentReaders.shift()
|
||||
this.documentReadInProgress = true
|
||||
while (reader) {
|
||||
reader()
|
||||
reader = this.documentReaders.shift()
|
||||
}
|
||||
this.documentReadInProgress = false
|
||||
|
||||
// process updates requested as a result of reads
|
||||
writer = this.documentWriters.shift()
|
||||
while (writer) {
|
||||
writer()
|
||||
writer = this.documentWriters.shift()
|
||||
}
|
||||
|
||||
if (resolveNextUpdatePromise) { resolveNextUpdatePromise() }
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
{Disposable, CompositeDisposable} = require 'event-kit'
|
||||
listen = require './delegated-listener'
|
||||
|
||||
# Handles low-level events related to the @window.
|
||||
module.exports =
|
||||
class WindowEventHandler
|
||||
constructor: ({@atomEnvironment, @applicationDelegate}) ->
|
||||
@reloadRequested = false
|
||||
@subscriptions = new CompositeDisposable
|
||||
|
||||
@handleNativeKeybindings()
|
||||
|
||||
initialize: (@window, @document) ->
|
||||
@subscriptions.add @atomEnvironment.commands.add @window,
|
||||
'window:toggle-full-screen': @handleWindowToggleFullScreen
|
||||
'window:close': @handleWindowClose
|
||||
'window:reload': @handleWindowReload
|
||||
'window:toggle-dev-tools': @handleWindowToggleDevTools
|
||||
|
||||
if process.platform in ['win32', 'linux']
|
||||
@subscriptions.add @atomEnvironment.commands.add @window,
|
||||
'window:toggle-menu-bar': @handleWindowToggleMenuBar
|
||||
|
||||
@subscriptions.add @atomEnvironment.commands.add @document,
|
||||
'core:focus-next': @handleFocusNext
|
||||
'core:focus-previous': @handleFocusPrevious
|
||||
|
||||
@addEventListener(@window, 'beforeunload', @handleWindowBeforeunload)
|
||||
@addEventListener(@window, 'focus', @handleWindowFocus)
|
||||
@addEventListener(@window, 'blur', @handleWindowBlur)
|
||||
|
||||
@addEventListener(@document, 'keyup', @handleDocumentKeyEvent)
|
||||
@addEventListener(@document, 'keydown', @handleDocumentKeyEvent)
|
||||
@addEventListener(@document, 'drop', @handleDocumentDrop)
|
||||
@addEventListener(@document, 'dragover', @handleDocumentDragover)
|
||||
@addEventListener(@document, 'contextmenu', @handleDocumentContextmenu)
|
||||
@subscriptions.add listen(@document, 'click', 'a', @handleLinkClick)
|
||||
@subscriptions.add listen(@document, 'submit', 'form', @handleFormSubmit)
|
||||
|
||||
@subscriptions.add(@applicationDelegate.onDidEnterFullScreen(@handleEnterFullScreen))
|
||||
@subscriptions.add(@applicationDelegate.onDidLeaveFullScreen(@handleLeaveFullScreen))
|
||||
|
||||
# Wire commands that should be handled by Chromium for elements with the
|
||||
# `.native-key-bindings` class.
|
||||
handleNativeKeybindings: ->
|
||||
bindCommandToAction = (command, action) =>
|
||||
@subscriptions.add @atomEnvironment.commands.add(
|
||||
'.native-key-bindings',
|
||||
command,
|
||||
((event) => @applicationDelegate.getCurrentWindow().webContents[action]()),
|
||||
false
|
||||
)
|
||||
|
||||
bindCommandToAction('core:copy', 'copy')
|
||||
bindCommandToAction('core:paste', 'paste')
|
||||
bindCommandToAction('core:undo', 'undo')
|
||||
bindCommandToAction('core:redo', 'redo')
|
||||
bindCommandToAction('core:select-all', 'selectAll')
|
||||
bindCommandToAction('core:cut', 'cut')
|
||||
|
||||
unsubscribe: ->
|
||||
@subscriptions.dispose()
|
||||
|
||||
on: (target, eventName, handler) ->
|
||||
target.on(eventName, handler)
|
||||
@subscriptions.add(new Disposable ->
|
||||
target.removeListener(eventName, handler)
|
||||
)
|
||||
|
||||
addEventListener: (target, eventName, handler) ->
|
||||
target.addEventListener(eventName, handler)
|
||||
@subscriptions.add(new Disposable(-> target.removeEventListener(eventName, handler)))
|
||||
|
||||
handleDocumentKeyEvent: (event) =>
|
||||
@atomEnvironment.keymaps.handleKeyboardEvent(event)
|
||||
event.stopImmediatePropagation()
|
||||
|
||||
handleDrop: (event) ->
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
handleDragover: (event) ->
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.dataTransfer.dropEffect = 'none'
|
||||
|
||||
eachTabIndexedElement: (callback) ->
|
||||
for element in @document.querySelectorAll('[tabindex]')
|
||||
continue if element.disabled
|
||||
continue unless element.tabIndex >= 0
|
||||
callback(element, element.tabIndex)
|
||||
return
|
||||
|
||||
handleFocusNext: =>
|
||||
focusedTabIndex = @document.activeElement.tabIndex ? -Infinity
|
||||
|
||||
nextElement = null
|
||||
nextTabIndex = Infinity
|
||||
lowestElement = null
|
||||
lowestTabIndex = Infinity
|
||||
@eachTabIndexedElement (element, tabIndex) ->
|
||||
if tabIndex < lowestTabIndex
|
||||
lowestTabIndex = tabIndex
|
||||
lowestElement = element
|
||||
|
||||
if focusedTabIndex < tabIndex < nextTabIndex
|
||||
nextTabIndex = tabIndex
|
||||
nextElement = element
|
||||
|
||||
if nextElement?
|
||||
nextElement.focus()
|
||||
else if lowestElement?
|
||||
lowestElement.focus()
|
||||
|
||||
handleFocusPrevious: =>
|
||||
focusedTabIndex = @document.activeElement.tabIndex ? Infinity
|
||||
|
||||
previousElement = null
|
||||
previousTabIndex = -Infinity
|
||||
highestElement = null
|
||||
highestTabIndex = -Infinity
|
||||
@eachTabIndexedElement (element, tabIndex) ->
|
||||
if tabIndex > highestTabIndex
|
||||
highestTabIndex = tabIndex
|
||||
highestElement = element
|
||||
|
||||
if focusedTabIndex > tabIndex > previousTabIndex
|
||||
previousTabIndex = tabIndex
|
||||
previousElement = element
|
||||
|
||||
if previousElement?
|
||||
previousElement.focus()
|
||||
else if highestElement?
|
||||
highestElement.focus()
|
||||
|
||||
handleWindowFocus: ->
|
||||
@document.body.classList.remove('is-blurred')
|
||||
|
||||
handleWindowBlur: =>
|
||||
@document.body.classList.add('is-blurred')
|
||||
@atomEnvironment.storeWindowDimensions()
|
||||
|
||||
handleEnterFullScreen: =>
|
||||
@document.body.classList.add("fullscreen")
|
||||
|
||||
handleLeaveFullScreen: =>
|
||||
@document.body.classList.remove("fullscreen")
|
||||
|
||||
handleWindowBeforeunload: (event) =>
|
||||
if not @reloadRequested and not @atomEnvironment.inSpecMode() and @atomEnvironment.getCurrentWindow().isWebViewFocused()
|
||||
@atomEnvironment.hide()
|
||||
@reloadRequested = false
|
||||
@atomEnvironment.storeWindowDimensions()
|
||||
@atomEnvironment.unloadEditorWindow()
|
||||
@atomEnvironment.destroy()
|
||||
|
||||
handleWindowToggleFullScreen: =>
|
||||
@atomEnvironment.toggleFullScreen()
|
||||
|
||||
handleWindowClose: =>
|
||||
@atomEnvironment.close()
|
||||
|
||||
handleWindowReload: =>
|
||||
@reloadRequested = true
|
||||
@atomEnvironment.reload()
|
||||
|
||||
handleWindowToggleDevTools: =>
|
||||
@atomEnvironment.toggleDevTools()
|
||||
|
||||
handleWindowToggleMenuBar: =>
|
||||
@atomEnvironment.config.set('core.autoHideMenuBar', not @atomEnvironment.config.get('core.autoHideMenuBar'))
|
||||
|
||||
if @atomEnvironment.config.get('core.autoHideMenuBar')
|
||||
detail = "To toggle, press the Alt key or execute the window:toggle-menu-bar command"
|
||||
@atomEnvironment.notifications.addInfo('Menu bar hidden', {detail})
|
||||
|
||||
handleLinkClick: (event) =>
|
||||
event.preventDefault()
|
||||
uri = event.currentTarget?.getAttribute('href')
|
||||
if uri and uri[0] isnt '#' and /^https?:\/\//.test(uri)
|
||||
@applicationDelegate.openExternal(uri)
|
||||
|
||||
handleFormSubmit: (event) ->
|
||||
# Prevent form submits from changing the current window's URL
|
||||
event.preventDefault()
|
||||
|
||||
handleDocumentContextmenu: (event) =>
|
||||
event.preventDefault()
|
||||
@atomEnvironment.contextMenu.showForEvent(event)
|
||||
253
src/window-event-handler.js
Normal file
253
src/window-event-handler.js
Normal file
@@ -0,0 +1,253 @@
|
||||
const {Disposable, CompositeDisposable} = require('event-kit')
|
||||
const listen = require('./delegated-listener')
|
||||
|
||||
// Handles low-level events related to the `window`.
|
||||
module.exports =
|
||||
class WindowEventHandler {
|
||||
constructor ({atomEnvironment, applicationDelegate}) {
|
||||
this.handleDocumentKeyEvent = this.handleDocumentKeyEvent.bind(this)
|
||||
this.handleFocusNext = this.handleFocusNext.bind(this)
|
||||
this.handleFocusPrevious = this.handleFocusPrevious.bind(this)
|
||||
this.handleWindowBlur = this.handleWindowBlur.bind(this)
|
||||
this.handleEnterFullScreen = this.handleEnterFullScreen.bind(this)
|
||||
this.handleLeaveFullScreen = this.handleLeaveFullScreen.bind(this)
|
||||
this.handleWindowBeforeunload = this.handleWindowBeforeunload.bind(this)
|
||||
this.handleWindowToggleFullScreen = this.handleWindowToggleFullScreen.bind(this)
|
||||
this.handleWindowClose = this.handleWindowClose.bind(this)
|
||||
this.handleWindowReload = this.handleWindowReload.bind(this)
|
||||
this.handleWindowToggleDevTools = this.handleWindowToggleDevTools.bind(this)
|
||||
this.handleWindowToggleMenuBar = this.handleWindowToggleMenuBar.bind(this)
|
||||
this.handleLinkClick = this.handleLinkClick.bind(this)
|
||||
this.handleDocumentContextmenu = this.handleDocumentContextmenu.bind(this)
|
||||
this.atomEnvironment = atomEnvironment
|
||||
this.applicationDelegate = applicationDelegate
|
||||
this.reloadRequested = false
|
||||
this.subscriptions = new CompositeDisposable()
|
||||
|
||||
this.handleNativeKeybindings()
|
||||
}
|
||||
|
||||
initialize (window, document) {
|
||||
this.window = window
|
||||
this.document = document
|
||||
this.subscriptions.add(this.atomEnvironment.commands.add(this.window, {
|
||||
'window:toggle-full-screen': this.handleWindowToggleFullScreen,
|
||||
'window:close': this.handleWindowClose,
|
||||
'window:reload': this.handleWindowReload,
|
||||
'window:toggle-dev-tools': this.handleWindowToggleDevTools
|
||||
}))
|
||||
|
||||
if (['win32', 'linux'].includes(process.platform)) {
|
||||
this.subscriptions.add(this.atomEnvironment.commands.add(this.window,
|
||||
{'window:toggle-menu-bar': this.handleWindowToggleMenuBar})
|
||||
)
|
||||
}
|
||||
|
||||
this.subscriptions.add(this.atomEnvironment.commands.add(this.document, {
|
||||
'core:focus-next': this.handleFocusNext,
|
||||
'core:focus-previous': this.handleFocusPrevious
|
||||
}))
|
||||
|
||||
this.addEventListener(this.window, 'beforeunload', this.handleWindowBeforeunload)
|
||||
this.addEventListener(this.window, 'focus', this.handleWindowFocus)
|
||||
this.addEventListener(this.window, 'blur', this.handleWindowBlur)
|
||||
|
||||
this.addEventListener(this.document, 'keyup', this.handleDocumentKeyEvent)
|
||||
this.addEventListener(this.document, 'keydown', this.handleDocumentKeyEvent)
|
||||
this.addEventListener(this.document, 'drop', this.handleDocumentDrop)
|
||||
this.addEventListener(this.document, 'dragover', this.handleDocumentDragover)
|
||||
this.addEventListener(this.document, 'contextmenu', this.handleDocumentContextmenu)
|
||||
this.subscriptions.add(listen(this.document, 'click', 'a', this.handleLinkClick))
|
||||
this.subscriptions.add(listen(this.document, 'submit', 'form', this.handleFormSubmit))
|
||||
|
||||
this.subscriptions.add(this.applicationDelegate.onDidEnterFullScreen(this.handleEnterFullScreen))
|
||||
this.subscriptions.add(this.applicationDelegate.onDidLeaveFullScreen(this.handleLeaveFullScreen))
|
||||
}
|
||||
|
||||
// Wire commands that should be handled by Chromium for elements with the
|
||||
// `.native-key-bindings` class.
|
||||
handleNativeKeybindings () {
|
||||
const bindCommandToAction = (command, action) => {
|
||||
this.subscriptions.add(
|
||||
this.atomEnvironment.commands.add(
|
||||
'.native-key-bindings',
|
||||
command,
|
||||
event => this.applicationDelegate.getCurrentWindow().webContents[action](),
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
bindCommandToAction('core:copy', 'copy')
|
||||
bindCommandToAction('core:paste', 'paste')
|
||||
bindCommandToAction('core:undo', 'undo')
|
||||
bindCommandToAction('core:redo', 'redo')
|
||||
bindCommandToAction('core:select-all', 'selectAll')
|
||||
bindCommandToAction('core:cut', 'cut')
|
||||
}
|
||||
|
||||
unsubscribe () {
|
||||
this.subscriptions.dispose()
|
||||
}
|
||||
|
||||
on (target, eventName, handler) {
|
||||
target.on(eventName, handler)
|
||||
this.subscriptions.add(new Disposable(function () {
|
||||
target.removeListener(eventName, handler)
|
||||
}))
|
||||
}
|
||||
|
||||
addEventListener (target, eventName, handler) {
|
||||
target.addEventListener(eventName, handler)
|
||||
this.subscriptions.add(new Disposable(function () {
|
||||
target.removeEventListener(eventName, handler)
|
||||
}))
|
||||
}
|
||||
|
||||
handleDocumentKeyEvent (event) {
|
||||
this.atomEnvironment.keymaps.handleKeyboardEvent(event)
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
|
||||
handleDrop (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
handleDragover (event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
event.dataTransfer.dropEffect = 'none'
|
||||
}
|
||||
|
||||
eachTabIndexedElement (callback) {
|
||||
for (let element of this.document.querySelectorAll('[tabindex]')) {
|
||||
if (element.disabled) { continue }
|
||||
if (!(element.tabIndex >= 0)) { continue }
|
||||
callback(element, element.tabIndex)
|
||||
}
|
||||
}
|
||||
|
||||
handleFocusNext () {
|
||||
const focusedTabIndex = this.document.activeElement.tabIndex != null ? this.document.activeElement.tabIndex : -Infinity
|
||||
|
||||
let nextElement = null
|
||||
let nextTabIndex = Infinity
|
||||
let lowestElement = null
|
||||
let lowestTabIndex = Infinity
|
||||
this.eachTabIndexedElement(function (element, tabIndex) {
|
||||
if (tabIndex < lowestTabIndex) {
|
||||
lowestTabIndex = tabIndex
|
||||
lowestElement = element
|
||||
}
|
||||
|
||||
if (focusedTabIndex < tabIndex && tabIndex < nextTabIndex) {
|
||||
nextTabIndex = tabIndex
|
||||
nextElement = element
|
||||
}
|
||||
})
|
||||
|
||||
if (nextElement != null) {
|
||||
nextElement.focus()
|
||||
} else if (lowestElement != null) {
|
||||
lowestElement.focus()
|
||||
}
|
||||
}
|
||||
|
||||
handleFocusPrevious () {
|
||||
const focusedTabIndex = this.document.activeElement.tabIndex != null ? this.document.activeElement.tabIndex : Infinity
|
||||
|
||||
let previousElement = null
|
||||
let previousTabIndex = -Infinity
|
||||
let highestElement = null
|
||||
let highestTabIndex = -Infinity
|
||||
this.eachTabIndexedElement(function (element, tabIndex) {
|
||||
if (tabIndex > highestTabIndex) {
|
||||
highestTabIndex = tabIndex
|
||||
highestElement = element
|
||||
}
|
||||
|
||||
if (focusedTabIndex > tabIndex && tabIndex > previousTabIndex) {
|
||||
previousTabIndex = tabIndex
|
||||
previousElement = element
|
||||
}
|
||||
})
|
||||
|
||||
if (previousElement != null) {
|
||||
previousElement.focus()
|
||||
} else if (highestElement != null) {
|
||||
highestElement.focus()
|
||||
}
|
||||
}
|
||||
|
||||
handleWindowFocus () {
|
||||
this.document.body.classList.remove('is-blurred')
|
||||
}
|
||||
|
||||
handleWindowBlur () {
|
||||
this.document.body.classList.add('is-blurred')
|
||||
this.atomEnvironment.storeWindowDimensions()
|
||||
}
|
||||
|
||||
handleEnterFullScreen () {
|
||||
this.document.body.classList.add('fullscreen')
|
||||
}
|
||||
|
||||
handleLeaveFullScreen () {
|
||||
this.document.body.classList.remove('fullscreen')
|
||||
}
|
||||
|
||||
handleWindowBeforeunload (event) {
|
||||
if (!this.reloadRequested && !this.atomEnvironment.inSpecMode() && this.atomEnvironment.getCurrentWindow().isWebViewFocused()) {
|
||||
this.atomEnvironment.hide()
|
||||
}
|
||||
this.reloadRequested = false
|
||||
this.atomEnvironment.storeWindowDimensions()
|
||||
this.atomEnvironment.unloadEditorWindow()
|
||||
this.atomEnvironment.destroy()
|
||||
}
|
||||
|
||||
handleWindowToggleFullScreen () {
|
||||
this.atomEnvironment.toggleFullScreen()
|
||||
}
|
||||
|
||||
handleWindowClose () {
|
||||
this.atomEnvironment.close()
|
||||
}
|
||||
|
||||
handleWindowReload () {
|
||||
this.reloadRequested = true
|
||||
this.atomEnvironment.reload()
|
||||
}
|
||||
|
||||
handleWindowToggleDevTools () {
|
||||
this.atomEnvironment.toggleDevTools()
|
||||
}
|
||||
|
||||
handleWindowToggleMenuBar () {
|
||||
this.atomEnvironment.config.set('core.autoHideMenuBar', !this.atomEnvironment.config.get('core.autoHideMenuBar'))
|
||||
|
||||
if (this.atomEnvironment.config.get('core.autoHideMenuBar')) {
|
||||
const detail = 'To toggle, press the Alt key or execute the window:toggle-menu-bar command'
|
||||
this.atomEnvironment.notifications.addInfo('Menu bar hidden', {detail})
|
||||
}
|
||||
}
|
||||
|
||||
handleLinkClick (event) {
|
||||
event.preventDefault()
|
||||
const uri = event.currentTarget && event.currentTarget.getAttribute('href')
|
||||
if (uri && (uri[0] !== '#') && /^https?:\/\//.test(uri)) {
|
||||
this.applicationDelegate.openExternal(uri)
|
||||
}
|
||||
}
|
||||
|
||||
handleFormSubmit (event) {
|
||||
// Prevent form submits from changing the current window's URL
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
handleDocumentContextmenu (event) {
|
||||
event.preventDefault()
|
||||
this.atomEnvironment.contextMenu.showForEvent(event)
|
||||
}
|
||||
}
|
||||
@@ -659,7 +659,7 @@ module.exports = class Workspace extends Model {
|
||||
// changing or closing tabs and ensures critical UI feedback, like changing the
|
||||
// highlighted tab, gets priority over work that can be done asynchronously.
|
||||
//
|
||||
// * `callback` {Function} to be called when the active pane item stopts
|
||||
// * `callback` {Function} to be called when the active pane item stops
|
||||
// changing.
|
||||
// * `item` The active pane item.
|
||||
//
|
||||
@@ -1050,10 +1050,10 @@ module.exports = class Workspace extends Model {
|
||||
|
||||
// Essential: Search the workspace for items matching the given URI and hide them.
|
||||
//
|
||||
// * `itemOrURI` (optional) The item to hide or a {String} containing the URI
|
||||
// * `itemOrURI` The item to hide or a {String} containing the URI
|
||||
// of the item to hide.
|
||||
//
|
||||
// Returns a {boolean} indicating whether any items were found (and hidden).
|
||||
// Returns a {Boolean} indicating whether any items were found (and hidden).
|
||||
hide (itemOrURI) {
|
||||
let foundItems = false
|
||||
|
||||
|
||||
Reference in New Issue
Block a user