diff --git a/.gitignore b/.gitignore index af9371e4d..32be5e65a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ tags /cef/ /sources.gypi /node/ +docs/api diff --git a/Rakefile b/Rakefile index bd8e4c7b7..10e00f439 100644 --- a/Rakefile +++ b/Rakefile @@ -116,6 +116,25 @@ task :tags do system %{find src native cef vendor -not -name "*spec.coffee" -type f -print0 | xargs -0 ctags} end +namespace :docs do + namespace :app do + desc "Builds the API docs in src/app" + task :build do + system %{./node_modules/biscotto/bin/biscotto -o docs/api src/app/} + end + + desc "Lists the stats for API doc coverage in src/app" + task :stats do + system %{./node_modules/biscotto/bin/biscotto --statsOnly src/app/} + end + + desc "Show which docs are missing" + task :missing do + system %{./node_modules/biscotto/bin/biscotto --listMissing src/app/} + end + end +end + def application_path applications = FileList["#{BUILD_DIR}/**/Atom.app"] if applications.size == 0 diff --git a/package.json b/package.json index 2bfa3c87f..086b99e0b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,10 @@ "less": "git://github.com/nathansobo/less.js.git" }, + "devDependencies" : { + "biscotto" : "0.0.8" + }, + "private": true, "scripts": { diff --git a/spec/app/atom-spec.coffee b/spec/app/atom-spec.coffee index 82a3d7672..0f24d9c71 100644 --- a/spec/app/atom-spec.coffee +++ b/spec/app/atom-spec.coffee @@ -2,6 +2,9 @@ $ = require 'jquery' RootView = require 'root-view' {$$} = require 'space-pen' fsUtils = require 'fs-utils' +Exec = require('child_process').exec +_ = require 'underscore' +Project = require 'project' describe "the `atom` global", -> beforeEach -> @@ -321,3 +324,26 @@ describe "the `atom` global", -> expect(atom.sendMessageToBrowserProcess.callCount).toBe 1 expect(atom.sendMessageToBrowserProcess.argsForCall[0][1][0]).toBe "A2" atom.sendMessageToBrowserProcess.simulateConfirmation('Next') + + describe "API documentation", -> + it "meets a minimum threshold for /app (with no errors)", -> + docRunner = jasmine.createSpy("docRunner") + Exec "cd #{project.resolve('../..')} && rake docs:app:stats", docRunner + waitsFor -> + docRunner.callCount > 0 + + runs -> + # error + expect(docRunner.argsForCall[0][0]).toBeNull() + + results = docRunner.argsForCall[0][1].split("\n") + results.pop() + + errors = parseInt results.pop().match(/\d+/) + expect(errors).toBe 0 + + coverage = parseFloat results.pop().match(/.+?%/) + expect(coverage).toBeGreaterThan 80 + + # stderr + expect(docRunner.argsForCall[0][2]).toBe '' \ No newline at end of file diff --git a/src/app/atom-package.coffee b/src/app/atom-package.coffee index 9013d461a..5aa89e281 100644 --- a/src/app/atom-package.coffee +++ b/src/app/atom-package.coffee @@ -5,6 +5,11 @@ _ = require 'underscore' $ = require 'jquery' CSON = require 'cson' + +### +# Internal: Loads and resolves packages. # +### + module.exports = class AtomPackage extends Package metadata: null diff --git a/src/app/atom-theme.coffee b/src/app/atom-theme.coffee index c679298a1..d0de2eaa0 100644 --- a/src/app/atom-theme.coffee +++ b/src/app/atom-theme.coffee @@ -2,12 +2,17 @@ fsUtils = require 'fs-utils' Theme = require 'theme' CSON = require 'cson' +# Internal: Represents a theme that Atom can use. module.exports = class AtomTheme extends Theme + # Internal: Given a path, this loads it as a stylesheet. + # + # stylesheetPath - A {String} to a stylesheet loadStylesheet: (stylesheetPath)-> @stylesheets[stylesheetPath] = window.loadStylesheet(stylesheetPath) + # Internal: Loads the stylesheets found in a `package.cson` file. load: -> if fsUtils.extension(@path) in ['.css', '.less'] @loadStylesheet(@path) diff --git a/src/app/binding-set.coffee b/src/app/binding-set.coffee index 9fadc8756..a13c564c1 100644 --- a/src/app/binding-set.coffee +++ b/src/app/binding-set.coffee @@ -5,6 +5,10 @@ fsUtils = require 'fs-utils' Specificity = require 'specificity' PEG = require 'pegjs' +### +# Internal # +### + module.exports = class BindingSet diff --git a/src/app/buffer-change-operation.coffee b/src/app/buffer-change-operation.coffee index cbbf68b0a..2dc6ccb08 100644 --- a/src/app/buffer-change-operation.coffee +++ b/src/app/buffer-change-operation.coffee @@ -1,6 +1,10 @@ Range = require 'range' _ = require 'underscore' +### +# Internal # +### + module.exports = class BufferChangeOperation buffer: null diff --git a/src/app/buffer-marker.coffee b/src/app/buffer-marker.coffee index 87b305155..4a6e35af1 100644 --- a/src/app/buffer-marker.coffee +++ b/src/app/buffer-marker.coffee @@ -10,10 +10,23 @@ class BufferMarker suppressObserverNotification: false invalidationStrategy: null + ### + # Internal # + ### constructor: ({@id, @buffer, range, @invalidationStrategy, noTail, reverse}) -> @invalidationStrategy ?= 'contains' @setRange(range, {noTail, reverse}) + ### + # Public # + ### + + # Public: Sets the marker's range, potentialy modifying both its head and tail. + # + # range - The new {Range} the marker should cover + # options - A hash of options with the following keys: + # :reverse - if `true`, the marker is reversed; that is, its tail is "above" the head + # :noTail - if `true`, the marker doesn't have a tail setRange: (range, options={}) -> @consolidateObserverNotifications false, => range = Range.fromObject(range) @@ -24,22 +37,47 @@ class BufferMarker @setTailPosition(range.start) unless options.noTail @setHeadPosition(range.end) + # Public: Identifies 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 {Buffer}. + # + # Returns a {Boolean}. isReversed: -> @tailPosition? and @headPosition.isLessThan(@tailPosition) + # Public: Identifies if the marker's head position is equal to its tail. + # + # Returns a {Boolean}. isRangeEmpty: -> @getHeadPosition().isEqual(@getTailPosition()) + # Public: Retrieves the {Range} between a marker's head and its tail. + # + # Returns a {Range}. getRange: -> if @tailPosition new Range(@tailPosition, @headPosition) else new Range(@headPosition, @headPosition) + # Public: Retrieves the position of the marker's head. + # + # Returns a {Point}. getHeadPosition: -> @headPosition + # Public: Retrieves the position of the marker's tail. + # + # Returns a {Point}. getTailPosition: -> @tailPosition ? @getHeadPosition() + # Public: Sets the position of the marker's head. + # + # newHeadPosition - The new {Point} to place the head + # options - A hash with the following keys: + # :clip - if `true`, the point is [clipped]{Buffer.clipPosition} + # :bufferChanged - if `true`, indicates that the {Buffer} should trigger an event that it's changed + # + # Returns a {Point} representing the new head position. setHeadPosition: (newHeadPosition, options={}) -> oldHeadPosition = @getHeadPosition() newHeadPosition = Point.fromObject(newHeadPosition) @@ -50,6 +88,14 @@ class BufferMarker @notifyObservers({oldHeadPosition, newHeadPosition, bufferChanged}) @headPosition + # Public: Sets the position of the marker's tail. + # + # newHeadPosition - The new {Point} to place the tail + # options - A hash with the following keys: + # :clip - if `true`, the point is [clipped]{Buffer.clipPosition} + # :bufferChanged - if `true`, indicates that the {Buffer} should trigger an event that it's changed + # + # Returns a {Point} representing the new tail position. setTailPosition: (newTailPosition, options={}) -> oldTailPosition = @getTailPosition() newTailPosition = Point.fromObject(newTailPosition) @@ -60,21 +106,52 @@ class BufferMarker @notifyObservers({oldTailPosition, newTailPosition, bufferChanged}) @tailPosition + # Public: Retrieves the starting position of the marker. + # + # Returns a {Point}. getStartPosition: -> @getRange().start + # Public: Retrieves the ending position of the marker. + # + # Returns a {Point}. getEndPosition: -> @getRange().end + # Public: 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. placeTail: -> @setTailPosition(@getHeadPosition()) unless @tailPosition + # Public: Removes the tail from the marker. clearTail: -> oldTailPosition = @getTailPosition() @tailPosition = null newTailPosition = @getTailPosition() @notifyObservers({oldTailPosition, newTailPosition, bufferChanged: false}) + # Public: Identifies if a {Point} is within the marker. + # + # Returns a {Boolean}. + containsPoint: (point) -> + @getRange().containsPoint(point) + + # Public: Sets a callback to be fired whenever a marker is changed. + observe: (callback) -> + @on 'changed', callback + cancel: => @unobserve(callback) + + # Public: Removes the fired callback whenever a marker changes. + unobserve: (callback) -> + @off 'changed', callback + + ### + # Internal # + ### + tryToInvalidate: (changedRange) -> betweenStartAndEnd = @getRange().containsRange(changedRange, exclusive: false) containsStart = changedRange.containsPoint(@getStartPosition(), exclusive: true) @@ -122,16 +199,6 @@ class BufferMarker [newRow, newColumn] - observe: (callback) -> - @on 'changed', callback - cancel: => @unobserve(callback) - - unobserve: (callback) -> - @off 'changed', callback - - containsPoint: (point) -> - @getRange().containsPoint(point) - notifyObservers: ({oldHeadPosition, newHeadPosition, oldTailPosition, newTailPosition, bufferChanged} = {}) -> return if @suppressObserverNotification diff --git a/src/app/config.coffee b/src/app/config.coffee index c57d357e5..5e2633fd0 100644 --- a/src/app/config.coffee +++ b/src/app/config.coffee @@ -11,6 +11,10 @@ vendoredThemesDirPath = fsUtils.join(resourcePath, "vendor/themes") userThemesDirPath = fsUtils.join(configDirPath, "themes") userPackagesDirPath = fsUtils.join(configDirPath, "packages") +# Public: Handles all of Atom's configuration details. +# +# This includes loading and setting default options, as well as reading from the +# user's configuration file. module.exports = class Config configDirPath: configDirPath @@ -22,6 +26,10 @@ class Config settings: null configFileHasErrors: null + ### + # Internal # + ### + constructor: -> @defaultSettings = core: _.clone(require('root-view').configDefaults) @@ -63,10 +71,24 @@ class Config console.error "Failed to load user config '#{@configFilePath}'", e.message console.error e.stack + # Public: Retrieves the setting for the given key. + # + # keyPath - The {String} name of the key to retrieve + # + # Returns the value from Atom's default settings, the user's configuration file, + # or `null` if the key doesn't exist in either. get: (keyPath) -> _.valueForKeyPath(@settings, keyPath) ? _.valueForKeyPath(@defaultSettings, keyPath) + # Public: Sets the value for a configuration setting. + # + # This value is stored in Atom's internal configuration file. + # + # keyPath - The {String} name of the key + # value - The value of the setting + # + # Returns the `value`. set: (keyPath, value) -> _.setValueForKeyPath(@settings, keyPath, value) @update() @@ -82,6 +104,13 @@ class Config _.extend hash, defaults @update() + # Public: Establishes an event listener for a given key. + # + # Whenever the value of the key is changed, a callback is fired. + # + # keyPath - The {String} name of the key to watch + # callback - The {Function} that fires when the. It is given a single argument, `value`, + # which is the new value of `keyPath`. observe: (keyPath, callback) -> value = @get(keyPath) previousValue = _.clone(value) diff --git a/src/app/cursor-view.coffee b/src/app/cursor-view.coffee index a2bead1f8..6e4c96686 100644 --- a/src/app/cursor-view.coffee +++ b/src/app/cursor-view.coffee @@ -3,6 +3,7 @@ Point = require 'point' Range = require 'range' _ = require 'underscore' +# Internal: module.exports = class CursorView extends View @content: -> diff --git a/src/app/cursor.coffee b/src/app/cursor.coffee index 696cbfb4d..e2905fdb7 100644 --- a/src/app/cursor.coffee +++ b/src/app/cursor.coffee @@ -3,6 +3,9 @@ Range = require 'range' EventEmitter = require 'event-emitter' _ = require 'underscore' +# Public: The `Cursor` class represents the little blinking line identifying where text can be inserted. +# +# Cursors have some metadata attached in the form of a {BufferMarker}. module.exports = class Cursor screenPosition: null @@ -11,6 +14,9 @@ class Cursor visible: true needsAutoscroll: null + ### + # Internal # + ### constructor: ({@editSession, @marker}) -> @updateVisibility() @editSession.observeMarker @marker, (e) => @@ -39,20 +45,6 @@ class Cursor @editSession.removeCursor(this) @trigger 'destroyed' - setScreenPosition: (screenPosition, options={}) -> - @changePosition options, => - @editSession.setMarkerHeadScreenPosition(@marker, screenPosition, options) - - getScreenPosition: -> - @editSession.getMarkerHeadScreenPosition(@marker) - - setBufferPosition: (bufferPosition, options={}) -> - @changePosition options, => - @editSession.setMarkerHeadBufferPosition(@marker, bufferPosition, options) - - getBufferPosition: -> - @editSession.getMarkerHeadBufferPosition(@marker) - changePosition: (options, fn) -> @goalColumn = null @clearSelection() @@ -60,82 +52,161 @@ class Cursor unless fn() @trigger 'autoscrolled' if @needsAutoscroll + ### + # Public # + ### + + # Public: Moves a cursor to a given screen position. + # + # screenPosition - An {Array} of two numbers: the screen row, and the screen column. + # options - An object with the following keys: + # :autoscroll - A {Boolean} which, if `true`, scrolls the {EditSession} to wherever the cursor moves to + # + setScreenPosition: (screenPosition, options={}) -> + @changePosition options, => + @editSession.setMarkerHeadScreenPosition(@marker, screenPosition, options) + + # Public: Gets the screen position of the cursor. + # + # Returns an {Array} of two numbers: the screen row, and the screen column. + getScreenPosition: -> + @editSession.getMarkerHeadScreenPosition(@marker) + + # Public: Moves a cursor to a given buffer position. + # + # bufferPosition - An {Array} of two numbers: the buffer row, and the buffer column. + # options - An object with the following keys: + # :autoscroll - A {Boolean} which, if `true`, scrolls the {EditSession} to wherever the cursor moves to + # + setBufferPosition: (bufferPosition, options={}) -> + @changePosition options, => + @editSession.setMarkerHeadBufferPosition(@marker, bufferPosition, options) + + # Public: Gets the current buffer position. + # + # Returns an {Array} of two numbers: the buffer row, and the buffer column. + getBufferPosition: -> + @editSession.getMarkerHeadBufferPosition(@marker) + + # Public: If the marker range is empty, the cursor is marked as being visible. updateVisibility: -> @setVisible(@editSession.isMarkerRangeEmpty(@marker)) + # Public: Sets the visibility of the cursor. + # + # visible - A {Boolean} indicating whether the cursor should be visible setVisible: (visible) -> if @visible != visible @visible = visible @needsAutoscroll ?= true if @visible and @isLastCursor() @trigger 'visibility-changed', @visible + # Public: Retrieves the visibility of the cursor. + # + # Returns a {Boolean}. isVisible: -> @visible + # Public: Identifies what the cursor considers a "word" RegExp. + # + # Returns a {RegExp}. wordRegExp: -> nonWordCharacters = config.get("editor.nonWordCharacters") new RegExp("^[\t ]*$|[^\\s#{_.escapeRegExp(nonWordCharacters)}]+|[#{_.escapeRegExp(nonWordCharacters)}]+", "g") + # Public: Identifies if this cursor is the last in the {EditSession}. + # + # Returns a {Boolean}. isLastCursor: -> this == @editSession.getCursor() + # Public: Identifies if the cursor is surrounded by whitespace. + # + # "Surrounded" here means that all characters before and after the cursor is whitespace. + # + # Returns a {Boolean}. isSurroundedByWhitespace: -> {row, column} = @getBufferPosition() range = [[row, Math.min(0, column - 1)], [row, Math.max(0, column + 1)]] /^\s+$/.test @editSession.getTextInBufferRange(range) + # Public: Removes the setting for auto-scroll. clearAutoscroll: -> @needsAutoscroll = null + # Public: Deselects whatever the cursor is selecting. clearSelection: -> if @selection @selection.goalBufferRange = null @selection.clear() unless @selection.retainSelection + # Public: Retrieves the cursor's screen row. + # + # Returns a {Number}. getScreenRow: -> @getScreenPosition().row + # Public: Retrieves the cursor's screen column. + # + # Returns a {Number}. getScreenColumn: -> @getScreenPosition().column + # Public: Retrieves the cursor's buffer row. + # + # Returns a {Number}. getBufferRow: -> @getBufferPosition().row + # Public: Retrieves the cursor's buffer column. + # + # Returns a {Number}. getBufferColumn: -> @getBufferPosition().column + # Public: Retrieves the cursor's buffer row text. + # + # Returns a {String}. getCurrentBufferLine: -> @editSession.lineForBufferRow(@getBufferRow()) + # Public: Moves the cursor up one screen row. moveUp: (rowCount = 1) -> { row, column } = @getScreenPosition() column = @goalColumn if @goalColumn? @setScreenPosition({row: row - rowCount, column: column}) @goalColumn = column + # Public: Moves the cursor down one screen row. moveDown: (rowCount = 1) -> { row, column } = @getScreenPosition() column = @goalColumn if @goalColumn? @setScreenPosition({row: row + rowCount, column: column}) @goalColumn = column + # Public: Moves the cursor left one screen column. moveLeft: -> { row, column } = @getScreenPosition() [row, column] = if column > 0 then [row, column - 1] else [row - 1, Infinity] @setScreenPosition({row, column}) + # Public: Moves the cursor right one screen column. moveRight: -> { row, column } = @getScreenPosition() @setScreenPosition([row, column + 1], skipAtomicTokens: true, wrapBeyondNewlines: true, wrapAtSoftNewlines: true) + # Public: Moves the cursor to the top of the buffer. moveToTop: -> @setBufferPosition([0,0]) + # Public: Moves the cursor to the bottom of the buffer. moveToBottom: -> @setBufferPosition(@editSession.getEofBufferPosition()) + # Public: Moves the cursor to the beginning of the buffer line. moveToBeginningOfLine: -> @setBufferPosition([@getBufferRow(), 0]) + # Public: Moves the cursor to the beginning of the first character in the line. moveToFirstCharacterOfLine: -> position = @getBufferPosition() scanRange = @getCurrentLineBufferRange() @@ -146,6 +217,7 @@ class Cursor newPosition = [position.row, 0] if newPosition.isEqual(position) @setBufferPosition(newPosition) + # Public: Moves the cursor to the beginning of the buffer line, skipping all whitespace. skipLeadingWhitespace: -> position = @getBufferPosition() scanRange = @getCurrentLineBufferRange() @@ -155,20 +227,30 @@ class Cursor @setBufferPosition(endOfLeadingWhitespace) if endOfLeadingWhitespace.isGreaterThan(position) + # Public: Moves the cursor to the end of the buffer line. moveToEndOfLine: -> @setBufferPosition([@getBufferRow(), Infinity]) + # Public: Moves the cursor to the beginning of the word. moveToBeginningOfWord: -> @setBufferPosition(@getBeginningOfCurrentWordBufferPosition()) + # Public: Moves the cursor to the end of the word. moveToEndOfWord: -> if position = @getEndOfCurrentWordBufferPosition() @setBufferPosition(position) + # Public: Moves the cursor to the beginning of the next word. moveToBeginningOfNextWord: -> if position = @getBeginningOfNextWordBufferPosition() @setBufferPosition(position) + # Public: Retrieves the buffer position of where the current word starts. + # + # options - A hash with one option: + # :wordRegex - A {RegExp} indicating what constitutes a "word" (default: {wordRegExp}) + # + # Returns a {Range}. getBeginningOfCurrentWordBufferPosition: (options = {}) -> allowPrevious = options.allowPrevious ? true currentBufferPosition = @getBufferPosition() @@ -184,6 +266,12 @@ class Cursor beginningOfWordPosition or currentBufferPosition + # Public: Retrieves the buffer position of where the current word ends. + # + # options - A hash with one option: + # :wordRegex - A {RegExp} indicating what constitutes a "word" (default: {wordRegExp}) + # + # Returns a {Range}. getEndOfCurrentWordBufferPosition: (options = {}) -> allowNext = options.allowNext ? true currentBufferPosition = @getBufferPosition() @@ -198,6 +286,12 @@ class Cursor endOfWordPosition ? currentBufferPosition + # Public: Retrieves the buffer position of where the next word starts. + # + # options - A hash with one option: + # :wordRegex - A {RegExp} indicating what constitutes a "word" (default: {wordRegExp}) + # + # Returns a {Range}. getBeginningOfNextWordBufferPosition: (options = {}) -> currentBufferPosition = @getBufferPosition() start = if @isSurroundedByWhitespace() then currentBufferPosition else @getEndOfCurrentWordBufferPosition() @@ -210,14 +304,29 @@ class Cursor beginningOfNextWordPosition or currentBufferPosition + # Public: Gets the word located under the cursor. + # + # options - An object with properties based on {.getBeginningOfCurrentWordBufferPosition}. + # + # Returns a {String}. getCurrentWordBufferRange: (options={}) -> startOptions = _.extend(_.clone(options), allowPrevious: false) endOptions = _.extend(_.clone(options), allowNext: false) new Range(@getBeginningOfCurrentWordBufferPosition(startOptions), @getEndOfCurrentWordBufferPosition(endOptions)) + # Public: Retrieves the range for the current line. + # + # options - A hash with the same keys as {EditSession.bufferRangeForBufferRow} + # + # Returns a {Range}. getCurrentLineBufferRange: (options) -> @editSession.bufferRangeForBufferRow(@getBufferRow(), options) + # Public: Retrieves the range for the current paragraph. + # + # A paragraph is defined as a block of text surrounded by empty lines. + # + # Returns a {Range}. getCurrentParagraphBufferRange: -> row = @getBufferRow() return unless /\w/.test(@editSession.lineForBufferRow(row)) @@ -235,21 +344,36 @@ class Cursor new Range([startRow, 0], [endRow, @editSession.lineLengthForBufferRow(endRow)]) + # Public: Retrieves the characters that constitute a word preceeding the current cursor position. + # + # Returns a {String}. getCurrentWordPrefix: -> @editSession.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()]) + # Public: Identifies if the cursor is at the start of a line. + # + # Returns a {Boolean}. isAtBeginningOfLine: -> @getBufferPosition().column == 0 + # Public: Retrieves the indentation level of the current line. + # + # Returns a {Number}. getIndentLevel: -> if @editSession.softTabs @getBufferColumn() / @editSession.getTabLength() else @getBufferColumn() + # Public: Identifies if the cursor is at the end of a line. + # + # Returns a {Boolean}. isAtEndOfLine: -> @getBufferPosition().isEqual(@getCurrentLineBufferRange().end) + # Public: Retrieves the grammar's token scopes for the line. + # + # Returns an {Array} of {String}s. getScopes: -> @editSession.scopesForBufferPosition(@getBufferPosition()) diff --git a/src/app/directory.coffee b/src/app/directory.coffee index aadc75a94..1b480de9b 100644 --- a/src/app/directory.coffee +++ b/src/app/directory.coffee @@ -5,17 +5,35 @@ pathWatcher = require 'pathwatcher' File = require 'file' EventEmitter = require 'event-emitter' +# Public: Represents a directory in the project. +# +# Directories contain an array of {File}s. module.exports = class Directory path: null + # Public: Creates a new directory. + # + # path - A {String} representing the file directory + # symlink - A {Boolean} indicating if the path is a symlink (default: false) constructor: (@path, @symlink=false) -> + # Public: Retrieves the basename of the directory. + # + # Returns a {String}. getBaseName: -> fsUtils.base(@path) + # Public: Retrieves the directory's path. + # + # Returns a {String}. getPath: -> @path + # Public: Retrieves the file entries in the directory. + # + # This does follow symlinks. + # + # Returns an {Array} of {Files}. getEntries: -> directories = [] files = [] @@ -33,6 +51,10 @@ class Directory directories.concat(files) + ### + # Internal # + ### + afterSubscribe: -> @subscribeToNativeChangeEvents() if @subscriptionCount() == 1 @@ -42,7 +64,7 @@ class Directory subscribeToNativeChangeEvents: -> @watchSubscription = pathWatcher.watch @path, (eventType) => @trigger "contents-changed" if eventType is "change" - + unsubscribeFromNativeChangeEvents: -> if @watchSubscription? @watchSubscription.close() diff --git a/src/app/display-buffer-marker.coffee b/src/app/display-buffer-marker.coffee index c8ed1648a..d88f67bdc 100644 --- a/src/app/display-buffer-marker.coffee +++ b/src/app/display-buffer-marker.coffee @@ -9,62 +9,128 @@ class DisplayBufferMarker tailScreenPosition: null valid: true + ### + # Internal # + ### + constructor: ({@id, @displayBuffer}) -> @buffer = @displayBuffer.buffer + ### + # Public # + ### + + # Public: Gets the screen range of the display marker. + # + # Returns a {Range}. getScreenRange: -> @displayBuffer.screenRangeForBufferRange(@getBufferRange(), wrapAtSoftNewlines: true) + # Public: Modifies the screen range of the display marker. + # + # screenRange - The new {Range} to use + # options - A hash of options matching those found in {BufferMarker.setRange} setScreenRange: (screenRange, options) -> - @setBufferRange(@displayBuffer.bufferRangeForScreenRange(screenRange, options), options) + @setBufferRange(@displayBuffer.bufferRangeForScreenRange(screenRange), options) + # Public: Gets the buffer range of the display marker. + # + # Returns a {Range}. getBufferRange: -> @buffer.getMarkerRange(@id) + # Public: Modifies the buffer range of the display marker. + # + # screenRange - The new {Range} to use + # options - A hash of options matching those found in {BufferMarker.setRange} setBufferRange: (bufferRange, options) -> @buffer.setMarkerRange(@id, bufferRange, options) + # Public: Retrieves the screen position of the marker's head. + # + # Returns a {Point}. getHeadScreenPosition: -> @headScreenPosition ?= @displayBuffer.screenPositionForBufferPosition(@getHeadBufferPosition(), wrapAtSoftNewlines: true) + # Public: Sets the screen position of the marker's head. + # + # screenRange - The new {Point} to use + # options - A hash of options matching those found in {DisplayBuffer.bufferPositionForScreenPosition} setHeadScreenPosition: (screenPosition, options) -> screenPosition = @displayBuffer.clipScreenPosition(screenPosition, options) @setHeadBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, options)) + # Public: Retrieves the buffer position of the marker's head. + # + # Returns a {Point}. getHeadBufferPosition: -> @buffer.getMarkerHeadPosition(@id) + # Public: Sets the buffer position of the marker's head. + # + # screenRange - The new {Point} to use + # options - A hash of options matching those found in {DisplayBuffer.bufferPositionForScreenPosition} setHeadBufferPosition: (bufferPosition) -> @buffer.setMarkerHeadPosition(@id, bufferPosition) + # Public: Retrieves the screen position of the marker's tail. + # + # Returns a {Point}. getTailScreenPosition: -> @tailScreenPosition ?= @displayBuffer.screenPositionForBufferPosition(@getTailBufferPosition(), wrapAtSoftNewlines: true) - + + # Public: Sets the screen position of the marker's tail. + # + # screenRange - The new {Point} to use + # options - A hash of options matching those found in {DisplayBuffer.bufferPositionForScreenPosition} setTailScreenPosition: (screenPosition, options) -> screenPosition = @displayBuffer.clipScreenPosition(screenPosition, options) @setTailBufferPosition(@displayBuffer.bufferPositionForScreenPosition(screenPosition, options)) + # Public: Retrieves the buffer position of the marker's tail. + # + # Returns a {Point}. getTailBufferPosition: -> @buffer.getMarkerTailPosition(@id) - + + # Public: Sets the buffer position of the marker's tail. + # + # screenRange - The new {Point} to use + # options - A hash of options matching those found in {DisplayBuffer.bufferPositionForScreenPosition} setTailBufferPosition: (bufferPosition) -> @buffer.setMarkerTailPosition(@id, bufferPosition) + # Public: 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. placeTail: -> @buffer.placeMarkerTail(@id) + # Public: Removes the tail from the marker. clearTail: -> @buffer.clearMarkerTail(@id) + # Public: Sets a callback to be fired whenever the marker is changed. + # + # callback - A {Function} to execute observe: (callback) -> @observeBufferMarkerIfNeeded() @on 'changed', callback cancel: => @unobserve(callback) + # Public: Removes the callback that's fired whenever the marker changes. + # + # callback - A {Function} to remove unobserve: (callback) -> @off 'changed', callback @unobserveBufferMarkerIfNeeded() + ### + # Internal # + ### + observeBufferMarkerIfNeeded: -> return if @subscriptionCount() @getHeadScreenPosition() # memoize current value diff --git a/src/app/display-buffer.coffee b/src/app/display-buffer.coffee index 37aba8bff..ee7daac27 100644 --- a/src/app/display-buffer.coffee +++ b/src/app/display-buffer.coffee @@ -19,6 +19,10 @@ class DisplayBuffer foldsById: null markers: null + ### + # Internal # + ### + constructor: (@buffer, options={}) -> @id = @constructor.idCounter++ @languageMode = options.languageMode @@ -31,8 +35,6 @@ class DisplayBuffer @tokenizedBuffer.on 'changed', @handleTokenizedBufferChange @buffer.on 'markers-updated', @handleMarkersUpdated - setVisible: (visible) -> @tokenizedBuffer.setVisible(visible) - buildLineMap: -> @lineMap = new LineMap @lineMap.insertAtScreenRow 0, @buildLinesForBufferRows(0, @buffer.getLastRow()) @@ -44,6 +46,15 @@ class DisplayBuffer @trigger 'changed', eventProperties @resumeMarkerObservers() + ### + # Public # + ### + + setVisible: (visible) -> @tokenizedBuffer.setVisible(visible) + + # Public: Defines the limit at which the buffer begins to soft wrap text. + # + # softWrapColumn - A {Number} defining the soft wrap limit. setSoftWrapColumn: (@softWrapColumn) -> start = 0 end = @getLastRow() @@ -51,19 +62,40 @@ class DisplayBuffer screenDelta = @getLastRow() - end bufferDelta = 0 @triggerChanged({ start, end, screenDelta, bufferDelta }) - + + # Public: Gets the line for the given screen row. + # + # screenRow - A {Number} indicating the screen row. + # + # Returns a {String}. lineForRow: (row) -> @lineMap.lineForScreenRow(row) + # Public: Gets the lines for the given screen row boundaries. + # + # startRow - A {Number} indicating the beginning screen row. + # endRow - A {Number} indicating the ending screen row. + # + # Returns an {Array} of {String}s. linesForRows: (startRow, endRow) -> @lineMap.linesForScreenRows(startRow, endRow) + # Public: Gets the lines in the buffer. + # + # Returns an {Array} of {String}s. getLines: -> @lineMap.linesForScreenRows(0, @lineMap.lastScreenRow()) + # Public: Given a starting and ending row, this converts every row into a buffer position. + # + # startRow - The row {Number} to start at + # endRow - The row {Number} to end at (default: {.getLastRow}) + # + # Returns an {Array} of {Range}s. bufferRowsForScreenRows: (startRow, endRow) -> @lineMap.bufferRowsForScreenRows(startRow, endRow) + # Public: Folds all the foldable lines in the buffer. foldAll: -> for currentRow in [0..@buffer.getLastRow()] [startRow, endRow] = @languageMode.rowRangeForFoldAtBufferRow(currentRow) ? [] @@ -71,6 +103,7 @@ class DisplayBuffer @createFold(startRow, endRow) + # Public: Unfolds all the foldable lines in the buffer. unfoldAll: -> for row in [@buffer.getLastRow()..0] @activeFolds[row]?.forEach (fold) => @destroyFold(fold) @@ -90,6 +123,9 @@ class DisplayBuffer endRow = currentRow return [startRow, endRow] if startRow isnt endRow + # Public: Given a buffer row, this folds it. + # + # bufferRow - A {Number} indicating the buffer row foldBufferRow: (bufferRow) -> for currentRow in [bufferRow..0] rowRange = @rowRangeForCommentAtBufferRow(currentRow) @@ -103,9 +139,18 @@ class DisplayBuffer return + # Public: Given a buffer row, this unfolds it. + # + # bufferRow - A {Number} indicating the buffer row unfoldBufferRow: (bufferRow) -> @largestFoldContainingBufferRow(bufferRow)?.destroy() + # Public: Creates a new fold between two row numbers. + # + # startRow - The row {Number} to start folding at + # endRow - The row {Number} to end the fold + # + # Returns the new {Fold}. createFold: (startRow, endRow) -> return fold if fold = @foldFor(startRow, endRow) fold = new Fold(this, startRow, endRow) @@ -127,15 +172,210 @@ class DisplayBuffer fold + # Public: Given a {Fold}, determines if it is contained within another fold. + # + # fold - The {Fold} to check + # + # Returns the contaiing {Fold} (if it exists), `null` otherwise. isFoldContainedByActiveFold: (fold) -> for row, folds of @activeFolds for otherFold in folds return otherFold if fold != otherFold and fold.isContainedByFold(otherFold) + # Public: Given a starting and ending row, tries to find an existing fold. + # + # startRow - A {Number} representing a fold's starting row + # endRow - A {Number} representing a fold's ending row + # + # Returns a {Fold} (if it exists). foldFor: (startRow, endRow) -> _.find @activeFolds[startRow] ? [], (fold) -> fold.startRow == startRow and fold.endRow == endRow + # Public: Removes any folds found that contain the given buffer row. + # + # bufferRow - The buffer row {Number} to check against + destroyFoldsContainingBufferRow: (bufferRow) -> + for row, folds of @activeFolds + for fold in new Array(folds...) + fold.destroy() if fold.getBufferRange().containsRow(bufferRow) + + # Public: Given a buffer row, this returns the largest fold that starts there. + # + # Largest is defined as the fold whose difference between its start and end points + # are the greatest. + # + # bufferRow - A {Number} indicating the buffer row + # + # Returns a {Fold}. + largestFoldStartingAtBufferRow: (bufferRow) -> + return unless folds = @activeFolds[bufferRow] + (folds.sort (a, b) -> b.endRow - a.endRow)[0] + + # Public: Given a screen row, this returns the largest fold that starts there. + # + # Largest is defined as the fold whose difference between its start and end points + # are the greatest. + # + # screenRow - A {Number} indicating the screen row + # + # Returns a {Fold}. + largestFoldStartingAtScreenRow: (screenRow) -> + @largestFoldStartingAtBufferRow(@bufferRowForScreenRow(screenRow)) + + # Public: Given a buffer row, this returns the largest fold that includes it. + # + # Largest is defined as the fold whose difference between its start and end points + # are the greatest. + # + # bufferRow - A {Number} indicating the buffer row + # + # Returns a {Fold}. + largestFoldContainingBufferRow: (bufferRow) -> + largestFold = null + for currentBufferRow in [bufferRow..0] + if fold = @largestFoldStartingAtBufferRow(currentBufferRow) + largestFold = fold if fold.endRow >= bufferRow + largestFold + + # Public: Given a buffer range, this converts it into a screen range. + # + # bufferRange - A {Range} consisting of buffer positions + # + # Returns a {Range}. + screenLineRangeForBufferRange: (bufferRange) -> + @expandScreenRangeToLineEnds( + @lineMap.screenRangeForBufferRange( + @expandBufferRangeToLineEnds(bufferRange))) + + # Public: Given a buffer row, this converts it into a screen row. + # + # bufferRow - A {Number} representing a buffer row + # + # Returns a {Number}. + screenRowForBufferRow: (bufferRow) -> + @lineMap.screenPositionForBufferPosition([bufferRow, 0]).row + + lastScreenRowForBufferRow: (bufferRow) -> + @lineMap.screenPositionForBufferPosition([bufferRow, Infinity]).row + + # Public: Given a screen row, this converts it into a buffer row. + # + # screenRow - A {Number} representing a screen row + # + # Returns a {Number}. + bufferRowForScreenRow: (screenRow) -> + @lineMap.bufferPositionForScreenPosition([screenRow, 0]).row + + # Public: Given a buffer range, this converts it into a screen position. + # + # bufferRange - The {Range} to convert + # + # Returns a {Range}. + screenRangeForBufferRange: (bufferRange) -> + @lineMap.screenRangeForBufferRange(bufferRange) + + # Public: Given a screen range, this converts it into a buffer position. + # + # screenRange - The {Range} to convert + # + # Returns a {Range}. + bufferRangeForScreenRange: (screenRange) -> + @lineMap.bufferRangeForScreenRange(screenRange) + + # Public: Gets the number of lines in the buffer. + # + # Returns a {Number}. + lineCount: -> + @lineMap.screenLineCount() + + # Public: Gets the number of the last row in the buffer. + # + # Returns a {Number}. + getLastRow: -> + @lineCount() - 1 + + # Public: Gets the length of the longest screen line. + # + # Returns a {Number}. + maxLineLength: -> + @lineMap.maxScreenLineLength + + # Public: Given a buffer position, this converts it into a screen position. + # + # bufferPosition - An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # options - A hash of options with the following keys: + # :wrapBeyondNewlines - + # :wrapAtSoftNewlines - + # + # Returns a {Point}. + screenPositionForBufferPosition: (position, options) -> + @lineMap.screenPositionForBufferPosition(position, options) + + # Public: Given a buffer range, this converts it into a screen position. + # + # screenPosition - An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # options - A hash of options with the following keys: + # :wrapBeyondNewlines - + # :wrapAtSoftNewlines - + # + # Returns a {Point}. + bufferPositionForScreenPosition: (position, options) -> + @lineMap.bufferPositionForScreenPosition(position, options) + + # Public: Retrieves the grammar's token scopes for a buffer position. + # + # bufferPosition - A {Point} in the {Buffer} + # + # Returns an {Array} of {String}s. + scopesForBufferPosition: (bufferPosition) -> + @tokenizedBuffer.scopesForPosition(bufferPosition) + + # Public: Retrieves the current tab length. + # + # Returns a {Number}. + getTabLength: -> + @tokenizedBuffer.getTabLength() + + # Public: Specifies the tab length. + # + # tabLength - A {Number} that defines the new tab length. + setTabLength: (tabLength) -> + @tokenizedBuffer.setTabLength(tabLength) + + # Public: Given a position, this clips it to a real position. + # + # For example, if `position`'s row exceeds the row count of the buffer, + # or if its column goes beyond a line's length, this "sanitizes" the value + # to a real position. + # + # position - The {Point} to clip + # options - A hash with the following values: + # :wrapBeyondNewlines - if `true`, continues wrapping past newlines + # :wrapAtSoftNewlines - if `true`, continues wrapping past soft newlines + # :screenLine - if `true`, indicates that you're using a line number, not a row number + # + # Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed. + clipScreenPosition: (position, options) -> + @lineMap.clipScreenPosition(position, options) + + ### + # Internal # + ### + + registerFold: (fold) -> + @activeFolds[fold.startRow] ?= [] + @activeFolds[fold.startRow].push(fold) + @foldsById[fold.id] = fold + + unregisterFold: (bufferRow, fold) -> + folds = @activeFolds[bufferRow] + _.remove(folds, fold) + delete @foldsById[fold.id] + delete @activeFolds[bufferRow] if folds.length == 0 + destroyFold: (fold) -> @unregisterFold(fold.startRow, fold) @@ -154,83 +394,6 @@ class DisplayBuffer @triggerChanged({ start, end, screenDelta, bufferDelta }) - destroyFoldsContainingBufferRow: (bufferRow) -> - for row, folds of @activeFolds - for fold in new Array(folds...) - fold.destroy() if fold.getBufferRange().containsRow(bufferRow) - - registerFold: (fold) -> - @activeFolds[fold.startRow] ?= [] - @activeFolds[fold.startRow].push(fold) - @foldsById[fold.id] = fold - - unregisterFold: (bufferRow, fold) -> - folds = @activeFolds[bufferRow] - _.remove(folds, fold) - delete @foldsById[fold.id] - delete @activeFolds[bufferRow] if folds.length == 0 - - largestFoldStartingAtBufferRow: (bufferRow) -> - return unless folds = @activeFolds[bufferRow] - (folds.sort (a, b) -> b.endRow - a.endRow)[0] - - largestFoldStartingAtScreenRow: (screenRow) -> - @largestFoldStartingAtBufferRow(@bufferRowForScreenRow(screenRow)) - - largestFoldContainingBufferRow: (bufferRow) -> - largestFold = null - for currentBufferRow in [bufferRow..0] - if fold = @largestFoldStartingAtBufferRow(currentBufferRow) - largestFold = fold if fold.endRow >= bufferRow - largestFold - - screenLineRangeForBufferRange: (bufferRange) -> - @expandScreenRangeToLineEnds( - @lineMap.screenRangeForBufferRange( - @expandBufferRangeToLineEnds(bufferRange))) - - screenRowForBufferRow: (bufferRow) -> - @lineMap.screenPositionForBufferPosition([bufferRow, 0]).row - - lastScreenRowForBufferRow: (bufferRow) -> - @lineMap.screenPositionForBufferPosition([bufferRow, Infinity]).row - - bufferRowForScreenRow: (screenRow) -> - @lineMap.bufferPositionForScreenPosition([screenRow, 0]).row - - screenRangeForBufferRange: (bufferRange) -> - @lineMap.screenRangeForBufferRange(bufferRange) - - bufferRangeForScreenRange: (screenRange) -> - @lineMap.bufferRangeForScreenRange(screenRange) - - lineCount: -> - @lineMap.screenLineCount() - - getLastRow: -> - @lineCount() - 1 - - maxLineLength: -> - @lineMap.maxScreenLineLength - - screenPositionForBufferPosition: (position, options) -> - @lineMap.screenPositionForBufferPosition(position, options) - - bufferPositionForScreenPosition: (position, options) -> - @lineMap.bufferPositionForScreenPosition(position, options) - - scopesForBufferPosition: (bufferPosition) -> - @tokenizedBuffer.scopesForPosition(bufferPosition) - - getTabLength: -> - @tokenizedBuffer.getTabLength() - - setTabLength: (tabLength) -> - @tokenizedBuffer.setTabLength(tabLength) - - clipScreenPosition: (position, options) -> - @lineMap.clipScreenPosition(position, options) - handleBufferChange: (e) -> allFolds = [] # Folds can modify @activeFolds, so first make sure we have a stable array of folds allFolds.push(folds...) for row, folds of @activeFolds @@ -299,6 +462,17 @@ class DisplayBuffer lineFragments + ### + # Public # + ### + + # Public: Given a line, finds the point where it would wrap. + # + # line - The {String} to check + # softWrapColumn - The {Number} where you want soft wrapping to occur + # + # Returns a {Number} representing the `line` position where the wrap would take place. + # Returns `null` if a wrap wouldn't occur. findWrapColumn: (line, softWrapColumn) -> return unless line.length > softWrapColumn @@ -313,99 +487,247 @@ class DisplayBuffer return column + 1 if /\s/.test(line[column]) return softWrapColumn + # Public: Given a range in screen coordinates, this expands it to the start and end of a line + # + # screenRange - The {Range} to expand + # + # Returns a new {Range}. expandScreenRangeToLineEnds: (screenRange) -> screenRange = Range.fromObject(screenRange) { start, end } = screenRange new Range([start.row, 0], [end.row, @lineMap.lineForScreenRow(end.row).text.length]) + # Public: Given a range in buffer coordinates, this expands it to the start and end of a line + # + # screenRange - The {Range} to expand + # + # Returns a new {Range}. expandBufferRangeToLineEnds: (bufferRange) -> bufferRange = Range.fromObject(bufferRange) { start, end } = bufferRange new Range([start.row, 0], [end.row, Infinity]) + # Public: Calculates a {Range} representing the start of the {Buffer} until the end. + # + # Returns a {Range}. rangeForAllLines: -> new Range([0, 0], @clipScreenPosition([Infinity, Infinity])) + # Public: Retrieves a {DisplayBufferMarker} based on its id. + # + # id - A {Number} representing a marker id + # + # Returns the {DisplayBufferMarker} (if it exists). getMarker: (id) -> @markers[id] ? new DisplayBufferMarker({id, displayBuffer: this}) + # Public: Retrieves the active markers in the buffer. + # + # Returns an {Array} of existing {DisplayBufferMarker}s. getMarkers: -> _.values(@markers) + # Public: Constructs a new marker at the given screen range. + # + # range - The marker {Range} (representing the distance between the head and tail) + # options - Options to pass to the {BufferMarker} constructor + # + # Returns a {Number} representing the new marker's ID. markScreenRange: (args...) -> bufferRange = @bufferRangeForScreenRange(args.shift()) @markBufferRange(bufferRange, args...) + # Public: Constructs a new marker at the given buffer range. + # + # range - The marker {Range} (representing the distance between the head and tail) + # options - Options to pass to the {BufferMarker} constructor + # + # Returns a {Number} representing the new marker's ID. markBufferRange: (args...) -> @buffer.markRange(args...) + # Public: Constructs a new marker at the given screen position. + # + # range - The marker {Range} (representing the distance between the head and tail) + # options - Options to pass to the {BufferMarker} constructor + # + # Returns a {Number} representing the new marker's ID. markScreenPosition: (screenPosition, options) -> @markBufferPosition(@bufferPositionForScreenPosition(screenPosition), options) + # Public: Constructs a new marker at the given buffer position. + # + # range - The marker {Range} (representing the distance between the head and tail) + # options - Options to pass to the {BufferMarker} constructor + # + # Returns a {Number} representing the new marker's ID. markBufferPosition: (bufferPosition, options) -> @buffer.markPosition(bufferPosition, options) + # Public: Removes the marker with the given id. + # + # id - The {Number} of the ID to remove destroyMarker: (id) -> @buffer.destroyMarker(id) delete @markers[id] + # Public: Gets the screen range of the display marker. + # + # id - The {Number} of the ID to check + # + # Returns a {Range}. getMarkerScreenRange: (id) -> @getMarker(id).getScreenRange() + # Public: Modifies the screen range of the display marker. + # + # id - The {Number} of the ID to change + # screenRange - The new {Range} to use + # options - A hash of options matching those found in {BufferMarker.setRange} setMarkerScreenRange: (id, screenRange, options) -> @getMarker(id).setScreenRange(screenRange, options) + # Public: Gets the buffer range of the display marker. + # + # id - The {Number} of the ID to check + # + # Returns a {Range}. getMarkerBufferRange: (id) -> @getMarker(id).getBufferRange() + # Public: Modifies the buffer range of the display marker. + # + # id - The {Number} of the ID to change + # screenRange - The new {Range} to use + # options - A hash of options matching those found in {BufferMarker.setRange} setMarkerBufferRange: (id, bufferRange, options) -> @getMarker(id).setBufferRange(bufferRange, options) + # Public: Retrieves the screen position of the marker's head. + # + # id - The {Number} of the ID to check + # + # Returns a {Point}. getMarkerScreenPosition: (id) -> @getMarkerHeadScreenPosition(id) + # Public: Retrieves the buffer position of the marker's head. + # + # id - The {Number} of the ID to check + # + # Returns a {Point}. getMarkerBufferPosition: (id) -> @getMarkerHeadBufferPosition(id) + # Public: Retrieves the screen position of the marker's head. + # + # id - The {Number} of the ID to check + # + # Returns a {Point}. getMarkerHeadScreenPosition: (id) -> @getMarker(id).getHeadScreenPosition() + # Public: Sets the screen position of the marker's head. + # + # id - The {Number} of the ID to change + # screenRange - The new {Point} to use + # options - A hash of options matching those found in {DisplayBuffer.bufferPositionForScreenPosition} setMarkerHeadScreenPosition: (id, screenPosition, options) -> @getMarker(id).setHeadScreenPosition(screenPosition, options) + # Public: Retrieves the buffer position of the marker's head. + # + # id - The {Number} of the ID to check + # + # Returns a {Point}. getMarkerHeadBufferPosition: (id) -> @getMarker(id).getHeadBufferPosition() + # Public: Sets the buffer position of the marker's head. + # + # id - The {Number} of the ID to check + # screenRange - The new {Point} to use + # options - A hash of options matching those found in {DisplayBuffer.bufferPositionForScreenPosition} setMarkerHeadBufferPosition: (id, bufferPosition) -> @getMarker(id).setHeadBufferPosition(bufferPosition) - + + # Public: Retrieves the screen position of the marker's tail. + # + # id - The {Number} of the ID to check + # + # Returns a {Point}. getMarkerTailScreenPosition: (id) -> @getMarker(id).getTailScreenPosition() + # Public: Sets the screen position of the marker's tail. + # + # id - The {Number} of the ID to change + # screenRange - The new {Point} to use + # options - A hash of options matching those found in {DisplayBuffer.bufferPositionForScreenPosition} setMarkerTailScreenPosition: (id, screenPosition, options) -> @getMarker(id).setTailScreenPosition(screenPosition, options) + # Public: Retrieves the buffer position of the marker's tail. + # + # id - The {Number} of the ID to check + # + # Returns a {Point}. getMarkerTailBufferPosition: (id) -> @getMarker(id).getTailBufferPosition() + # Public: Sets the buffer position of the marker's tail. + # + # id - The {Number} of the ID to check + # screenRange - The new {Point} to use + # options - A hash of options matching those found in {DisplayBuffer.bufferPositionForScreenPosition} setMarkerTailBufferPosition: (id, bufferPosition) -> @getMarker(id).setTailBufferPosition(bufferPosition) + # Public: 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. + # + # id - A {Number} representing the marker to change + # + # Returns a {Point} representing the new tail position. placeMarkerTail: (id) -> @getMarker(id).placeTail() + # Public: Removes the tail from the marker. + # + # id - A {Number} representing the marker to change clearMarkerTail: (id) -> @getMarker(id).clearTail() + # Public: Identifies 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 {Buffer}. + # + # id - A {Number} representing the marker to check + # + # Returns a {Boolean}. isMarkerReversed: (id) -> @buffer.isMarkerReversed(id) + # Public: Identifies if the marker's head position is equal to its tail. + # + # id - A {Number} representing the marker to check + # + # Returns a {Boolean}. isMarkerRangeEmpty: (id) -> @buffer.isMarkerRangeEmpty(id) + # Public: Sets a callback to be fired whenever a marker is changed. + # + # id - A {Number} representing the marker to watch + # callback - A {Function} to execute observeMarker: (id, callback) -> @getMarker(id).observe(callback) + ### + # Internal # + ### + pauseMarkerObservers: -> marker.pauseEvents() for marker in @getMarkers() diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index e394fddc7..7100c5429 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -10,10 +10,15 @@ Range = require 'range' _ = require 'underscore' fsUtils = require 'fs-utils' +# Public: An `EditSession` manages the states between {Editor}s, {Buffer}s, and the project as a whole. module.exports = class EditSession registerDeserializer(this) + ### + # Internal # + ### + @version: 1 @deserialize: (state) -> @@ -62,20 +67,6 @@ class EditSession getViewClass: -> require 'editor' - getTitle: -> - if path = @getPath() - fsUtils.base(path) - else - 'untitled' - - getLongTitle: -> - if path = @getPath() - fileName = fsUtils.base(path) - directory = fsUtils.base(fsUtils.directory(path)) - "#{fileName} - #{directory}" - else - 'untitled' - destroy: -> return if @destroyed @destroyed = true @@ -96,9 +87,49 @@ class EditSession scrollLeft: @getScrollLeft() cursorScreenPosition: @getCursorScreenPosition().serialize() + # Internal: Creates a copy of the current {EditSession}. + # + # Returns an identical `EditSession`. copy: -> EditSession.deserialize(@serialize(), @project) + ### + # Public # + ### + + # Public: Retrieves the filename of the open file. + # + # This is `'untitled'` if the file is new and not saved to the disk. + # + # Returns a {String}. + getTitle: -> + if path = @getPath() + fsUtils.base(path) + else + 'untitled' + + # Public: Retrieves the filename of the open file, followed by a dash, then the file's directory. + # + # If the file is brand new, the title is `untitled`. + # + # Returns a {String}. + getLongTitle: -> + if path = @getPath() + fileName = fsUtils.base(path) + directory = fsUtils.base(fsUtils.directory(path)) + "#{fileName} - #{directory}" + else + 'untitled' + + # Public: Compares two `EditSession`s to determine equality. + # + # Equality is based on the condition that: + # + # * the two {Buffer}s are the same + # * the two `scrollTop` and `scrollLeft` property are the same + # * the two {Cursor} screen positions are the same + # + # Returns a {Boolean}. isEqual: (other) -> return false unless other instanceof EditSession @buffer == other.buffer and @@ -106,37 +137,107 @@ class EditSession @scrollLeft == other.getScrollLeft() and @getCursorScreenPosition().isEqual(other.getCursorScreenPosition()) + setVisible: (visible) -> @displayBuffer.setVisible(visible) + # Public: Defines the value of the `EditSession`'s `scrollTop` property. + # + # scrollTop - A {Number} defining the `scrollTop`, in pixels. setScrollTop: (@scrollTop) -> + # Public: Gets the value of the `EditSession`'s `scrollTop` property. + # + # Returns a {Number} defining the `scrollTop`, in pixels. getScrollTop: -> @scrollTop + # Public: Defines the value of the `EditSession`'s `scrollLeft` property. + # + # scrollLeft - A {Number} defining the `scrollLeft`, in pixels. setScrollLeft: (@scrollLeft) -> + # Public: Gets the value of the `EditSession`'s `scrollLeft` property. + # + # Returns a {Number} defining the `scrollLeft`, in pixels. getScrollLeft: -> @scrollLeft + # Public: Defines the limit at which the buffer begins to soft wrap text. + # + # softWrapColumn - A {Number} defining the soft wrap limit setSoftWrapColumn: (@softWrapColumn) -> @displayBuffer.setSoftWrapColumn(@softWrapColumn) + # Public: Defines whether to use soft tabs. + # + # softTabs - A {Boolean} which, if `true`, indicates that you want soft tabs. setSoftTabs: (@softTabs) -> - + # Public: Retrieves whether soft tabs are enabled. + # + # Returns a {Boolean}. getSoftWrap: -> @softWrap + # Public: Defines whether to use soft wrapping of text. + # + # softTabs - A {Boolean} which, if `true`, indicates that you want soft wraps. setSoftWrap: (@softWrap) -> + # Public: Retrieves that character used to indicate a tab. + # + # If soft tabs are enabled, this is a space (`" "`) times the {.getTabLength} value. + # Otherwise, it's a tab (`\t`). + # + # Returns a {String}. getTabText: -> @buildIndentString(1) + # Public: Retrieves the current tab length. + # + # Returns a {Number}. getTabLength: -> @displayBuffer.getTabLength() - setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength) + # Public: Specifies the tab length. + # + # tabLength - A {Number} that defines the new tab length. + setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength) + + # Public: Given a position, this clips it to a real position. + # + # For example, if `position`'s row exceeds the row count of the buffer, + # or if its column goes beyond a line's length, this "sanitizes" the value + # to a real position. + # + # position - The {Point} to clip + # + # Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed. clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition) + + # Public: Given a range, this clips it to a real range. + # + # For example, if `range`'s row exceeds the row count of the buffer, + # or if its column goes beyond a line's length, this "sanitizes" the value + # to a real range. + # + # range - The {Point} to clip + # + # Returns the new, clipped {Point}. Note that this could be the same as `range` if no clipping was performed. clipBufferRange: (range) -> @buffer.clipRange(range) + # Public: Given a buffer row, this retrieves the indentation level. + # + # bufferRow - A {Number} indicating the buffer row. + # + # Returns the indentation level as a {Number}. indentationForBufferRow: (bufferRow) -> @indentLevelForLine(@lineForBufferRow(bufferRow)) + # Public: This specifies the new indentation level for a buffer row. + # + # bufferRow - A {Number} indicating the buffer row. + # newLevel - A {Number} indicating the new indentation level. setIndentationForBufferRow: (bufferRow, newLevel) -> currentLevel = @indentationForBufferRow(bufferRow) currentIndentString = @buildIndentString(currentLevel) newIndentString = @buildIndentString(newLevel) @buffer.change([[bufferRow, 0], [bufferRow, currentIndentString.length]], newIndentString) + # Internal: Given a line, this gets the indentation level. + # + # line - A {String} in the current {Buffer}. + # + # Returns a {Number}. indentLevelForLine: (line) -> if match = line.match(/^[\t ]+/) leadingWhitespace = match[0] @@ -146,63 +247,218 @@ class EditSession else 0 + # Internal: Constructs the string used for tabs. buildIndentString: (number) -> if @softTabs _.multiplyString(" ", number * @getTabLength()) else _.multiplyString("\t", Math.floor(number)) + # Public: Saves the buffer. save: -> @buffer.save() + # Public: Saves the buffer at a specific path. + # + # path - The path to save at. saveAs: (path) -> @buffer.saveAs(path) + # Public: Retrieves the current buffer's file extension. + # + # Returns a {String}. getFileExtension: -> @buffer.getExtension() + # Public: Retrieves the current buffer's file path. + # + # Returns a {String}. getPath: -> @buffer.getPath() + # Public: Retrieves the current buffer. + # + # Returns a {String}. getBuffer: -> @buffer + # Public: Retrieves the current buffer's URI. + # + # Returns a {String}. getUri: -> @getPath() + # Public: Given a buffer row, identifies if it is blank. + # + # bufferRow - A buffer row {Number} to check + # + # Returns a {Boolean}. isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow) + # Public: Given a buffer row, this finds the next row that's blank. + # + # bufferRow - A buffer row {Number} to check + # + # Returns a {Number}, or `null` if there's no other blank row. nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow) + # Public: Finds the last point in the current buffer. + # + # Returns a {Point} representing the last position. getEofBufferPosition: -> @buffer.getEofPosition() + # Public: Finds the last line in the current buffer. + # + # Returns a {Number}. getLastBufferRow: -> @buffer.getLastRow() + # Public: Given a buffer row, this retrieves the range for that line. + # + # row - A {Number} identifying the row + # options - A hash with one key, `includeNewline`, which specifies whether you + # want to include the trailing newline + # + # Returns a {Range}. bufferRangeForBufferRow: (row, options) -> @buffer.rangeForRow(row, options) + # Public: Given a buffer row, this retrieves that line. + # + # row - A {Number} identifying the row + # + # Returns a {String}. lineForBufferRow: (row) -> @buffer.lineForRow(row) + # Public: Given a buffer row, this retrieves that line's length. + # + # row - A {Number} identifying the row + # + # Returns a {Number}. lineLengthForBufferRow: (row) -> @buffer.lineLengthForRow(row) + # Public: Scans for text in the buffer, calling a function on each match. + # + # regex - A {RegExp} representing the text to find + # range - A {Range} in the buffer to search within + # iterator - A {Function} that's called on each match scanInBufferRange: (args...) -> @buffer.scanInRange(args...) + # Public: Scans for text in the buffer _backwards_, calling a function on each match. + # + # regex - A {RegExp} representing the text to find + # range - A {Range} in the buffer to search within + # iterator - A {Function} that's called on each match backwardsScanInBufferRange: (args...) -> @buffer.backwardsScanInRange(args...) + # Public: Identifies if the {Buffer} is modified (and not saved). + # + # Returns a {Boolean}. isModified: -> @buffer.isModified() + # Public: Identifies if the modified buffer should let you know if it's closing + # without being saved. + # + # Returns a {Boolean}. shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors() + # Public: Given a buffer position, this converts it into a screen position. + # + # bufferPosition - An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # options - The same options available to {DisplayBuffer.screenPositionForBufferPosition}. + # + # Returns a {Point}. screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, options) + + # Public: Given a buffer range, this converts it into a screen position. + # + # screenPosition - An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # options - The same options available to {DisplayBuffer.bufferPositionForScreenPosition}. + # + # Returns a {Point}. bufferPositionForScreenPosition: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options) - screenRangeForBufferRange: (range) -> @displayBuffer.screenRangeForBufferRange(range) - bufferRangeForScreenRange: (range) -> @displayBuffer.bufferRangeForScreenRange(range) + + # Public: Given a buffer range, this converts it into a screen position. + # + # bufferRange - The {Range} to convert + # + # Returns a {Range}. + screenRangeForBufferRange: (bufferRange) -> @displayBuffer.screenRangeForBufferRange(bufferRange) + + # Public: Given a screen range, this converts it into a buffer position. + # + # screenRange - The {Range} to convert + # + # Returns a {Range}. + bufferRangeForScreenRange: (screenRange) -> @displayBuffer.bufferRangeForScreenRange(screenRange) + # Public: Given a position, this clips it to a real position. + # + # For example, if `position`'s row exceeds the row count of the buffer, + # or if its column goes beyond a line's length, this "sanitizes" the value + # to a real position. + # + # position - The {Point} to clip + # options - A hash with the following values: + # :wrapBeyondNewlines - if `true`, continues wrapping past newlines + # :wrapAtSoftNewlines - if `true`, continues wrapping past soft newlines + # :screenLine - if `true`, indicates that you're using a line number, not a row number + # + # Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed. clipScreenPosition: (screenPosition, options) -> @displayBuffer.clipScreenPosition(screenPosition, options) + # Public: Gets the line for the given screen row. + # + # screenRow - A {Number} indicating the screen row. + # + # Returns a {String}. lineForScreenRow: (row) -> @displayBuffer.lineForRow(row) + # Public: Gets the lines for the given screen row boundaries. + # + # start - A {Number} indicating the beginning screen row. + # end - A {Number} indicating the ending screen row. + # + # Returns an {Array} of {String}s. linesForScreenRows: (start, end) -> @displayBuffer.linesForRows(start, end) + # Public: Gets the number of screen rows. + # + # Returns a {Number}. screenLineCount: -> @displayBuffer.lineCount() + # Public: Gets the length of the longest screen line. + # + # Returns a {Number}. maxScreenLineLength: -> @displayBuffer.maxLineLength() + # Public: Gets the number of the last row in the buffer. + # + # Returns a {Number}. getLastScreenRow: -> @displayBuffer.getLastRow() + # Public: Given a starting and ending row, this converts every row into a buffer position. + # + # startRow - The row {Number} to start at + # endRow - The row {Number} to end at (default: {.getLastScreenRow}) + # + # Returns an {Array} of {Range}s. bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow) + # Public: Retrieves the grammar's token scopes for a buffer position. + # + # bufferPosition - A {Point} in the {Buffer} + # + # Returns an {Array} of {String}s. scopesForBufferPosition: (bufferPosition) -> @displayBuffer.scopesForBufferPosition(bufferPosition) + # Public: Retrieves the grammar's token scopes for the line with the most recently added cursor. + # + # Returns an {Array} of {String}s. getCursorScopes: -> @getCursor().getScopes() + # Internal: logScreenLines: (start, end) -> @displayBuffer.logLines(start, end) + # Public: Determines whether the {Editor} will auto indent rows. + # + # Returns a {Boolean}. shouldAutoIndent: -> config.get("editor.autoIndent") + # Public: Determines whether the {Editor} will auto indent pasted text. + # + # Returns a {Boolean}. shouldAutoIndentPastedText: -> config.get("editor.autoIndentOnPaste") + # Public: Inserts text at the current cursor positions + # + # text - A {String} representing the text to insert. + # options - A set of options equivalent to {Selection.insertText} insertText: (text, options={}) -> options.autoIndent ?= @shouldAutoIndent() @mutateSelectedText (selection) -> selection.insertText(text, options) + # Public: Inserts a new line at the current cursor positions. insertNewline: -> @insertText('\n') + # Public: Inserts a new line below the current cursor positions. insertNewlineBelow: -> @transact => @moveCursorToEndOfLine() @insertNewline() + # Public: Inserts a new line above the current cursor positions. insertNewlineAbove: -> @transact => onFirstLine = @getCursorBufferPosition().row is 0 @@ -211,62 +467,91 @@ class EditSession @insertNewline() @moveCursorUp() if onFirstLine + # Public: Indents the current line. + # + # options - A set of options equivalent to {Selection.indent}. indent: (options={})-> options.autoIndent ?= @shouldAutoIndent() @mutateSelectedText (selection) -> selection.indent(options) + # Public: Performs a backspace, removing the character found behind the cursor position. backspace: -> @mutateSelectedText (selection) -> selection.backspace() + # Public: Performs a backspace to the beginning of the current word, removing characters found there. backspaceToBeginningOfWord: -> @mutateSelectedText (selection) -> selection.backspaceToBeginningOfWord() + # Public: Performs a backspace to the beginning of the current line, removing characters found there. backspaceToBeginningOfLine: -> @mutateSelectedText (selection) -> selection.backspaceToBeginningOfLine() + # Public: Performs a delete, removing the character found ahead of the cursor position. delete: -> @mutateSelectedText (selection) -> selection.delete() + # Public: Performs a delete to the end of the current word, removing characters found there. deleteToEndOfWord: -> @mutateSelectedText (selection) -> selection.deleteToEndOfWord() + # Public: Deletes the entire line. deleteLine: -> @mutateSelectedText (selection) -> selection.deleteLine() + # Public: Indents the selected rows. indentSelectedRows: -> @mutateSelectedText (selection) -> selection.indentSelectedRows() + # Public: Outdents the selected rows. outdentSelectedRows: -> @mutateSelectedText (selection) -> selection.outdentSelectedRows() + # Public: Wraps the lines within a selection in comments. + # + # If the language doesn't have comments, nothing happens. + # + # selection - The {Selection} to comment + # + # Returns an {Array} of the commented {Ranges}. toggleLineCommentsInSelection: -> @mutateSelectedText (selection) -> selection.toggleLineComments() autoIndentSelectedRows: -> @mutateSelectedText (selection) -> selection.autoIndentSelectedRows() + # Given a buffer range, this converts all `\t` characters to the appopriate {.getTabText} value. + # + # bufferRange - The {Range} to perform the replace in normalizeTabsInBufferRange: (bufferRange) -> return unless @softTabs @scanInBufferRange /\t/, bufferRange, ({replace}) => replace(@getTabText()) + # Public: Performs a cut to the end of the current line. + # + # Characters are removed, but the text remains in the clipboard. cutToEndOfLine: -> maintainPasteboard = false @mutateSelectedText (selection) -> selection.cutToEndOfLine(maintainPasteboard) maintainPasteboard = true + # Public: Cuts the selected text. cutSelectedText: -> maintainPasteboard = false @mutateSelectedText (selection) -> selection.cut(maintainPasteboard) maintainPasteboard = true + # Public: Copies the selected text. copySelectedText: -> maintainPasteboard = false for selection in @getSelections() selection.copy(maintainPasteboard) maintainPasteboard = true + # Public: Pastes the text in the clipboard. + # + # options - A set of options equivalent to {Selection.insertText}. pasteText: (options={}) -> options.normalizeIndent ?= true options.autoIndent ?= @shouldAutoIndentPastedText() @@ -276,12 +561,18 @@ class EditSession @insertText(text, options) + # Public: Undos the last {Buffer} change. undo: -> @buffer.undo(this) + # Public: Redos the last {Buffer} change. redo: -> @buffer.redo(this) + ### + # Internal # + ### + transact: (fn) -> isNewTransaction = @buffer.transact() oldSelectedRanges = @getSelectedBufferRanges() @@ -303,78 +594,166 @@ class EditSession abort: -> @buffer.abort() + ### + # Public # + ### + + # Public: Folds all the rows. foldAll: -> @displayBuffer.foldAll() + # Public: Unfolds all the rows. unfoldAll: -> @displayBuffer.unfoldAll() + # Public: Folds the current row. foldCurrentRow: -> bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row @foldBufferRow(bufferRow) + # Public: Given a buffer row, this folds it. + # + # bufferRow - A {Number} indicating the buffer row foldBufferRow: (bufferRow) -> @displayBuffer.foldBufferRow(bufferRow) + # Public: Unfolds the current row. unfoldCurrentRow: -> bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row @unfoldBufferRow(bufferRow) + # Public: Given a buffer row, this unfolds it. + # + # bufferRow - A {Number} indicating the buffer row unfoldBufferRow: (bufferRow) -> @displayBuffer.unfoldBufferRow(bufferRow) + # Public: Folds all selections. foldSelection: -> selection.fold() for selection in @getSelections() + # Public: Creates a new fold between two row numbers. + # + # startRow - The row {Number} to start folding at + # endRow - The row {Number} to end the fold + # + # Returns the new {Fold}. createFold: (startRow, endRow) -> @displayBuffer.createFold(startRow, endRow) + # Public: Removes any {Fold}s found that contain the given buffer row. + # + # bufferRow - The buffer row {Number} to check against destroyFoldsContainingBufferRow: (bufferRow) -> @displayBuffer.destroyFoldsContainingBufferRow(bufferRow) + # Public: Removes any {Fold}s found that intersect the given buffer row. + # + # bufferRow - The buffer row {Number} to check against destroyFoldsIntersectingBufferRange: (bufferRange) -> for row in [bufferRange.start.row..bufferRange.end.row] @destroyFoldsContainingBufferRow(row) + # Public: Given the id of a {Fold}, this removes it. + # + # foldId - The fold id {Number} to remove destroyFold: (foldId) -> fold = @displayBuffer.foldsById[foldId] fold.destroy() @setCursorBufferPosition([fold.startRow, 0]) + # Public: Determines if the given row that the cursor is at is folded. + # + # Returns `true` if the row is folded, `false` otherwise. isFoldedAtCursorRow: -> @isFoldedAtScreenRow(@getCursorScreenRow()) + # Public: Determines if the given buffer row is folded. + # + # bufferRow - A {Number} indicating the buffer row. + # + # Returns `true` if the buffer row is folded, `false` otherwise. isFoldedAtBufferRow: (bufferRow) -> screenRow = @screenPositionForBufferPosition([bufferRow]).row @isFoldedAtScreenRow(screenRow) + # Public: Determines if the given screen row is folded. + # + # screenRow - A {Number} indicating the screen row. + # + # Returns `true` if the screen row is folded, `false` otherwise. isFoldedAtScreenRow: (screenRow) -> @lineForScreenRow(screenRow)?.fold? + # Public: Given a buffer row, this returns the largest fold that includes it. + # + # Largest is defined as the fold whose difference between its start and end points + # are the greatest. + # + # bufferRow - A {Number} indicating the buffer row + # + # Returns a {Fold}. largestFoldContainingBufferRow: (bufferRow) -> @displayBuffer.largestFoldContainingBufferRow(bufferRow) + # Public: Given a screen row, this returns the largest fold that starts there. + # + # Largest is defined as the fold whose difference between its start and end points + # are the greatest. + # + # screenRow - A {Number} indicating the screen row + # + # Returns a {Fold}. largestFoldStartingAtScreenRow: (screenRow) -> @displayBuffer.largestFoldStartingAtScreenRow(screenRow) + # Public: Given a buffer row, this returns a suggested indentation level. + # + # The indentation level provided is based on the current language. + # + # bufferRow - A {Number} indicating the buffer row + # + # Returns a {Number}. suggestedIndentForBufferRow: (bufferRow) -> @languageMode.suggestedIndentForBufferRow(bufferRow) + # Public: Indents all the rows between two buffer rows. + # + # startRow - The row {Number} to start at + # endRow - The row {Number} to end at autoIndentBufferRows: (startRow, endRow) -> @languageMode.autoIndentBufferRows(startRow, endRow) + # Public: Given a buffer row, this indents it. + # + # bufferRow - The row {Number} autoIndentBufferRow: (bufferRow) -> @languageMode.autoIndentBufferRow(bufferRow) + # Public: Given a buffer row, this increases the indentation. + # + # bufferRow - The row {Number} autoIncreaseIndentForBufferRow: (bufferRow) -> @languageMode.autoIncreaseIndentForBufferRow(bufferRow) + # Public: Given a buffer row, this decreases the indentation. + # + # bufferRow - The row {Number} autoDecreaseIndentForRow: (bufferRow) -> @languageMode.autoDecreaseIndentForBufferRow(bufferRow) + # Public: Wraps the lines between two rows in comments. + # + # If the language doesn't have comments, nothing happens. + # + # startRow - The row {Number} to start at + # endRow - The row {Number} to end at + # + # Returns an {Array} of the commented {Ranges}. toggleLineCommentsForBufferRows: (start, end) -> @languageMode.toggleLineCommentsForBufferRows(start, end) + # Public: Moves the selected line up one row. moveLineUp: -> selection = @getSelectedBufferRange() return if selection.start.row is 0 @@ -408,6 +787,7 @@ class EditSession @setSelectedBufferRange(selection.translate([-1]), preserveFolds: true) + # Public: Moves the selected line down one row. moveLineDown: -> selection = @getSelectedBufferRange() lastRow = @buffer.getLastRow() @@ -445,6 +825,9 @@ class EditSession @setSelectedBufferRange(selection.translate([1]), preserveFolds: true) + # Public: Duplicates the current line. + # + # If more than one cursor is present, only the most recently added one is considered. duplicateLine: -> return unless @getSelection().isEmpty() @@ -468,6 +851,11 @@ class EditSession @setCursorScreenPosition(@getCursorScreenPosition().translate([1])) @foldCurrentRow() if cursorRowFolded + + ### + # Internal # + ### + mutateSelectedText: (fn) -> @transact => fn(selection) for selection in @getSelections() @@ -485,106 +873,271 @@ class EditSession pushOperation: (operation) -> @buffer.pushOperation(operation, this) + ### + # Public # + ### + + # Public: Constructs a new marker at the given screen range. + # + # range - The marker {Range} (representing the distance between the head and tail) + # options - Options to pass to the {BufferMarker} constructor + # + # Returns a {Number} representing the new marker's ID. markScreenRange: (args...) -> @displayBuffer.markScreenRange(args...) + # Public: Constructs a new marker at the given buffer range. + # + # range - The marker {Range} (representing the distance between the head and tail) + # options - Options to pass to the {BufferMarker} constructor + # + # Returns a {Number} representing the new marker's ID. markBufferRange: (args...) -> @displayBuffer.markBufferRange(args...) + # Public: Constructs a new marker at the given screen position. + # + # range - The marker {Range} (representing the distance between the head and tail) + # options - Options to pass to the {BufferMarker} constructor + # + # Returns a {Number} representing the new marker's ID. markScreenPosition: (args...) -> @displayBuffer.markScreenPosition(args...) + # Public: Constructs a new marker at the given buffer position. + # + # range - The marker {Range} (representing the distance between the head and tail) + # options - Options to pass to the {BufferMarker} constructor + # + # Returns a {Number} representing the new marker's ID. markBufferPosition: (args...) -> @displayBuffer.markBufferPosition(args...) + # Public: Removes the marker with the given id. + # + # id - The {Number} of the ID to remove destroyMarker: (args...) -> @displayBuffer.destroyMarker(args...) + # Public: Gets the number of markers in the buffer. + # + # Returns a {Number}. getMarkerCount: -> @buffer.getMarkerCount() + # Public: Gets the screen range of the display marker. + # + # id - The {Number} of the ID to check + # + # Returns a {Range}. getMarkerScreenRange: (args...) -> @displayBuffer.getMarkerScreenRange(args...) + # Public: Modifies the screen range of the display marker. + # + # id - The {Number} of the ID to change + # screenRange - The new {Range} to use + # options - A hash of options matching those found in {BufferMarker.setRange} setMarkerScreenRange: (args...) -> @displayBuffer.setMarkerScreenRange(args...) + # Public: Gets the buffer range of the display marker. + # + # id - The {Number} of the ID to check + # + # Returns a {Range}. getMarkerBufferRange: (args...) -> @displayBuffer.getMarkerBufferRange(args...) + # Public: Modifies the buffer range of the display marker. + # + # id - The {Number} of the ID to check + # screenRange - The new {Range} to use + # options - A hash of options matching those found in {BufferMarker.setRange} setMarkerBufferRange: (args...) -> @displayBuffer.setMarkerBufferRange(args...) + # Public: Retrieves the screen position of the marker's head. + # + # id - The {Number} of the ID to check + # + # Returns a {Point}. getMarkerScreenPosition: (args...) -> @displayBuffer.getMarkerScreenPosition(args...) + # Public: Retrieves the buffer position of the marker's head. + # + # id - The {Number} of the ID to check + # + # Returns a {Point}. getMarkerBufferPosition: (args...) -> @displayBuffer.getMarkerBufferPosition(args...) + # Public: Retrieves the screen position of the marker's head. + # + # id - The {Number} of the ID to check + # + # Returns a {Point}. getMarkerHeadScreenPosition: (args...) -> @displayBuffer.getMarkerHeadScreenPosition(args...) - + + # Public: Sets the screen position of the marker's head. + # + # id - The {Number} of the ID to change + # screenRange - The new {Point} to use + # options - A hash of options matching those found in {DisplayBuffer.bufferPositionForScreenPosition} setMarkerHeadScreenPosition: (args...) -> @displayBuffer.setMarkerHeadScreenPosition(args...) + # Public: Retrieves the buffer position of the marker's head. + # + # id - The {Number} of the ID to check + # + # Returns a {Point}. getMarkerHeadBufferPosition: (args...) -> @displayBuffer.getMarkerHeadBufferPosition(args...) + # Public: Sets the buffer position of the marker's head. + # + # id - The {Number} of the ID to check + # screenRange - The new {Point} to use + # options - A hash of options matching those found in {DisplayBuffer.bufferPositionForScreenPosition} setMarkerHeadBufferPosition: (args...) -> @displayBuffer.setMarkerHeadBufferPosition(args...) + # Public: Retrieves the screen position of the marker's tail. + # + # id - The {Number} of the ID to check + # + # Returns a {Point}. getMarkerTailScreenPosition: (args...) -> @displayBuffer.getMarkerTailScreenPosition(args...) + # Public: Sets the screen position of the marker's tail. + # + # id - The {Number} of the ID to change + # screenRange - The new {Point} to use + # options - A hash of options matching those found in {DisplayBuffer.bufferPositionForScreenPosition} setMarkerTailScreenPosition: (args...) -> @displayBuffer.setMarkerTailScreenPosition(args...) - + + # Public: Retrieves the buffer position of the marker's tail. + # + # id - The {Number} of the ID to check + # + # Returns a {Point}. getMarkerTailBufferPosition: (args...) -> @displayBuffer.getMarkerTailBufferPosition(args...) + # Public: Sets the buffer position of the marker's tail. + # + # id - The {Number} of the ID to change + # screenRange - The new {Point} to use + # options - A hash of options matching those found in {DisplayBuffer.bufferPositionForScreenPosition} setMarkerTailBufferPosition: (args...) -> @displayBuffer.setMarkerTailBufferPosition(args...) + # Public: Sets a callback to be fired whenever a marker is changed. + # + # id - A {Number} representing the marker to watch + # callback - A {Function} to execute observeMarker: (args...) -> @displayBuffer.observeMarker(args...) + # Public: 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. + # + # id - A {Number} representing the marker to change + # + # Returns a {Point} representing the new tail position. placeMarkerTail: (args...) -> @displayBuffer.placeMarkerTail(args...) + # Public: Removes the tail from the marker. + # + # id - A {Number} representing the marker to change clearMarkerTail: (args...) -> @displayBuffer.clearMarkerTail(args...) + # Public: Identifies 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 {Buffer}. + # + # id - A {Number} representing the marker to check + # + # Returns a {Boolean}. isMarkerReversed: (args...) -> @displayBuffer.isMarkerReversed(args...) + # Public: Identifies if the marker's head position is equal to its tail. + # + # id - A {Number} representing the marker to check + # + # Returns a {Boolean}. isMarkerRangeEmpty: (args...) -> @displayBuffer.isMarkerRangeEmpty(args...) + # Public: Returns `true` if there are multiple cursors in the edit session. + # + # Returns a {Boolean}. hasMultipleCursors: -> @getCursors().length > 1 + # Public: Retrieves all the cursors. + # + # Returns an {Array} of {Cursor}s. getCursors: -> new Array(@cursors...) + # Public: Retrieves a single cursor + # + # Returns a {Cursor}. getCursor: -> _.last(@cursors) + # Public: Adds a cursor at the provided `screenPosition`. + # + # screenPosition - An {Array} of two numbers: the screen row, and the screen column. + # + # Returns the new {Cursor}. addCursorAtScreenPosition: (screenPosition) -> marker = @markScreenPosition(screenPosition, invalidationStrategy: 'never') @addSelection(marker).cursor + # Public: Adds a cursor at the provided `bufferPosition`. + # + # bufferPosition - An {Array} of two numbers: the buffer row, and the buffer column. + # + # Returns the new {Cursor}. addCursorAtBufferPosition: (bufferPosition) -> marker = @markBufferPosition(bufferPosition, invalidationStrategy: 'never') @addSelection(marker).cursor + # Public: Adds a cursor to the `EditSession`. + # + # marker - The marker where the cursor should be added + # + # Returns the new {Cursor}. addCursor: (marker) -> cursor = new Cursor(editSession: this, marker: marker) @cursors.push(cursor) @trigger 'cursor-added', cursor cursor + # Public: Removes a cursor from the `EditSession`. + # + # cursor - The cursor to remove + # + # Returns the removed {Cursor}. removeCursor: (cursor) -> _.remove(@cursors, cursor) + # Public: Creates a new selection at the given marker. + # + # marker - The marker to highlight + # options - A hash of options that pertain to the {Selection} constructor. + # + # Returns the new {Selection}. addSelection: (marker, options={}) -> unless options.preserveFolds @destroyFoldsIntersectingBufferRange(@getMarkerBufferRange(marker)) @@ -601,14 +1154,28 @@ class EditSession @trigger 'selection-added', selection selection + # Public: Given a buffer range, this adds a new selection for it. + # + # bufferRange - A {Range} in the buffer + # options - A hash of options + # + # Returns the new {Selection}. addSelectionForBufferRange: (bufferRange, options={}) -> options = _.defaults({invalidationStrategy: 'never'}, options) marker = @markBufferRange(bufferRange, options) @addSelection(marker, options) + # Public: Given a buffer range, this removes all previous selections and creates a new selection for it. + # + # bufferRange - A {Range} in the buffer + # options - A hash of options setSelectedBufferRange: (bufferRange, options) -> @setSelectedBufferRanges([bufferRange], options) + # Public: Given an array of buffer ranges, this removes all previous selections and creates new selections for them. + # + # bufferRanges - An {Array} of {Range}s in the buffer + # options - A hash of options setSelectedBufferRanges: (bufferRanges, options={}) -> throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length @@ -623,9 +1190,13 @@ class EditSession @addSelectionForBufferRange(bufferRange, options) @mergeIntersectingSelections(options) + # Public: Unselects a given selection. + # + # selection - The {Selection} to remove. removeSelection: (selection) -> _.remove(@selections, selection) + # Public: Clears every selection. TODO clearSelections: -> @consolidateSelections() @getSelection().clear() @@ -638,145 +1209,248 @@ class EditSession else false + # Public: Gets all the selections. + # + # Returns an {Array} of {Selection}s. getSelections: -> new Array(@selections...) - + + # Public: Gets the selection at the specified index. + # + # index - The id {Number} of the selection + # + # Returns a {Selection}. getSelection: (index) -> index ?= @selections.length - 1 @selections[index] + # Public: Gets the last selection, _i.e._ the most recently added. + # + # Returns a {Selection}. getLastSelection: -> _.last(@selections) + # Public: Gets all selections, ordered by their position in the buffer. + # + # Returns an {Array} of {Selection}s. getSelectionsOrderedByBufferPosition: -> @getSelections().sort (a, b) -> aRange = a.getBufferRange() bRange = b.getBufferRange() aRange.end.compare(bRange.end) + # Public: Gets the very last selection, as it's ordered in the buffer. + # + # Returns a {Selection}. getLastSelectionInBuffer: -> _.last(@getSelectionsOrderedByBufferPosition()) + # Public: Determines if a given buffer range is included in a selection. + # + # bufferRange - The {Range} you're checking against + # + # Returns a {Boolean}. selectionIntersectsBufferRange: (bufferRange) -> _.any @getSelections(), (selection) -> selection.intersectsBufferRange(bufferRange) + # Public: Moves every cursor to a given screen position. + # + # position - An {Array} of two numbers: the screen row, and the screen column. + # options - An object with properties based on {Cursor.setScreenPosition} + # setCursorScreenPosition: (position, options) -> @moveCursors (cursor) -> cursor.setScreenPosition(position, options) + # Public: Gets the current screen position. + # + # Returns an {Array} of two numbers: the screen row, and the screen column. getCursorScreenPosition: -> @getCursor().getScreenPosition() + # Public: Gets the current cursor's screen row. + # + # Returns the screen row. getCursorScreenRow: -> @getCursor().getScreenRow() + # Public: Moves every cursor to a given buffer position. + # + # position - An {Array} of two numbers: the buffer row, and the buffer column. + # options - An object with properties based on {Cursor.setBufferPosition} + # setCursorBufferPosition: (position, options) -> @moveCursors (cursor) -> cursor.setBufferPosition(position, options) + # Public: Gets the current buffer position of the cursor. + # + # Returns an {Array} of two numbers: the buffer row, and the buffer column. getCursorBufferPosition: -> @getCursor().getBufferPosition() + # Public: Gets the screen range of the most recently added {Selection}. + # + # Returns a {Range}. getSelectedScreenRange: -> @getLastSelection().getScreenRange() + # Public: Gets the buffer range of the most recently added {Selection}. + # + # Returns a {Range}. getSelectedBufferRange: -> @getLastSelection().getBufferRange() + # Public: Gets the buffer ranges of all the {Selection}s. + # + # This is ordered by their buffer position. + # + # Returns an {Array} of {Range}s. getSelectedBufferRanges: -> selection.getBufferRange() for selection in @getSelectionsOrderedByBufferPosition() + # Public: Gets the currently selected text. + # + # Returns a {String}. getSelectedText: -> @getLastSelection().getText() + # Public: Given a buffer range, this retrieves the text in that range. + # + # range - The {Range} you're interested in + # + # Returns a {String} of the combined lines. getTextInBufferRange: (range) -> @buffer.getTextInRange(range) + # Public: Retrieves the range for the current paragraph. + # + # A paragraph is defined as a block of text surrounded by empty lines. + # + # Returns a {Range}. getCurrentParagraphBufferRange: -> @getCursor().getCurrentParagraphBufferRange() + # Public: Gets the word located under the cursor. + # + # options - An object with properties based on {Cursor.getBeginningOfCurrentWordBufferPosition}. + # + # Returns a {String}. getWordUnderCursor: (options) -> @getTextInBufferRange(@getCursor().getCurrentWordBufferRange(options)) + # Public: Moves every cursor up one row. moveCursorUp: (lineCount) -> @moveCursors (cursor) -> cursor.moveUp(lineCount) + # Public: Moves every cursor down one row. moveCursorDown: (lineCount) -> @moveCursors (cursor) -> cursor.moveDown(lineCount) + # Public: Moves every cursor left one column. moveCursorLeft: -> @moveCursors (cursor) -> cursor.moveLeft() + # Public: Moves every cursor right one column. moveCursorRight: -> @moveCursors (cursor) -> cursor.moveRight() + # Public: Moves every cursor to the top of the buffer. moveCursorToTop: -> @moveCursors (cursor) -> cursor.moveToTop() + # Public: Moves every cursor to the bottom of the buffer. moveCursorToBottom: -> @moveCursors (cursor) -> cursor.moveToBottom() + # Public: Moves every cursor to the beginning of the line. moveCursorToBeginningOfLine: -> @moveCursors (cursor) -> cursor.moveToBeginningOfLine() + # Public: Moves every cursor to the first non-whitespace character of the line. moveCursorToFirstCharacterOfLine: -> @moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine() + # Public: Moves every cursor to the end of the line. moveCursorToEndOfLine: -> @moveCursors (cursor) -> cursor.moveToEndOfLine() + # Public: Moves every cursor to the beginning of the current word. moveCursorToBeginningOfWord: -> @moveCursors (cursor) -> cursor.moveToBeginningOfWord() + # Public: Moves every cursor to the end of the current word. moveCursorToEndOfWord: -> @moveCursors (cursor) -> cursor.moveToEndOfWord() + # Public: Moves every cursor to the beginning of the next word. moveCursorToBeginningOfNextWord: -> @moveCursors (cursor) -> cursor.moveToBeginningOfNextWord() + # Internal: moveCursors: (fn) -> fn(cursor) for cursor in @getCursors() @mergeCursors() + # 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) -> lastSelection = @getLastSelection() lastSelection.selectToScreenPosition(position) @mergeIntersectingSelections(reverse: lastSelection.isReversed()) + # Public: Selects the text one position right of the cursor. selectRight: -> @expandSelectionsForward (selection) => selection.selectRight() + # Public: Selects the text one position left of the cursor. selectLeft: -> @expandSelectionsBackward (selection) => selection.selectLeft() + # Public: Selects all the text one position above the cursor. selectUp: -> @expandSelectionsBackward (selection) => selection.selectUp() + # Public: Selects all the text one position below the cursor. selectDown: -> @expandSelectionsForward (selection) => selection.selectDown() + # Public: Selects all the text from the current cursor position to the top of the buffer. selectToTop: -> @expandSelectionsBackward (selection) => selection.selectToTop() + # Public: Selects all the text in the buffer. selectAll: -> @expandSelectionsForward (selection) => selection.selectAll() + # Public: Selects all the text from the current cursor position to the bottom of the buffer. selectToBottom: -> @expandSelectionsForward (selection) => selection.selectToBottom() + # Public: Selects all the text from the current cursor position to the beginning of the line. selectToBeginningOfLine: -> @expandSelectionsBackward (selection) => selection.selectToBeginningOfLine() + # Public: Selects all the text from the current cursor position to the end of the line. selectToEndOfLine: -> @expandSelectionsForward (selection) => selection.selectToEndOfLine() + # Public: Selects the current line. selectLine: -> @expandSelectionsForward (selection) => selection.selectLine() + # Public: Moves the current selection down one row. addSelectionBelow: -> @expandSelectionsForward (selection) => selection.addSelectionBelow() + # Public: Moves the current selection up one row. addSelectionAbove: -> @expandSelectionsBackward (selection) => selection.addSelectionAbove() + # Public: Transposes the current text selections. + # + # This only works if there is more than one selection. Each selection is transferred + # to the position of the selection after it. The last selection is transferred to the + # position of the first. transpose: -> @mutateSelectedText (selection) => if selection.isEmpty() @@ -788,27 +1462,37 @@ class EditSession else selection.insertText selection.getText().split('').reverse().join('') + # Public: Turns the current selection into upper case. upperCase: -> @replaceSelectedText selectWordIfEmpty:true, (text) => text.toUpperCase() + # Public: Turns the current selection into lower case. lowerCase: -> @replaceSelectedText selectWordIfEmpty:true, (text) => text.toLowerCase() + # Public: Joins the current line with the one below it. + # + # Multiple cursors are considered equally. If there's a selection in the editor, + # all the lines are joined together. joinLine: -> @mutateSelectedText (selection) -> selection.joinLine() expandLastSelectionOverLine: -> @getLastSelection().expandOverLine() + # Public: Selects all the text from the current cursor position to the beginning of the word. selectToBeginningOfWord: -> @expandSelectionsBackward (selection) => selection.selectToBeginningOfWord() + # Public: Selects all the text from the current cursor position to the end of the word. selectToEndOfWord: -> @expandSelectionsForward (selection) => selection.selectToEndOfWord() + # Public: Selects all the text from the current cursor position to the beginning of the next word. selectToBeginningOfNextWord: -> @expandSelectionsForward (selection) => selection.selectToBeginningOfNextWord() - + + # Public: Selects the current word. selectWord: -> @expandSelectionsForward (selection) => selection.selectWord() @@ -822,6 +1506,11 @@ class EditSession else false + # Public: Given a buffer position, this finds all markers that contain the position. + # + # bufferPosition - A {Point} to check + # + # Returns an {Array} of {Numbers}, representing marker IDs containing `bufferPosition`. markersForBufferPosition: (bufferPosition) -> @buffer.markersForPosition(bufferPosition) @@ -855,6 +1544,7 @@ class EditSession @mergeIntersectingSelections(options) return + # Internal: inspect: -> JSON.stringify @serialize() @@ -866,18 +1556,27 @@ class EditSession @setCursorBufferPosition(cursorPosition) if cursorPosition cursorPosition = null + # Public: Retrieves the current {EditSession}'s grammar. + # + # Returns a {String} indicating the language's grammar rules. getGrammar: -> @languageMode.grammar + # Public: Sets the current {EditSession}'s grammar. + # + # grammar - A {String} indicating the language's grammar rules. setGrammar: (grammar) -> @languageMode.setGrammar(grammar) + # Public: Reloads the current grammar. reloadGrammar: -> @languageMode.reloadGrammar() + # Internal: handleGrammarChange: -> @unfoldAll() @trigger 'grammar-changed' + # Internal: getDebugSnapshot: -> [ @displayBuffer.getDebugSnapshot() diff --git a/src/app/editor.coffee b/src/app/editor.coffee index 09c1829d7..202f817cf 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -10,6 +10,9 @@ fsUtils = require 'fs-utils' $ = require 'jquery' _ = require 'underscore' +# Public: Represents the entire visual pane in Atom. +# +# The Editor manages the {EditSession}, which manages the file buffers. module.exports = class Editor extends View @configDefaults: @@ -23,6 +26,10 @@ class Editor extends View @nextEditorId: 1 + ### + # Internal # + ### + @content: (params) -> @div class: @classes(params), tabindex: -1, => @subview 'gutter', new Gutter @@ -56,6 +63,13 @@ class Editor extends View newSelections: null redrawOnReattach: false + # Public: The constructor for setting up an `Editor` instance. + # + # editSessionOrOptions - Either an {EditSession}, or an object with one property, `mini`. + # If `mini` is `true`, a "miniature" `EditSession` is constructed. + # Typically, this is ideal for scenarios where you need an Atom editor, + # but without all the chrome, like scrollbars, gutter, _e.t.c._. + # initialize: (editSessionOrOptions) -> if editSessionOrOptions instanceof EditSession editSession = editSessionOrOptions @@ -87,6 +101,9 @@ class Editor extends View else throw new Error("Must supply an EditSession or mini: true") + # Internal: Sets up the core Atom commands. + # + # Some commands are excluded from mini-editors. bindKeys: -> editorBindings = 'core:move-left': @moveCursorLeft @@ -171,139 +188,418 @@ class Editor extends View do (name, method) => @command name, (e) => method.call(this, e); false - getCursor: -> @activeEditSession.getCursor() - getCursors: -> @activeEditSession.getCursors() - addCursorAtScreenPosition: (screenPosition) -> @activeEditSession.addCursorAtScreenPosition(screenPosition) - addCursorAtBufferPosition: (bufferPosition) -> @activeEditSession.addCursorAtBufferPosition(bufferPosition) - moveCursorUp: -> @activeEditSession.moveCursorUp() - moveCursorDown: -> @activeEditSession.moveCursorDown() - moveCursorLeft: -> @activeEditSession.moveCursorLeft() - moveCursorRight: -> @activeEditSession.moveCursorRight() - moveCursorToBeginningOfWord: -> @activeEditSession.moveCursorToBeginningOfWord() - moveCursorToEndOfWord: -> @activeEditSession.moveCursorToEndOfWord() - moveCursorToBeginningOfNextWord: -> @activeEditSession.moveCursorToBeginningOfNextWord() - moveCursorToTop: -> @activeEditSession.moveCursorToTop() - moveCursorToBottom: -> @activeEditSession.moveCursorToBottom() - moveCursorToBeginningOfLine: -> @activeEditSession.moveCursorToBeginningOfLine() - moveCursorToFirstCharacterOfLine: -> @activeEditSession.moveCursorToFirstCharacterOfLine() - moveCursorToEndOfLine: -> @activeEditSession.moveCursorToEndOfLine() - moveLineUp: -> @activeEditSession.moveLineUp() - moveLineDown: -> @activeEditSession.moveLineDown() - setCursorScreenPosition: (position, options) -> @activeEditSession.setCursorScreenPosition(position, options) - duplicateLine: -> @activeEditSession.duplicateLine() - joinLine: -> @activeEditSession.joinLine() - getCursorScreenPosition: -> @activeEditSession.getCursorScreenPosition() - getCursorScreenRow: -> @activeEditSession.getCursorScreenRow() - setCursorBufferPosition: (position, options) -> @activeEditSession.setCursorBufferPosition(position, options) - getCursorBufferPosition: -> @activeEditSession.getCursorBufferPosition() - getCurrentParagraphBufferRange: -> @activeEditSession.getCurrentParagraphBufferRange() - getWordUnderCursor: (options) -> @activeEditSession.getWordUnderCursor(options) + ### + # Public # + ### + # Public: Retrieves a single cursor + # + # Returns a {Cursor}. + getCursor: -> @activeEditSession.getCursor() + # Public: Retrieves an array of all the cursors. + # + # Returns a {[Cursor]}. + getCursors: -> @activeEditSession.getCursors() + # Public: Adds a cursor at the provided `screenPosition`. + # + # screenPosition - An {Array} of two numbers: the screen row, and the screen column. + # + # Returns the new {Cursor}. + addCursorAtScreenPosition: (screenPosition) -> @activeEditSession.addCursorAtScreenPosition(screenPosition) + # Public: Adds a cursor at the provided `bufferPosition`. + # + # bufferPosition - An {Array} of two numbers: the buffer row, and the buffer column. + # + # Returns the new {Cursor}. + addCursorAtBufferPosition: (bufferPosition) -> @activeEditSession.addCursorAtBufferPosition(bufferPosition) + # Public: Moves every cursor up one row. + moveCursorUp: -> @activeEditSession.moveCursorUp() + # Public: Moves every cursor down one row. + moveCursorDown: -> @activeEditSession.moveCursorDown() + # Public: Moves every cursor left one column. + moveCursorLeft: -> @activeEditSession.moveCursorLeft() + # Public: Moves every cursor right one column. + moveCursorRight: -> @activeEditSession.moveCursorRight() + # Public: Moves every cursor to the beginning of the current word. + moveCursorToBeginningOfWord: -> @activeEditSession.moveCursorToBeginningOfWord() + # Public: Moves every cursor to the end of the current word. + moveCursorToEndOfWord: -> @activeEditSession.moveCursorToEndOfWord() + # Public: Moves the cursor to the beginning of the next word. + moveCursorToBeginningOfNextWord: -> @activeEditSession.moveCursorToBeginningOfNextWord() + # Public: Moves every cursor to the top of the buffer. + moveCursorToTop: -> @activeEditSession.moveCursorToTop() + # Public: Moves every cursor to the bottom of the buffer. + moveCursorToBottom: -> @activeEditSession.moveCursorToBottom() + # Public: Moves every cursor to the beginning of the line. + moveCursorToBeginningOfLine: -> @activeEditSession.moveCursorToBeginningOfLine() + # Public: Moves every cursor to the first non-whitespace character of the line. + moveCursorToFirstCharacterOfLine: -> @activeEditSession.moveCursorToFirstCharacterOfLine() + # Public: Moves every cursor to the end of the line. + moveCursorToEndOfLine: -> @activeEditSession.moveCursorToEndOfLine() + # Public: Moves the selected line up one row. + moveLineUp: -> @activeEditSession.moveLineUp() + # Public: Moves the selected line down one row. + moveLineDown: -> @activeEditSession.moveLineDown() + # Public: Sets the cursor based on a given screen position. + # + # position - An {Array} of two numbers: the screen row, and the screen column. + # options - An object with properties based on {Cursor.setScreenPosition}. + # + setCursorScreenPosition: (position, options) -> @activeEditSession.setCursorScreenPosition(position, options) + # Public: Duplicates the current line. + # + # If more than one cursor is present, only the most recently added one is considered. + duplicateLine: -> @activeEditSession.duplicateLine() + # Public: Joins the current line with the one below it. + # + # Multiple cursors are considered equally. If there's a selection in the editor, + # all the lines are joined together. + joinLine: -> @activeEditSession.joinLine() + # Public: Gets the current screen position. + # + # Returns an {Array} of two numbers: the screen row, and the screen column. + getCursorScreenPosition: -> @activeEditSession.getCursorScreenPosition() + # Public: Gets the current screen row. + # + # Returns a {Number}. + getCursorScreenRow: -> @activeEditSession.getCursorScreenRow() + # Public: Sets the cursor based on a given buffer position. + # + # position - An {Array} of two numbers: the buffer row, and the buffer column. + # options - An object with properties based on {Cursor.setBufferPosition}. + # + setCursorBufferPosition: (position, options) -> @activeEditSession.setCursorBufferPosition(position, options) + # Public: Gets the current buffer position of the cursor. + # + # Returns an {Array} of two numbers: the buffer row, and the buffer column. + getCursorBufferPosition: -> @activeEditSession.getCursorBufferPosition() + # Public: Retrieves the range for the current paragraph. + # + # A paragraph is defined as a block of text surrounded by empty lines. + # + # Returns a {Range}. + getCurrentParagraphBufferRange: -> @activeEditSession.getCurrentParagraphBufferRange() + # Public: Gets the word located under the cursor. + # + # options - An object with properties based on {Cursor.getBeginningOfCurrentWordBufferPosition}. + # + # Returns a {String}. + getWordUnderCursor: (options) -> @activeEditSession.getWordUnderCursor(options) + # Public: Gets the selection at the specified index. + # + # index - The id {Number} of the selection + # + # Returns a {Selection}. getSelection: (index) -> @activeEditSession.getSelection(index) + # Public: Gets the last selection, _i.e._ the most recently added. + # + # Returns a {Selection}. getSelections: -> @activeEditSession.getSelections() + # Public: Gets all selections, ordered by their position in the buffer. + # + # Returns an {Array} of {Selection}s. getSelectionsOrderedByBufferPosition: -> @activeEditSession.getSelectionsOrderedByBufferPosition() + # Public: Gets the very last selection, as it's ordered in the buffer. + # + # Returns a {Selection}. getLastSelectionInBuffer: -> @activeEditSession.getLastSelectionInBuffer() + # Public: Gets the currently selected text. + # + # Returns a {String}. getSelectedText: -> @activeEditSession.getSelectedText() + # Public: Gets the buffer ranges of all the {Selection}s. + # + # This is ordered by their buffer position. + # + # Returns an {Array} of {Range}s. getSelectedBufferRanges: -> @activeEditSession.getSelectedBufferRanges() + # Public: Gets the buffer range of the most recently added {Selection}. + # + # Returns a {Range}. getSelectedBufferRange: -> @activeEditSession.getSelectedBufferRange() - setSelectedBufferRange: (bufferRange, options) -> @activeEditSession.setSelectedBufferRange(bufferRange, options) + # Public: Given a buffer range, this removes all previous selections and creates a new selection for it. + # + # bufferRange - A {Range} in the buffer + # options - A hash of options + setSelectedBufferRange: (bufferRange, options) -> @activeEditSession.setSelectedBufferRange(bufferRange, options) + # Public: Given an array of buffer ranges, this removes all previous selections and creates new selections for them. + # + # bufferRanges - An {Array} of {Range}s in the buffer + # options - A hash of options setSelectedBufferRanges: (bufferRanges, options) -> @activeEditSession.setSelectedBufferRanges(bufferRanges, options) + # Public: Given a buffer range, this adds a new selection for it. + # + # bufferRange - A {Range} in the buffer + # options - A hash of options + # + # Returns the new {Selection}. addSelectionForBufferRange: (bufferRange, options) -> @activeEditSession.addSelectionForBufferRange(bufferRange, options) + # Public: Selects the text one position right of the cursor. selectRight: -> @activeEditSession.selectRight() + # Public: Selects the text one position left of the cursor. selectLeft: -> @activeEditSession.selectLeft() + # Public: Selects all the text one position above the cursor. selectUp: -> @activeEditSession.selectUp() + # Public: Selects all the text one position below the cursor. selectDown: -> @activeEditSession.selectDown() + # Public: Selects all the text from the current cursor position to the top of the buffer. selectToTop: -> @activeEditSession.selectToTop() + # Public: Selects all the text from the current cursor position to the bottom of the buffer. selectToBottom: -> @activeEditSession.selectToBottom() + # Public: Selects all the text in the buffer. selectAll: -> @activeEditSession.selectAll() + # Public: Selects all the text from the current cursor position to the beginning of the line. selectToBeginningOfLine: -> @activeEditSession.selectToBeginningOfLine() + # Public: Selects all the text from the current cursor position to the end of the line. selectToEndOfLine: -> @activeEditSession.selectToEndOfLine() + # Public: Moves the current selection down one row. addSelectionBelow: -> @activeEditSession.addSelectionBelow() + # Public: Moves the current selection up one row. addSelectionAbove: -> @activeEditSession.addSelectionAbove() + # Public: Selects all the text from the current cursor position to the beginning of the word. selectToBeginningOfWord: -> @activeEditSession.selectToBeginningOfWord() + # Public: Selects all the text from the current cursor position to the end of the word. selectToEndOfWord: -> @activeEditSession.selectToEndOfWord() + # Public: Selects all the text from the current cursor position to the beginning of the next word. selectToBeginningOfNextWord: -> @activeEditSession.selectToBeginningOfNextWord() + # Public: Selects the current word. selectWord: -> @activeEditSession.selectWord() + # Public: Selects the current line. selectLine: -> @activeEditSession.selectLine() + # Public: Selects the text from the current cursor position to a given position. + # + # position - An instance of {Point}, with a given `row` and `column`. selectToScreenPosition: (position) -> @activeEditSession.selectToScreenPosition(position) + # Public: Transposes the current text selections. + # + # This only works if there is more than one selection. Each selection is transferred + # to the position of the selection after it. The last selection is transferred to the + # position of the first. transpose: -> @activeEditSession.transpose() + # Public: Turns the current selection into upper case. upperCase: -> @activeEditSession.upperCase() + # Public: Turns the current selection into lower case. lowerCase: -> @activeEditSession.lowerCase() + # Public: Clears every selection. TODO clearSelections: -> @activeEditSession.clearSelections() + # Public: Performs a backspace, removing the character found behind the cursor position. backspace: -> @activeEditSession.backspace() + # Public: Performs a backspace to the beginning of the current word, removing characters found there. backspaceToBeginningOfWord: -> @activeEditSession.backspaceToBeginningOfWord() + # Public: Performs a backspace to the beginning of the current line, removing characters found there. backspaceToBeginningOfLine: -> @activeEditSession.backspaceToBeginningOfLine() + # Public: Performs a delete, removing the character found ahead the cursor position. delete: -> @activeEditSession.delete() + # Public: Performs a delete to the end of the current word, removing characters found there. deleteToEndOfWord: -> @activeEditSession.deleteToEndOfWord() + # Public: Performs a delete to the end of the current line, removing characters found there. deleteLine: -> @activeEditSession.deleteLine() + # Public: Performs a cut to the end of the current line. + # + # Characters are removed, but the text remains in the clipboard. cutToEndOfLine: -> @activeEditSession.cutToEndOfLine() + # Public: Inserts text at the current cursor positions. + # + # text - A {String} representing the text to insert. + # options - A set of options equivalent to {Selection.insertText}. insertText: (text, options) -> @activeEditSession.insertText(text, options) + # Public: Inserts a new line at the current cursor positions. insertNewline: -> @activeEditSession.insertNewline() + # Internal: consolidateSelections: (e) -> e.abortKeyBinding() unless @activeEditSession.consolidateSelections() + # Public: Inserts a new line below the current cursor positions. insertNewlineBelow: -> @activeEditSession.insertNewlineBelow() + # Public: Inserts a new line above the current cursor positions. insertNewlineAbove: -> @activeEditSession.insertNewlineAbove() + # Public: Indents the current line. + # + # options - A set of options equivalent to {Selection.indent}. indent: (options) -> @activeEditSession.indent(options) - autoIndent: (options) -> @activeEditSession.autoIndentSelectedRows(options) + # Public: TODO + autoIndent: (options) -> @activeEditSession.autoIndentSelectedRows() + # Public: Indents the selected rows. indentSelectedRows: -> @activeEditSession.indentSelectedRows() + # Public: Outdents the selected rows. outdentSelectedRows: -> @activeEditSession.outdentSelectedRows() + # Public: Cuts the selected text. cutSelection: -> @activeEditSession.cutSelectedText() + # Public: Copies the selected text. copySelection: -> @activeEditSession.copySelectedText() - paste: -> @activeEditSession.pasteText() + # Public: Pastes the text in the clipboard. + # + # options - A set of options equivalent to {Selection.insertText}. + paste: (options) -> @activeEditSession.pasteText(options) + # Public: Undos the last {Buffer} change. undo: -> @activeEditSession.undo() + # Public: Redos the last {Buffer} change. redo: -> @activeEditSession.redo() - transact: (fn) -> @activeEditSession.transact(fn) - commit: -> @activeEditSession.commit() - abort: -> @activeEditSession.abort() + # Public: Creates a new fold between two row numbers. + # + # startRow - The row {Number} to start folding at + # endRow - The row {Number} to end the fold + # + # Returns the new {Fold}. createFold: (startRow, endRow) -> @activeEditSession.createFold(startRow, endRow) + # Public: Folds the current row. foldCurrentRow: -> @activeEditSession.foldCurrentRow() + # Public: Unfolds the current row. unfoldCurrentRow: -> @activeEditSession.unfoldCurrentRow() + # Public: Folds all the rows. foldAll: -> @activeEditSession.foldAll() + # Public: Unfolds all the rows. unfoldAll: -> @activeEditSession.unfoldAll() + # Public: Folds the most recent selection. foldSelection: -> @activeEditSession.foldSelection() + # Public: Given the id of a {Fold}, this removes it. + # + # foldId - The fold id {Number} to remove destroyFold: (foldId) -> @activeEditSession.destroyFold(foldId) + # Public: Removes any {Fold}s found that contain the given buffer row. + # + # bufferRow - The buffer row {Number} to check against destroyFoldsContainingBufferRow: (bufferRow) -> @activeEditSession.destroyFoldsContainingBufferRow(bufferRow) + # Public: Determines if the given screen row is folded. + # + # screenRow - A {Number} indicating the screen row. + # + # Returns `true` if the screen row is folded, `false` otherwise. isFoldedAtScreenRow: (screenRow) -> @activeEditSession.isFoldedAtScreenRow(screenRow) + # Public: Determines if the given buffer row is folded. + # + # screenRow - A {Number} indicating the buffer row. + # + # Returns `true` if the buffer row is folded, `false` otherwise. isFoldedAtBufferRow: (bufferRow) -> @activeEditSession.isFoldedAtBufferRow(bufferRow) + # Public: Determines if the given row that the cursor is at is folded. + # + # Returns `true` if the row is folded, `false` otherwise. isFoldedAtCursorRow: -> @activeEditSession.isFoldedAtCursorRow() + # Public: Gets the line for the given screen row. + # + # screenRow - A {Number} indicating the screen row. + # + # Returns a {String}. lineForScreenRow: (screenRow) -> @activeEditSession.lineForScreenRow(screenRow) + # Public: Gets the lines for the given screen row boundaries. + # + # start - A {Number} indicating the beginning screen row. + # end - A {Number} indicating the ending screen row. + # + # Returns an {Array} of {String}s. linesForScreenRows: (start, end) -> @activeEditSession.linesForScreenRows(start, end) + # Public: Gets the number of screen rows. + # + # Returns a {Number}. screenLineCount: -> @activeEditSession.screenLineCount() + # Public: Defines the limit at which the buffer begins to soft wrap text. + # + # softWrapColumn - A {Number} defining the soft wrap limit setSoftWrapColumn: (softWrapColumn) -> softWrapColumn ?= @calcSoftWrapColumn() @activeEditSession.setSoftWrapColumn(softWrapColumn) if softWrapColumn - + # Public: Gets the length of the longest screen line. + # + # Returns a {Number}. maxScreenLineLength: -> @activeEditSession.maxScreenLineLength() + # Public: Gets the text in the last screen row. + # + # Returns a {String}. getLastScreenRow: -> @activeEditSession.getLastScreenRow() + # Public: Given a position, this clips it to a real position. + # + # For example, if `position`'s row exceeds the row count of the buffer, + # or if its column goes beyond a line's length, this "sanitizes" the value + # to a real position. + # + # position - The {Point} to clip + # options - A hash with the following values: + # :wrapBeyondNewlines - if `true`, continues wrapping past newlines + # :wrapAtSoftNewlines - if `true`, continues wrapping past soft newlines + # :screenLine - if `true`, indicates that you're using a line number, not a row number + # + # Returns the new, clipped {Point}. Note that this could be the same as `position` if no clipping was performed. clipScreenPosition: (screenPosition, options={}) -> @activeEditSession.clipScreenPosition(screenPosition, options) - screenPositionForBufferPosition: (position, options) -> @activeEditSession.screenPositionForBufferPosition(position, options) - bufferPositionForScreenPosition: (position, options) -> @activeEditSession.bufferPositionForScreenPosition(position, options) - screenRangeForBufferRange: (range) -> @activeEditSession.screenRangeForBufferRange(range) - bufferRangeForScreenRange: (range) -> @activeEditSession.bufferRangeForScreenRange(range) - bufferRowsForScreenRows: (startRow, endRow) -> @activeEditSession.bufferRowsForScreenRows(startRow, endRow) - getLastScreenRow: -> @activeEditSession.getLastScreenRow() + # Public: Given a buffer position, this converts it into a screen position. + # + # bufferPosition - An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # options - The same options available to {DisplayBuffer.screenPositionForBufferPosition}. + # + # Returns a {Point}. + screenPositionForBufferPosition: (position, options) -> @activeEditSession.screenPositionForBufferPosition(position, options) + + # Public: Given a buffer range, this converts it into a screen position. + # + # screenPosition - An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # options - The same options available to {DisplayBuffer.bufferPositionForScreenPosition}. + # + # Returns a {Point}. + bufferPositionForScreenPosition: (position, options) -> @activeEditSession.bufferPositionForScreenPosition(position, options) + + # Public: Given a buffer range, this converts it into a screen position. + # + # bufferRange - The {Range} to convert + # + # Returns a {Range}. + screenRangeForBufferRange: (range) -> @activeEditSession.screenRangeForBufferRange(range) + + # Public: Given a screen range, this converts it into a buffer position. + # + # screenRange - The {Range} to convert + # + # Returns a {Range}. + bufferRangeForScreenRange: (range) -> @activeEditSession.bufferRangeForScreenRange(range) + # Public: Given a starting and ending row, this converts every row into a buffer position. + # + # startRow - The row {Number} to start at + # endRow - The row {Number} to end at (default: {.getLastScreenRow}) + # + # Returns an {Array} of {Range}s. + bufferRowsForScreenRows: (startRow, endRow) -> @activeEditSession.bufferRowsForScreenRows(startRow, endRow) + # Public: Gets the number of the last row in the buffer. + # + # Returns a {Number}. + getLastScreenRow: -> @activeEditSession.getLastScreenRow() + # Internal: logCursorScope: -> console.log @activeEditSession.getCursorScopes() - + # Public: Emulates the "page down" key, where the last row of a buffer scrolls to become the first. pageDown: -> newScrollTop = @scrollTop() + @scrollView[0].clientHeight @activeEditSession.moveCursorDown(@getPageRows()) @scrollTop(newScrollTop, adjustVerticalScrollbar: true) + + # Public: Emulates the "page up" key, where the frst row of a buffer scrolls to become the last. pageUp: -> newScrollTop = @scrollTop() - @scrollView[0].clientHeight @activeEditSession.moveCursorUp(@getPageRows()) @scrollTop(newScrollTop, adjustVerticalScrollbar: true) + # Public: Gets the number of actual page rows existing in an editor. + # + # Returns a {Number}. getPageRows: -> Math.max(1, Math.ceil(@scrollView[0].clientHeight / @lineHeight)) + # Public: Set whether invisible characters are shown. + # + # showInvisibles - A {Boolean} which, if `true`, show invisible characters setShowInvisibles: (showInvisibles) -> return if showInvisibles == @showInvisibles @showInvisibles = showInvisibles @resetDisplay() + # Public: Defines which characters are invisible. + # + # invisibles - A hash defining the invisible characters: The defaults are: + # :eol - `\u00ac` + # :space - `\u00b7` + # :tab - `\u00bb` + # :cr - `\u00a4` setInvisibles: (@invisibles={}) -> _.defaults @invisibles, eol: '\u00ac' @@ -312,25 +608,80 @@ class Editor extends View cr: '\u00a4' @resetDisplay() + # Public: Sets whether you want to show the indentation guides. + # + # showIndentGuide - A {Boolean} you can set to `true` if you want to see the indentation guides. setShowIndentGuide: (showIndentGuide) -> return if showIndentGuide == @showIndentGuide @showIndentGuide = showIndentGuide @resetDisplay() + # Public: Checks out the current HEAD revision of the file. checkoutHead: -> @getBuffer().checkoutHead() + # Public: Replaces the current buffer contents. + # + # text - A {String} containing the new buffer contents. setText: (text) -> @getBuffer().setText(text) + # Public: Retrieves the current buffer contents. + # + # Returns a {String}. getText: -> @getBuffer().getText() + # Public: Retrieves the current buffer's file path. + # + # Returns a {String}. getPath: -> @activeEditSession?.getPath() + # Public: Gets the number of lines in a file. + # + # Returns a {Number}. getLineCount: -> @getBuffer().getLineCount() + # Public: Gets the row number of the last line. + # + # Returns a {Number}. getLastBufferRow: -> @getBuffer().getLastRow() + # Public: Given a range, returns the lines of text within it. + # + # range - A {Range} object specifying your points of interest + # + # Returns a {String} of the combined lines. getTextInRange: (range) -> @getBuffer().getTextInRange(range) + # Public: Finds the last point in the current buffer. + # + # Returns a {Point} representing the last position. getEofPosition: -> @getBuffer().getEofPosition() + # Public: Given a row, returns the line of text. + # + # row - A {Number} indicating the row. + # + # Returns a {String}. lineForBufferRow: (row) -> @getBuffer().lineForRow(row) + # Public: Given a row, returns the length of the line of text. + # + # row - A {Number} indicating the row + # + # Returns a {Number}. lineLengthForBufferRow: (row) -> @getBuffer().lineLengthForRow(row) + # Public: Given a buffer row, this retrieves the range for that line. + # + # row - A {Number} identifying the row + # options - A hash with one key, `includeNewline`, which specifies whether you + # want to include the trailing newline + # + # Returns a {Range}. rangeForBufferRow: (row) -> @getBuffer().rangeForRow(row) + # Public: Scans for text in the buffer, calling a function on each match. + # + # regex - A {RegExp} representing the text to find + # range - A {Range} in the buffer to search within + # iterator - A {Function} that's called on each match scanInBufferRange: (args...) -> @getBuffer().scanInRange(args...) + # Public: Scans for text in the buffer _backwards_, calling a function on each match. + # + # regex - A {RegExp} representing the text to find + # range - A {Range} in the buffer to search within + # iterator - A {Function} that's called on each match backwardsScanInBufferRange: (args...) -> @getBuffer().backwardsScanInRange(args...) + # Internal: configure: -> @observeConfig 'editor.showLineNumbers', (showLineNumbers) => @gutter.setShowLineNumbers(showLineNumbers) @observeConfig 'editor.showInvisibles', (showInvisibles) => @setShowInvisibles(showInvisibles) @@ -339,6 +690,7 @@ class Editor extends View @observeConfig 'editor.fontSize', (fontSize) => @setFontSize(fontSize) @observeConfig 'editor.fontFamily', (fontFamily) => @setFontFamily(fontFamily) + # Internal: Responsible for handling events made to the editor. handleEvents: -> @on 'focus', => @hiddenInput.focus() @@ -410,6 +762,7 @@ class Editor extends View else @gutter.addClass('drop-shadow') + # Internal: selectOnMousemoveUntilMouseup: -> lastMoveEvent = null moveHandler = (event = lastMoveEvent) => @@ -428,6 +781,7 @@ class Editor extends View @activeEditSession.finalizeSelections() @syncCursorAnimations() + # Internal: afterAttach: (onDom) -> return unless onDom @redraw() if @redrawOnReattach @@ -449,6 +803,7 @@ class Editor extends View @trigger 'editor:attached', [this] + # Internal: edit: (editSession) -> return if editSession is @activeEditSession @@ -478,14 +833,24 @@ class Editor extends View if @attached and @activeEditSession.buffer.isInConflict() _.defer => @showBufferConflictAlert(@activeEditSession) # Display after editSession has a chance to display + # Internal: Retrieves the currently active session. + # + # Returns an {EditSession}. getModel: -> @activeEditSession + # Internal: Set the new active session. + # + # editSession - The new {EditSession} to use. setModel: (editSession) -> @edit(editSession) + # Public: Retrieves the {EditSession}'s buffer. + # + # Returns the current {Buffer}. getBuffer: -> @activeEditSession.buffer + # Internal: showBufferConflictAlert: (editSession) -> atom.confirm( editSession.getPath(), @@ -494,6 +859,7 @@ class Editor extends View "Cancel" ) + # Internal: scrollTop: (scrollTop, options={}) -> return @cachedScrollTop or 0 unless scrollTop? maxScrollTop = @verticalScrollbar.prop('scrollHeight') - @verticalScrollbar.height() @@ -510,29 +876,51 @@ class Editor extends View if options?.adjustVerticalScrollbar ? true @verticalScrollbar.scrollTop(scrollTop) + # Internal: scrollBottom: (scrollBottom) -> if scrollBottom? @scrollTop(scrollBottom - @scrollView.height()) else @scrollTop() + @scrollView.height() + # Public: Scrolls the editor to the bottom. scrollToBottom: -> @scrollBottom(@screenLineCount() * @lineHeight) + # Public: Scrolls the editor to the position of the most recently added cursor. + # + # The editor is also centered. scrollToCursorPosition: -> @scrollToBufferPosition(@getCursorBufferPosition(), center: true) + # Public: Scrolls the editor to the given buffer position. + # + # bufferPosition - An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # options - A hash matching the options available to {.scrollToPixelPosition} scrollToBufferPosition: (bufferPosition, options) -> @scrollToPixelPosition(@pixelPositionForBufferPosition(bufferPosition), options) + # Public: Scrolls the editor to the given screen position. + # + # screenPosition - An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # options - A hash matching the options available to {.scrollToPixelPosition} scrollToScreenPosition: (screenPosition, options) -> @scrollToPixelPosition(@pixelPositionForScreenPosition(screenPosition), options) + # Public: Scrolls the editor to the given pixel position. + # + # pixelPosition - An object that represents a pixel position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # options - A hash with the following keys: + # :center - if `true`, the position is scrolled such that it's in the center of the editor scrollToPixelPosition: (pixelPosition, options) -> return unless @attached @scrollVertically(pixelPosition, options) @scrollHorizontally(pixelPosition) + # Internal: Scrolls the editor vertically to a given position. scrollVertically: (pixelPosition, {center}={}) -> scrollViewHeight = @scrollView.height() scrollTop = @scrollTop() @@ -553,6 +941,7 @@ class Editor extends View else if desiredTop < scrollTop @scrollTop(desiredTop) + # Internal: Scrolls the editor horizontally to a given position. scrollHorizontally: (pixelPosition) -> return if @activeEditSession.getSoftWrap() @@ -568,6 +957,11 @@ class Editor extends View else if desiredLeft < @scrollView.scrollLeft() @scrollView.scrollLeft(desiredLeft) + # Public: Given a buffer range, this highlights all the folds within that range + # + # "Highlighting" essentially just adds the `selected` class to the line + # + # bufferRange - The {Range} to check highlightFoldsContainingBufferRange: (bufferRange) -> screenLines = @linesForScreenRows(@firstRenderedScreenRow, @lastRenderedScreenRow) for screenLine, i in screenLines @@ -588,9 +982,11 @@ class Editor extends View @activeEditSession.setScrollTop(@scrollTop()) @activeEditSession.setScrollLeft(@scrollView.scrollLeft()) + # Public: Activates soft tabs in the editor. toggleSoftTabs: -> @activeEditSession.setSoftTabs(not @activeEditSession.softTabs) + # Public: Activates soft wraps in the editor. toggleSoftWrap: -> @setSoftWrap(not @activeEditSession.getSoftWrap()) @@ -600,6 +996,11 @@ class Editor extends View else Infinity + # Public: Sets the soft wrap column for the editor. + # + # softWrap - A {Boolean} which, if `true`, sets soft wraps + # softWrapColumn - A {Number} indicating the length of a line in the editor when soft + # wrapping turns on setSoftWrap: (softWrap, softWrapColumn=undefined) -> @activeEditSession.setSoftWrap(softWrap) @setSoftWrapColumn(softWrapColumn) if @attached @@ -612,6 +1013,9 @@ class Editor extends View @removeClass 'soft-wrap' $(window).off 'resize', @_setSoftWrapColumn + # Public: Sets the font size for the editor. + # + # fontSize - A {Number} indicating the font size in pixels. setFontSize: (fontSize) -> headTag = $("head") styleTag = headTag.find("style.font-size") @@ -626,9 +1030,15 @@ class Editor extends View else @redrawOnReattach = @attached + # Public: Retrieves the font size for the editor. + # + # Returns a {Number} indicating the font size in pixels. getFontSize: -> parseInt(@css("font-size")) + # Public: Sets the font family for the editor. + # + # fontFamily - A {String} identifying the CSS `font-family`, setFontFamily: (fontFamily) -> return if fontFamily == undefined headTag = $("head") @@ -640,11 +1050,16 @@ class Editor extends View styleTag.text(".editor {font-family: #{fontFamily}}") @redraw() + # Public: Gets the font family for the editor. + # + # Returns a {String} identifying the CSS `font-family`, getFontFamily: -> @css("font-family") + # Public: Clears the CSS `font-family` property from the editor. clearFontFamily: -> $('head style.editor-font-family').remove() + # Public: Clears the CSS `font-family` property from the editor. redraw: -> return unless @hasParent() return unless @attached @@ -666,6 +1081,9 @@ class Editor extends View splitDown: (items...) -> @getPane()?.splitDown(items...).activeView + # Public: Retrieve's the `Editor`'s pane. + # + # Returns a {Pane}. getPane: -> @parent('.item-views').parent('.pane').view() @@ -720,6 +1138,10 @@ class Editor extends View appendToLinesView: (view) -> @overlayer.append(view) + ### + # Internal # + ### + calculateDimensions: -> fragment = $('
') @renderedLines.append(fragment) @@ -969,15 +1391,34 @@ class Editor extends View @renderedLines.css('padding-bottom', paddingBottom) @gutter.lineNumbers.css('padding-bottom', paddingBottom) + ### + # Public # + ### + + # Public: Retrieves the number of the row that is visible and currently at the top of the editor. + # + # Returns a {Number}. getFirstVisibleScreenRow: -> Math.floor(@scrollTop() / @lineHeight) + # Public: Retrieves the number of the row that is visible and currently at the top of the editor. + # + # Returns a {Number}. getLastVisibleScreenRow: -> Math.max(0, Math.ceil((@scrollTop() + @scrollView.height()) / @lineHeight) - 1) + # Public: Given a row number, identifies if it is currently visible. + # + # row - A row {Number} to check + # + # Returns a {Boolean}. isScreenRowVisible: (row) -> @getFirstVisibleScreenRow() <= row <= @getLastVisibleScreenRow() + ### + # Internal # + ### + handleScreenLinesChange: (change) -> @pendingChanges.push(change) @requestDisplayUpdate() @@ -1084,9 +1525,25 @@ class Editor extends View toggleLineCommentsInSelection: -> @activeEditSession.toggleLineCommentsInSelection() + ### + # Public # + ### + + # Public: Converts a buffer position to a pixel position. + # + # position - An object that represents a buffer position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # + # Returns an object with two values: `top` and `left`, representing the pixel positions. pixelPositionForBufferPosition: (position) -> @pixelPositionForScreenPosition(@screenPositionForBufferPosition(position)) + # Public: Converts a screen position to a pixel position. + # + # position - An object that represents a screen position. It can be either + # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} + # + # Returns an object with two values: `top` and `left`, representing the pixel positions. pixelPositionForScreenPosition: (position) -> return { top: 0, left: 0 } unless @isOnDom() and @isVisible() {row, column} = Point.fromObject(position) @@ -1152,6 +1609,7 @@ class Editor extends View new Point(row, column) + # Public: Highlights the current line the cursor is on. highlightCursorLine: -> return if @mini @@ -1162,13 +1620,20 @@ class Editor extends View else @highlightedLine = null + # Public: Retrieves the current {EditSession}'s grammar. + # + # Returns a {String} indicating the language's grammar rules. getGrammar: -> @activeEditSession.getGrammar() + # Public: Sets the current {EditSession}'s grammar. This only works for mini-editors. + # + # grammar - A {String} indicating the language's grammar rules. setGrammar: (grammar) -> throw new Error("Only mini-editors can explicity set their grammar") unless @mini @activeEditSession.setGrammar(grammar) + # Public: Reloads the current grammar. reloadGrammar: -> @activeEditSession.reloadGrammar() @@ -1179,6 +1644,7 @@ class Editor extends View @on event, => callback(this, event) + # Internal: Replaces all the currently selected text. replaceSelectedText: (replaceFn) -> selection = @getSelection() return false if selection.isEmpty() @@ -1189,10 +1655,19 @@ class Editor extends View @insertText(text, select: true) true + # Public: Copies the current file path to the native clipboard. copyPathToPasteboard: -> path = @getPath() pasteboard.write(path) if path? + ### + # Internal # + ### + + transact: (fn) -> @activeEditSession.transact(fn) + commit: -> @activeEditSession.commit() + abort: -> @activeEditSession.abort() + saveDebugSnapshot: -> atom.showSaveDialog (path) => fsUtils.write(path, @getDebugSnapshot()) if path diff --git a/src/app/event-emitter.coffee b/src/app/event-emitter.coffee index 3abcb74ac..cd7bbccda 100644 --- a/src/app/event-emitter.coffee +++ b/src/app/event-emitter.coffee @@ -1,6 +1,16 @@ _ = require 'underscore' +# Public: Provides a list of functions that can be used in Atom for event management. +# +# Each event can have more than one handler; that is, an event can trigger multiple functions. module.exports = + # Public: Associates an event name with a function to perform. + # + # This is called endlessly, until the event is turned {.off}. The {.on} method + # calls an `eventName` only once. + # + # eventName - A {String} name identifying an event + # handler - A {Function} that's executed when the event is triggered on: (eventName, handler) -> [eventName, namespace] = eventName.split('.') @@ -17,6 +27,10 @@ module.exports = @afterSubscribe?() + # Public: Associates an event name with a function to perform only once. + # + # eventName - A {String} name identifying an event + # handler - A {Function} that's executed when the event is triggered one: (eventName, handler) -> oneShotHandler = (args...) => @off(eventName, oneShotHandler) @@ -24,6 +38,10 @@ module.exports = @on eventName, oneShotHandler + # Public: Triggers a registered event. + # + # eventName - A {String} name identifying an event + # args - Any additional arguments to pass over to the event `handler` trigger: (eventName, args...) -> if @queuedEvents @queuedEvents.push [eventName, args...] @@ -37,6 +55,10 @@ module.exports = if handlers = @eventHandlersByEventName?[eventName] handlers.forEach (handler) -> handler(args...) + # Public: Stops executing handlers for a registered event. + # + # eventName - A {String} name identifying an event + # handler - The {Function} to remove from the event. If not provided, all handlers are removed. off: (eventName='', handler) -> [eventName, namespace] = eventName.split('.') eventName = undefined if eventName == '' @@ -67,17 +89,22 @@ module.exports = @afterUnsubscribe?() if @subscriptionCount() < subscriptionCountBefore + # Public: When called, stops triggering any event. pauseEvents: -> @pauseCount ?= 0 if @pauseCount++ == 0 @queuedEvents ?= [] + # Public: When called, resumes triggering events. resumeEvents: -> if --@pauseCount == 0 queuedEvents = @queuedEvents @queuedEvents = null @trigger(event...) for event in queuedEvents + # Public: Identifies how many events are registered. + # + # Returns a `number`. subscriptionCount: -> count = 0 for name, handlers of @eventHandlersByEventName diff --git a/src/app/file.coffee b/src/app/file.coffee index ec06ef189..4f1782ff2 100644 --- a/src/app/file.coffee +++ b/src/app/file.coffee @@ -5,29 +5,55 @@ fsUtils = require 'fs-utils' pathWatcher = require 'pathwatcher' _ = require 'underscore' +# Public: Represents an individual file in the editor. +# +# The entry point for this class is in two locations: +# * {Buffer}, which associates text contents with a file +# * {Directory}, which associcates the children of a directory as files module.exports = class File path: null cachedContents: null + # Public: Creates a new file. + # + # path - A {String} representing the file path + # symlink - A {Boolean} indicating if the path is a symlink (default: false) constructor: (@path, @symlink=false) -> try if fs.statSync(@path).isDirectory() throw new Error("#{@path} is a directory") + # Public: Sets the path for the file. + # + # path - A {String} representing the new file path setPath: (@path) -> + # Public: Retrieves the path for the file. + # + # Returns a {String}. getPath: -> @path + # Public: Gets the file's basename--that is, the file without any directory information. + # + # Returns a {String}. getBaseName: -> fsUtils.base(@path) + # Public: Writes (and saves) new contents to the file. + # + # text - A {String} representing the new contents. write: (text) -> previouslyExisted = @exists() @cachedContents = text fsUtils.write(@getPath(), text) @subscribeToNativeChangeEvents() if not previouslyExisted and @subscriptionCount() > 0 + # Public: Reads the file. + # + # flushCache - A {Boolean} indicating if the cache should be erased--_i.e._, a force read is performed + # + # Returns a {String}. read: (flushCache)-> if not @exists() @cachedContents = null @@ -36,12 +62,19 @@ class File else @cachedContents + # Public: Checks to see if a file exists. + # + # Returns a {Boolean}. exists: -> fsUtils.exists(@getPath()) + ### + # Internal # + ### + afterSubscribe: -> @subscribeToNativeChangeEvents() if @exists() and @subscriptionCount() == 1 - + afterUnsubscribe: -> @unsubscribeFromNativeChangeEvents() if @subscriptionCount() == 0 diff --git a/src/app/fold.coffee b/src/app/fold.coffee index 50e5d34ba..e53824924 100644 --- a/src/app/fold.coffee +++ b/src/app/fold.coffee @@ -1,6 +1,10 @@ Range = require 'range' Point = require 'point' +# Public: Represents a fold that's hiding text from the screen. +# +# Folds are the primary reason that screen ranges and buffer ranges vary. Their +# creation is managed by the {DisplayBuffer}. module.exports = class Fold @idCounter: 1 @@ -9,6 +13,10 @@ class Fold startRow: null endRow: null + ### + # Internal # + ### + constructor: (@displayBuffer, @startRow, @endRow) -> @id = @constructor.idCounter++ @@ -18,6 +26,11 @@ class Fold inspect: -> "Fold(#{@startRow}, #{@endRow})" + # Public: Retrieves the buffer row range that a fold occupies. + # + # includeNewline - A {Boolean} which, if `true`, includes the trailing newline + # + # Returns a {Range}. getBufferRange: ({includeNewline}={}) -> if includeNewline end = [@endRow + 1, 0] @@ -26,6 +39,9 @@ class Fold new Range([@startRow, 0], end) + # Public: Retrieves the number of buffer rows a fold occupies. + # + # Returns a {Number}. getBufferRowCount: -> @endRow - @startRow + 1 @@ -43,9 +59,19 @@ class Fold @displayBuffer.unregisterFold(oldStartRow, this) @displayBuffer.registerFold(this) + # Public: Identifies if a {Range} occurs within a fold. + # + # range - A {Range} to check + # + # Returns a {Boolean}. isContainedByRange: (range) -> range.start.row <= @startRow and @endRow <= range.end.row + # Public: Identifies if a fold is nested within a fold. + # + # fold - A {Fold} to check + # + # Returns a {Boolean}. isContainedByFold: (fold) -> @isContainedByRange(fold.getBufferRange()) diff --git a/src/app/git.coffee b/src/app/git.coffee index ee9c953ac..7c15a55ac 100644 --- a/src/app/git.coffee +++ b/src/app/git.coffee @@ -5,8 +5,18 @@ EventEmitter = require 'event-emitter' RepositoryStatusTask = require 'repository-status-task' GitUtils = require 'git-utils' +# Public: Represents the underlying git operations performed by Atom. +# +# Ultimately, this is an overlay to the native [git-utils](https://github.com/atom/node-git) model. module.exports = class Git + # Public: Creates a new `Git` instance. + # + # path - The git repository to open + # options - A hash with one key: + # :refreshOnWindowFocus - A {Boolean} that identifies if the windows should refresh + # + # Returns a new {Git} object. @open: (path, options) -> return null unless path try @@ -18,6 +28,15 @@ class Git upstream: null statusTask: null + ### + # Internal # + ### + + # Internal: Creates a new `Git` object. + # + # path - The {String} representing the path to your git working directory + # options - A hash with the following keys: + # :refreshOnWindowFocus - If `true`, {#refreshIndex} and {#refreshStatus} are called on focus constructor: (path, options={}) -> @repo = GitUtils.open(path) unless @repo? @@ -40,16 +59,6 @@ class Git @subscribe buffer, 'saved', bufferStatusHandler @subscribe buffer, 'reloaded', bufferStatusHandler - getRepo: -> - unless @repo? - throw new Error("Repository has been destroyed") - @repo - - refreshIndex: -> @getRepo().refreshIndex() - - getPath: -> - @path ?= fsUtils.absolute(@getRepo().getPath()) - destroy: -> if @statusTask? @statusTask.abort() @@ -62,12 +71,46 @@ class Git @unsubscribe() + ### + # Public # + ### + + # Public: Retrieves the git repository. + # + # Returns a new `Repository`. + getRepo: -> + unless @repo? + throw new Error("Repository has been destroyed") + @repo + + # Public: Reread the index to update any values that have changed since the last time the index was read. + refreshIndex: -> @getRepo().refreshIndex() + + # Public: Retrieves the path of the repository. + # + # Returns a {String}. + getPath: -> + @path ?= fsUtils.absolute(@getRepo().getPath()) + + # Public: Retrieves the working directory of the repository. + # + # Returns a {String}. getWorkingDirectory: -> @getRepo().getWorkingDirectory() + # Public: Retrieves the reference or SHA-1 that `HEAD` points to. + # + # This can be `refs/heads/master`, or a full SHA-1 if the repository is in a detached `HEAD` state. + # + # Returns a {String}. getHead: -> @getRepo().getHead() ? '' + # Public: Retrieves the status of a single path in the repository. + # + # path - An {String} defining a relative path + # + # Returns a {Number}. getPathStatus: (path) -> currentPathStatus = @statuses[path] ? 0 pathStatus = @getRepo().getStatus(@relativize(path)) ? 0 @@ -79,38 +122,132 @@ class Git @trigger 'status-changed', path, pathStatus pathStatus + # Public: Identifies if a path is ignored. + # + # path - The {String} path to check + # + # Returns a {Boolean}. isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path)) + # Public: Identifies if a value represents a status code. + # + # status - The code {Number} to check + # + # Returns a {Boolean}. isStatusModified: (status) -> @getRepo().isStatusModified(status) + # Public: Identifies if a path was modified. + # + # path - The {String} path to check + # + # Returns a {Boolean}. isPathModified: (path) -> @isStatusModified(@getPathStatus(path)) + # Public: Identifies if a status code represents a new path. + # + # status - The code {Number} to check + # + # Returns a {Boolean}. isStatusNew: (status) -> @getRepo().isStatusNew(status) + # Public: Identifies if a path is new. + # + # path - The {String} path to check + # + # Returns a {Boolean}. isPathNew: (path) -> @isStatusNew(@getPathStatus(path)) + # Public: Makes a path relative to the repository's working directory. + # + # path - The {String} path to convert + # + # Returns a {String}. relativize: (path) -> @getRepo().relativize(path) + # Public: Retrieves a shortened version of {.getHead}. + # + # This removes the leading segments of `refs/heads`, `refs/tags`, or `refs/remotes`. + # It also shortenes the SHA-1 of a detached `HEAD` to 7 characters. + # + # Returns a {String}. getShortHead: -> @getRepo().getShortHead() + # Public: Restore the contents of a path in the working directory and index to the version at `HEAD`. + # + # This is essentially the same as running: + # ``` + # git reset HEAD --