_ = require 'underscore-plus' path = require 'path' Grim = require 'grim' {CompositeDisposable, Emitter} = require 'event-kit' {Point, Range} = TextBuffer = require 'text-buffer' LanguageMode = require './language-mode' DisplayBuffer = require './display-buffer' Cursor = require './cursor' Model = require './model' Selection = require './selection' TextMateScopeSelector = require('first-mate').ScopeSelector {Directory} = require "pathwatcher" GutterContainer = require './gutter-container' TextEditorElement = require './text-editor-element' # Essential: This class represents all essential editing state for a single # {TextBuffer}, including cursor and selection positions, folds, and soft wraps. # If you're manipulating the state of an editor, use this class. If you're # interested in the visual appearance of editors, use {TextEditorElement} # instead. # # A single {TextBuffer} can belong to multiple editors. For example, if the # same file is open in two different panes, Atom creates a separate editor for # each pane. If the buffer is manipulated the changes are reflected in both # editors, but each maintains its own cursor position, folded lines, etc. # # ## Accessing TextEditor Instances # # The easiest way to get hold of `TextEditor` objects is by registering a callback # with `::observeTextEditors` on the `atom.workspace` global. Your callback will # then be called with all current editor instances and also when any editor is # created in the future. # # ```coffee # atom.workspace.observeTextEditors (editor) -> # editor.insertText('Hello World') # ``` # # ## Buffer vs. Screen Coordinates # # Because editors support folds and soft-wrapping, the lines on screen don't # always match the lines in the buffer. For example, a long line that soft wraps # twice renders as three lines on screen, but only represents one line in the # buffer. Similarly, if rows 5-10 are folded, then row 6 on screen corresponds # to row 11 in the buffer. # # Your choice of coordinates systems will depend on what you're trying to # achieve. For example, if you're writing a command that jumps the cursor up or # down by 10 lines, you'll want to use screen coordinates because the user # probably wants to skip lines *on screen*. However, if you're writing a package # that jumps between method definitions, you'll want to work in buffer # coordinates. # # **When in doubt, just default to buffer coordinates**, then experiment with # soft wraps and folds to ensure your code interacts with them correctly. module.exports = class TextEditor extends Model buffer: null languageMode: null cursors: null selections: null suppressSelectionMerging: false selectionFlashDuration: 500 gutterContainer: null editorElement: null Object.defineProperty @prototype, "element", get: -> @getElement() @deserialize: (state, atomEnvironment) -> try displayBuffer = DisplayBuffer.deserialize(state.displayBuffer, atomEnvironment) catch error if error.syscall is 'read' return # Error reading the file, don't deserialize an editor for it else throw error state.displayBuffer = displayBuffer state.selectionsMarkerLayer = displayBuffer.getMarkerLayer(state.selectionsMarkerLayerId) state.config = atomEnvironment.config state.notificationManager = atomEnvironment.notifications state.packageManager = atomEnvironment.packages state.clipboard = atomEnvironment.clipboard state.viewRegistry = atomEnvironment.views state.grammarRegistry = atomEnvironment.grammars state.project = atomEnvironment.project state.assert = atomEnvironment.assert.bind(atomEnvironment) state.applicationDelegate = atomEnvironment.applicationDelegate editor = new this(state) disposable = atomEnvironment.textEditors.add(editor) editor.onDidDestroy -> disposable.dispose() editor constructor: (params={}) -> super { @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength, softWrapped, @displayBuffer, @selectionsMarkerLayer, buffer, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, largeFileMode, @config, @notificationManager, @packageManager, @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate, grammar, showInvisibles, @autoHeight, @scrollPastEnd } = params throw new Error("Must pass a config parameter when constructing TextEditors") unless @config? throw new Error("Must pass a notificationManager parameter when constructing TextEditors") unless @notificationManager? throw new Error("Must pass a packageManager parameter when constructing TextEditors") unless @packageManager? throw new Error("Must pass a clipboard parameter when constructing TextEditors") unless @clipboard? throw new Error("Must pass a viewRegistry parameter when constructing TextEditors") unless @viewRegistry? throw new Error("Must pass a grammarRegistry parameter when constructing TextEditors") unless @grammarRegistry? throw new Error("Must pass a project parameter when constructing TextEditors") unless @project? throw new Error("Must pass an assert parameter when constructing TextEditors") unless @assert? @firstVisibleScreenRow ?= 0 @firstVisibleScreenColumn ?= 0 @emitter = new Emitter @disposables = new CompositeDisposable @cursors = [] @cursorsByMarkerId = new Map @selections = [] @autoHeight ?= true @scrollPastEnd ?= true @hasTerminatedPendingState = false showInvisibles ?= true buffer ?= new TextBuffer @displayBuffer ?= new DisplayBuffer({ buffer, tabLength, softWrapped, ignoreInvisibles: @mini or not showInvisibles, largeFileMode, @config, @assert, @grammarRegistry, @packageManager }) {@buffer, @displayLayer} = @displayBuffer @selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true) for marker in @selectionsMarkerLayer.getMarkers() marker.setProperties(preserveFolds: true) @addSelection(marker) @subscribeToTabTypeConfig() @subscribeToBuffer() @subscribeToDisplayBuffer() if @cursors.length is 0 and not suppressCursorCreation initialLine = Math.max(parseInt(initialLine) or 0, 0) initialColumn = Math.max(parseInt(initialColumn) or 0, 0) @addCursorAtBufferPosition([initialLine, initialColumn]) @languageMode = new LanguageMode(this, @config) @setEncoding(@config.get('core.fileEncoding', scope: @getRootScopeDescriptor())) @gutterContainer = new GutterContainer(this) @lineNumberGutter = @gutterContainer.addGutter name: 'line-number' priority: 0 visible: lineNumberGutterVisible if grammar? @setGrammar(grammar) serialize: -> deserializer: 'TextEditor' id: @id softTabs: @softTabs firstVisibleScreenRow: @getFirstVisibleScreenRow() firstVisibleScreenColumn: @getFirstVisibleScreenColumn() displayBuffer: @displayBuffer.serialize() selectionsMarkerLayerId: @selectionsMarkerLayer.id subscribeToBuffer: -> @buffer.retain() @disposables.add @buffer.onDidChangePath => unless @project.getPaths().length > 0 @project.setPaths([path.dirname(@getPath())]) @emitter.emit 'did-change-title', @getTitle() @emitter.emit 'did-change-path', @getPath() @disposables.add @buffer.onDidChangeEncoding => @emitter.emit 'did-change-encoding', @getEncoding() @disposables.add @buffer.onDidDestroy => @destroy() @disposables.add @buffer.onDidChangeModified => @terminatePendingState() if not @hasTerminatedPendingState and @buffer.isModified() @preserveCursorPositionOnBufferReload() terminatePendingState: -> @emitter.emit 'did-terminate-pending-state' if not @hasTerminatedPendingState @hasTerminatedPendingState = true onDidTerminatePendingState: (callback) -> @emitter.on 'did-terminate-pending-state', callback subscribeToDisplayBuffer: -> @disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this) @disposables.add @displayBuffer.onDidChangeGrammar @handleGrammarChange.bind(this) @disposables.add @displayBuffer.onDidTokenize @handleTokenization.bind(this) @disposables.add @displayLayer.onDidChangeSync (e) => @mergeIntersectingSelections() @emitter.emit 'did-change', e subscribeToTabTypeConfig: -> @tabTypeSubscription?.dispose() @tabTypeSubscription = @config.observe 'editor.tabType', scope: @getRootScopeDescriptor(), => @softTabs = @shouldUseSoftTabs(defaultValue: @softTabs) destroyed: -> @disposables.dispose() @tabTypeSubscription.dispose() selection.destroy() for selection in @selections.slice() @selectionsMarkerLayer.destroy() @buffer.release() @displayBuffer.destroy() @languageMode.destroy() @gutterContainer.destroy() @emitter.emit 'did-destroy' ### Section: Event Subscription ### # Essential: Calls your `callback` when the buffer's title has changed. # # * `callback` {Function} # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeTitle: (callback) -> @emitter.on 'did-change-title', callback # Essential: Calls your `callback` when the buffer's path, and therefore title, has changed. # # * `callback` {Function} # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangePath: (callback) -> @emitter.on 'did-change-path', callback # Essential: Invoke the given callback synchronously when the content of the # buffer changes. # # Because observers are invoked synchronously, it's important not to perform # any expensive operations via this method. Consider {::onDidStopChanging} to # delay expensive operations until after changes stop occurring. # # * `callback` {Function} # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChange: (callback) -> @emitter.on 'did-change', callback # Essential: Invoke `callback` when the buffer's contents change. It is # emit asynchronously 300ms after the last buffer change. This is a good place # to handle changes to the buffer without compromising typing performance. # # * `callback` {Function} # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidStopChanging: (callback) -> @getBuffer().onDidStopChanging(callback) # Essential: Calls your `callback` when a {Cursor} is moved. If there are # multiple cursors, your callback will be called for each cursor. # # * `callback` {Function} # * `event` {Object} # * `oldBufferPosition` {Point} # * `oldScreenPosition` {Point} # * `newBufferPosition` {Point} # * `newScreenPosition` {Point} # * `textChanged` {Boolean} # * `cursor` {Cursor} that triggered the event # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeCursorPosition: (callback) -> @emitter.on 'did-change-cursor-position', callback # Essential: Calls your `callback` when a selection's screen range changes. # # * `callback` {Function} # * `event` {Object} # * `oldBufferRange` {Range} # * `oldScreenRange` {Range} # * `newBufferRange` {Range} # * `newScreenRange` {Range} # * `selection` {Selection} that triggered the event # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeSelectionRange: (callback) -> @emitter.on 'did-change-selection-range', callback # Extended: Calls your `callback` when soft wrap was enabled or disabled. # # * `callback` {Function} # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeSoftWrapped: (callback) -> @displayBuffer.onDidChangeSoftWrapped(callback) # Extended: Calls your `callback` when the buffer's encoding has changed. # # * `callback` {Function} # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeEncoding: (callback) -> @emitter.on 'did-change-encoding', callback # Extended: Calls your `callback` when the grammar that interprets and # colorizes the text has been changed. Immediately calls your callback with # the current grammar. # # * `callback` {Function} # * `grammar` {Grammar} # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeGrammar: (callback) -> callback(@getGrammar()) @onDidChangeGrammar(callback) # Extended: Calls your `callback` when the grammar that interprets and # colorizes the text has been changed. # # * `callback` {Function} # * `grammar` {Grammar} # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeGrammar: (callback) -> @emitter.on 'did-change-grammar', callback # Extended: Calls your `callback` when the result of {::isModified} changes. # # * `callback` {Function} # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangeModified: (callback) -> @getBuffer().onDidChangeModified(callback) # Extended: Calls your `callback` when the buffer's underlying file changes on # disk at a moment when the result of {::isModified} is true. # # * `callback` {Function} # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidConflict: (callback) -> @getBuffer().onDidConflict(callback) # Extended: Calls your `callback` before text has been inserted. # # * `callback` {Function} # * `event` event {Object} # * `text` {String} text to be inserted # * `cancel` {Function} Call to prevent the text from being inserted # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onWillInsertText: (callback) -> @emitter.on 'will-insert-text', callback # Extended: Calls your `callback` after text has been inserted. # # * `callback` {Function} # * `event` event {Object} # * `text` {String} text to be inserted # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidInsertText: (callback) -> @emitter.on 'did-insert-text', callback # Essential: Invoke the given callback after the buffer is saved to disk. # # * `callback` {Function} to be called after the buffer is saved. # * `event` {Object} with the following keys: # * `path` The path to which the buffer was saved. # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidSave: (callback) -> @getBuffer().onDidSave(callback) # Essential: Invoke the given callback when the editor is destroyed. # # * `callback` {Function} to be called when the editor is destroyed. # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidDestroy: (callback) -> @emitter.on 'did-destroy', callback # Extended: Calls your `callback` when a {Cursor} is added to the editor. # Immediately calls your callback for each existing cursor. # # * `callback` {Function} # * `cursor` {Cursor} that was added # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeCursors: (callback) -> callback(cursor) for cursor in @getCursors() @onDidAddCursor(callback) # Extended: Calls your `callback` when a {Cursor} is added to the editor. # # * `callback` {Function} # * `cursor` {Cursor} that was added # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddCursor: (callback) -> @emitter.on 'did-add-cursor', callback # Extended: Calls your `callback` when a {Cursor} is removed from the editor. # # * `callback` {Function} # * `cursor` {Cursor} that was removed # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidRemoveCursor: (callback) -> @emitter.on 'did-remove-cursor', callback # Extended: Calls your `callback` when a {Selection} is added to the editor. # Immediately calls your callback for each existing selection. # # * `callback` {Function} # * `selection` {Selection} that was added # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeSelections: (callback) -> callback(selection) for selection in @getSelections() @onDidAddSelection(callback) # Extended: Calls your `callback` when a {Selection} is added to the editor. # # * `callback` {Function} # * `selection` {Selection} that was added # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddSelection: (callback) -> @emitter.on 'did-add-selection', callback # Extended: Calls your `callback` when a {Selection} is removed from the editor. # # * `callback` {Function} # * `selection` {Selection} that was removed # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidRemoveSelection: (callback) -> @emitter.on 'did-remove-selection', callback # Extended: Calls your `callback` with each {Decoration} added to the editor. # Calls your `callback` immediately for any existing decorations. # # * `callback` {Function} # * `decoration` {Decoration} # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeDecorations: (callback) -> @displayBuffer.observeDecorations(callback) # Extended: Calls your `callback` when a {Decoration} is added to the editor. # # * `callback` {Function} # * `decoration` {Decoration} that was added # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddDecoration: (callback) -> @displayBuffer.onDidAddDecoration(callback) # Extended: Calls your `callback` when a {Decoration} is removed from the editor. # # * `callback` {Function} # * `decoration` {Decoration} that was removed # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidRemoveDecoration: (callback) -> @displayBuffer.onDidRemoveDecoration(callback) # Extended: Calls your `callback` when the placeholder text is changed. # # * `callback` {Function} # * `placeholderText` {String} new text # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidChangePlaceholderText: (callback) -> @emitter.on 'did-change-placeholder-text', callback onDidChangeCharacterWidths: (callback) -> @displayBuffer.onDidChangeCharacterWidths(callback) onDidChangeFirstVisibleScreenRow: (callback, fromView) -> @emitter.on 'did-change-first-visible-screen-row', callback onDidChangeScrollTop: (callback) -> Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.") @viewRegistry.getView(this).onDidChangeScrollTop(callback) onDidChangeScrollLeft: (callback) -> Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollLeft instead.") @viewRegistry.getView(this).onDidChangeScrollLeft(callback) onDidRequestAutoscroll: (callback) -> @displayBuffer.onDidRequestAutoscroll(callback) # TODO Remove once the tabs package no longer uses .on subscriptions onDidChangeIcon: (callback) -> @emitter.on 'did-change-icon', callback onDidUpdateMarkers: (callback) -> @displayBuffer.onDidUpdateMarkers(callback) onDidUpdateDecorations: (callback) -> @displayBuffer.onDidUpdateDecorations(callback) # Essential: Retrieves the current {TextBuffer}. getBuffer: -> @buffer # Retrieves the current buffer's URI. getURI: -> @buffer.getUri() # Create an {TextEditor} with its initial state based on this object copy: -> displayBuffer = @displayBuffer.copy() selectionsMarkerLayer = displayBuffer.getMarkerLayer(@buffer.getMarkerLayer(@selectionsMarkerLayer.id).copy().id) softTabs = @getSoftTabs() newEditor = new TextEditor({ @buffer, displayBuffer, selectionsMarkerLayer, @tabLength, softTabs, suppressCursorCreation: true, @config, @notificationManager, @packageManager, @firstVisibleScreenRow, @firstVisibleScreenColumn, @clipboard, @viewRegistry, @grammarRegistry, @project, @assert, @applicationDelegate }) newEditor # Controls visibility based on the given {Boolean}. setVisible: (visible) -> @displayBuffer.setVisible(visible) setMini: (mini) -> if mini isnt @mini @mini = mini @displayBuffer.setIgnoreInvisibles(@mini) @emitter.emit 'did-change-mini', @mini @mini isMini: -> @mini setUpdatedSynchronously: (updatedSynchronously) -> @displayBuffer.setUpdatedSynchronously(updatedSynchronously) onDidChangeMini: (callback) -> @emitter.on 'did-change-mini', callback setLineNumberGutterVisible: (lineNumberGutterVisible) -> unless lineNumberGutterVisible is @lineNumberGutter.isVisible() if lineNumberGutterVisible @lineNumberGutter.show() else @lineNumberGutter.hide() @emitter.emit 'did-change-line-number-gutter-visible', @lineNumberGutter.isVisible() @lineNumberGutter.isVisible() isLineNumberGutterVisible: -> @lineNumberGutter.isVisible() onDidChangeLineNumberGutterVisible: (callback) -> @emitter.on 'did-change-line-number-gutter-visible', callback # Essential: Calls your `callback` when a {Gutter} is added to the editor. # Immediately calls your callback for each existing gutter. # # * `callback` {Function} # * `gutter` {Gutter} that currently exists/was added. # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. observeGutters: (callback) -> @gutterContainer.observeGutters callback # Essential: Calls your `callback` when a {Gutter} is added to the editor. # # * `callback` {Function} # * `gutter` {Gutter} that was added. # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidAddGutter: (callback) -> @gutterContainer.onDidAddGutter callback # Essential: Calls your `callback` when a {Gutter} is removed from the editor. # # * `callback` {Function} # * `name` The name of the {Gutter} that was removed. # # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. onDidRemoveGutter: (callback) -> @gutterContainer.onDidRemoveGutter callback # Set the number of characters that can be displayed horizontally in the # editor. # # * `editorWidthInChars` A {Number} representing the width of the # {TextEditorElement} in characters. setEditorWidthInChars: (editorWidthInChars) -> @displayBuffer.setEditorWidthInChars(editorWidthInChars) # Returns the editor width in characters. getEditorWidthInChars: -> @displayBuffer.getEditorWidthInChars() ### Section: File Details ### # Essential: Get the editor's title for display in other parts of the # UI such as the tabs. # # If the editor's buffer is saved, its title is the file name. If it is # unsaved, its title is "untitled". # # Returns a {String}. getTitle: -> @getFileName() ? 'untitled' # Essential: Get unique title for display in other parts of the UI, such as # the window title. # # If the editor's buffer is unsaved, its title is "untitled" # If the editor's buffer is saved, its unique title is formatted as one # of the following, # * "" when it is the only editing buffer with this file name. # * "" when other buffers have this file name. # # Returns a {String} getLongTitle: -> if @getPath() fileName = @getFileName() allPathSegments = [] for textEditor in atom.workspace.getTextEditors() when textEditor isnt this if textEditor.getFileName() is fileName allPathSegments.push(textEditor.getDirectoryPath().split(path.sep)) if allPathSegments.length is 0 return fileName ourPathSegments = @getDirectoryPath().split(path.sep) allPathSegments.push ourPathSegments loop firstSegment = ourPathSegments[0] commonBase = _.all(allPathSegments, (pathSegments) -> pathSegments.length > 1 and pathSegments[0] is firstSegment) if commonBase pathSegments.shift() for pathSegments in allPathSegments else break "#{fileName} \u2014 #{path.join(pathSegments...)}" else 'untitled' # Essential: Returns the {String} path of this editor's text buffer. getPath: -> @buffer.getPath() getFileName: -> if fullPath = @getPath() path.basename(fullPath) else null getDirectoryPath: -> if fullPath = @getPath() path.dirname(fullPath) else null # Extended: Returns the {String} character set encoding of this editor's text # buffer. getEncoding: -> @buffer.getEncoding() # Extended: Set the character set encoding to use in this editor's text # buffer. # # * `encoding` The {String} character set encoding name such as 'utf8' setEncoding: (encoding) -> @buffer.setEncoding(encoding) # Essential: Returns {Boolean} `true` if this editor has been modified. isModified: -> @buffer.isModified() # Essential: Returns {Boolean} `true` if this editor has no content. isEmpty: -> @buffer.isEmpty() # Copies the current file path to the native clipboard. copyPathToClipboard: (relative = false) -> if filePath = @getPath() filePath = atom.project.relativize(filePath) if relative @clipboard.write(filePath) ### Section: File Operations ### # Essential: Saves the editor's text buffer. # # See {TextBuffer::save} for more details. save: -> @buffer.save(backup: @config.get('editor.backUpBeforeSaving')) # Essential: Saves the editor's text buffer as the given path. # # See {TextBuffer::saveAs} for more details. # # * `filePath` A {String} path. saveAs: (filePath) -> @buffer.saveAs(filePath, backup: @config.get('editor.backUpBeforeSaving')) # Determine whether the user should be prompted to save before closing # this editor. shouldPromptToSave: ({windowCloseRequested}={}) -> if windowCloseRequested false else @isModified() and not @buffer.hasMultipleEditors() # Returns an {Object} to configure dialog shown when this editor is saved # via {Pane::saveItemAs}. getSaveDialogOptions: -> {} checkoutHeadRevision: -> if @getPath() checkoutHead = => @project.repositoryForDirectory(new Directory(@getDirectoryPath())) .then (repository) => repository?.async.checkoutHeadForEditor(this) if @config.get('editor.confirmCheckoutHeadRevision') @applicationDelegate.confirm message: 'Confirm Checkout HEAD Revision' detailedMessage: "Are you sure you want to discard all changes to \"#{@getFileName()}\" since the last Git commit?" buttons: OK: checkoutHead Cancel: null else checkoutHead() else Promise.resolve(false) ### Section: Reading Text ### # Essential: Returns a {String} representing the entire contents of the editor. getText: -> @buffer.getText() # Essential: Get the text in the given {Range} in buffer coordinates. # # * `range` A {Range} or range-compatible {Array}. # # Returns a {String}. getTextInBufferRange: (range) -> @buffer.getTextInRange(range) # Essential: Returns a {Number} representing the number of lines in the buffer. getLineCount: -> @buffer.getLineCount() # Essential: Returns a {Number} representing the number of screen lines in the # editor. This accounts for folds. getScreenLineCount: -> @displayLayer.getScreenLineCount() # Essential: Returns a {Number} representing the last zero-indexed buffer row # number of the editor. getLastBufferRow: -> @buffer.getLastRow() # Essential: Returns a {Number} representing the last zero-indexed screen row # number of the editor. getLastScreenRow: -> @getScreenLineCount() - 1 # Essential: Returns a {String} representing the contents of the line at the # given buffer row. # # * `bufferRow` A {Number} representing a zero-indexed buffer row. lineTextForBufferRow: (bufferRow) -> @buffer.lineForRow(bufferRow) # Essential: Returns a {String} representing the contents of the line at the # given screen row. # # * `screenRow` A {Number} representing a zero-indexed screen row. lineTextForScreenRow: (screenRow) -> line = @displayLayer.getScreenLines(screenRow, screenRow + 1)[0] line?.tokens.map((t) -> t.text).join('') # Gets the screen line for the given screen row. # # * `screenRow` - A {Number} indicating the screen row. # # Returns {TokenizedLine} tokenizedLineForScreenRow: (screenRow) -> @displayBuffer.tokenizedLineForScreenRow(screenRow) # {Delegates to: DisplayBuffer.tokenizedLinesForScreenRows} tokenizedLinesForScreenRows: (start, end) -> @displayBuffer.tokenizedLinesForScreenRows(start, end) bufferRowForScreenRow: (row) -> @displayLayer.translateScreenPosition(Point(row, 0)).row # {Delegates to: DisplayBuffer.bufferRowsForScreenRows} bufferRowsForScreenRows: (startRow, endRow) -> @displayBuffer.bufferRowsForScreenRows(startRow, endRow) screenRowForBufferRow: (row) -> @displayLayer.translateBufferPosition(Point(row, 0)).row getRightmostScreenPosition: -> @displayLayer.getRightmostScreenPosition() # {Delegates to: DisplayBuffer.getMaxLineLength} getMaxScreenLineLength: -> @getRightmostScreenPosition().column getLongestScreenRow: -> @getRightmostScreenPosition().row lineLengthForScreenRow: (screenRow) -> @displayLayer.lineLengthForScreenRow(screenRow) # Returns the range for the given buffer row. # # * `row` A row {Number}. # * `options` (optional) An options hash with an `includeNewline` key. # # Returns a {Range}. bufferRangeForBufferRow: (row, {includeNewline}={}) -> @buffer.rangeForRow(row, includeNewline) # Get the text in the given {Range}. # # Returns a {String}. getTextInRange: (range) -> @buffer.getTextInRange(range) # {Delegates to: TextBuffer.isRowBlank} isBufferRowBlank: (bufferRow) -> @buffer.isRowBlank(bufferRow) # {Delegates to: TextBuffer.nextNonBlankRow} nextNonBlankBufferRow: (bufferRow) -> @buffer.nextNonBlankRow(bufferRow) # {Delegates to: TextBuffer.getEndPosition} getEofBufferPosition: -> @buffer.getEndPosition() # Essential: Get the {Range} of the paragraph surrounding the most recently added # cursor. # # Returns a {Range}. getCurrentParagraphBufferRange: -> @getLastCursor().getCurrentParagraphBufferRange() ### Section: Mutating Text ### # Essential: Replaces the entire contents of the buffer with the given {String}. # # * `text` A {String} to replace with setText: (text) -> @buffer.setText(text) # Essential: Set the text in the given {Range} in buffer coordinates. # # * `range` A {Range} or range-compatible {Array}. # * `text` A {String} # * `options` (optional) {Object} # * `normalizeLineEndings` (optional) {Boolean} (default: true) # * `undo` (optional) {String} 'skip' will skip the undo system # # Returns the {Range} of the newly-inserted text. setTextInBufferRange: (range, text, options) -> @getBuffer().setTextInRange(range, text, options) # Essential: For each selection, replace the selected text with the given text. # # * `text` A {String} representing the text to insert. # * `options` (optional) See {Selection::insertText}. # # Returns a {Range} when the text has been inserted # Returns a {Boolean} false when the text has not been inserted insertText: (text, options={}) -> return false unless @emitWillInsertTextEvent(text) groupingInterval = if options.groupUndo @config.get('editor.undoGroupingInterval') else 0 options.autoIndentNewline ?= @shouldAutoIndent() options.autoDecreaseIndent ?= @shouldAutoIndent() @mutateSelectedText( (selection) => range = selection.insertText(text, options) didInsertEvent = {text, range} @emitter.emit 'did-insert-text', didInsertEvent range , groupingInterval ) # Essential: For each selection, replace the selected text with a newline. insertNewline: -> @insertText('\n') # Essential: For each selection, if the selection is empty, delete the character # following the cursor. Otherwise delete the selected text. delete: -> @mutateSelectedText (selection) -> selection.delete() # Essential: For each selection, if the selection is empty, delete the character # preceding the cursor. Otherwise delete the selected text. backspace: -> @mutateSelectedText (selection) -> selection.backspace() # Extended: Mutate the text of all the selections in a single transaction. # # All the changes made inside the given {Function} can be reverted with a # single call to {::undo}. # # * `fn` A {Function} that will be called once for each {Selection}. The first # argument will be a {Selection} and the second argument will be the # {Number} index of that selection. mutateSelectedText: (fn, groupingInterval=0) -> @mergeIntersectingSelections => @transact groupingInterval, => fn(selection, index) for selection, index in @getSelectionsOrderedByBufferPosition() # Move lines intersecting the most recent selection or multiple selections # up by one row in screen coordinates. moveLineUp: -> selections = @getSelectedBufferRanges() selections.sort (a, b) -> a.compare(b) if selections[0].start.row is 0 return if selections[selections.length - 1].start.row is @getLastBufferRow() and @buffer.getLastLine() is '' return @transact => newSelectionRanges = [] while selections.length > 0 # Find selections spanning a contiguous set of lines selection = selections.shift() selectionsToMove = [selection] while selection.end.row is selections[0]?.start.row selectionsToMove.push(selections[0]) selection.end.row = selections[0].end.row selections.shift() # Compute the range spanned by all these selections... linesRangeStart = [selection.start.row, 0] if selection.end.row > selection.start.row and selection.end.column is 0 # Don't move the last line of a multi-line selection if the selection ends at column 0 linesRange = new Range(linesRangeStart, selection.end) else linesRange = new Range(linesRangeStart, [selection.end.row + 1, 0]) # If there's a fold containing either the starting row or the end row # of the selection then the whole fold needs to be moved and restored. # The initial fold range is stored and will be translated once the # insert delta is know. selectionFoldRanges = [] foldAtSelectionStart = @displayBuffer.largestFoldContainingBufferRow(selection.start.row) foldAtSelectionEnd = @displayBuffer.largestFoldContainingBufferRow(selection.end.row) if fold = foldAtSelectionStart ? foldAtSelectionEnd selectionFoldRanges.push range = fold.getBufferRange() newEndRow = range.end.row + 1 linesRange.end.row = newEndRow if newEndRow > linesRange.end.row fold.destroy() # If selected line range is preceded by a fold, one line above on screen # could be multiple lines in the buffer. precedingScreenRow = @screenRowForBufferRow(linesRange.start.row) - 1 precedingBufferRow = @bufferRowForScreenRow(precedingScreenRow) insertDelta = linesRange.start.row - precedingBufferRow # Any folds in the text that is moved will need to be re-created. # It includes the folds that were intersecting with the selection. rangesToRefold = selectionFoldRanges.concat( @outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) -> range = fold.getBufferRange() fold.destroy() range ).map (range) -> range.translate([-insertDelta, 0]) # # Make sure the inserted text doesn't go into an existing fold # if fold = @displayBuffer.largestFoldStartingAtBufferRow(precedingBufferRow) # rangesToRefold.push(fold.getBufferRange().translate([linesRange.getRowCount() - 1, 0])) # fold.destroy() # Delete lines spanned by selection and insert them on the preceding buffer row lines = @buffer.getTextInRange(linesRange) lines += @buffer.lineEndingForRow(linesRange.end.row - 1) unless lines[lines.length - 1] is '\n' @buffer.delete(linesRange) @buffer.insert([precedingBufferRow, 0], lines) # Restore folds that existed before the lines were moved for rangeToRefold in rangesToRefold @displayBuffer.foldBufferRowRange(rangeToRefold.start.row, rangeToRefold.end.row) for selection in selectionsToMove newSelectionRanges.push(selection.translate([-insertDelta, 0])) @setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true}) @autoIndentSelectedRows() if @shouldAutoIndent() @scrollToBufferPosition([newSelectionRanges[0].start.row, 0]) # Move lines intersecting the most recent selection or muiltiple selections # down by one row in screen coordinates. moveLineDown: -> selections = @getSelectedBufferRanges() selections.sort (a, b) -> a.compare(b) selections = selections.reverse() @transact => @consolidateSelections() newSelectionRanges = [] while selections.length > 0 # Find selections spanning a contiguous set of lines selection = selections.shift() selectionsToMove = [selection] # if the current selection start row matches the next selections' end row - make them one selection while selection.start.row is selections[0]?.end.row selectionsToMove.push(selections[0]) selection.start.row = selections[0].start.row selections.shift() # Compute the range spanned by all these selections... linesRangeStart = [selection.start.row, 0] if selection.end.row > selection.start.row and selection.end.column is 0 # Don't move the last line of a multi-line selection if the selection ends at column 0 linesRange = new Range(linesRangeStart, selection.end) else linesRange = new Range(linesRangeStart, [selection.end.row + 1, 0]) # If there's a fold containing either the starting row or the end row # of the selection then the whole fold needs to be moved and restored. # The initial fold range is stored and will be translated once the # insert delta is know. selectionFoldRanges = [] foldAtSelectionStart = @displayBuffer.largestFoldContainingBufferRow(selection.start.row) foldAtSelectionEnd = @displayBuffer.largestFoldContainingBufferRow(selection.end.row) if fold = foldAtSelectionStart ? foldAtSelectionEnd selectionFoldRanges.push range = fold.getBufferRange() newEndRow = range.end.row + 1 linesRange.end.row = newEndRow if newEndRow > linesRange.end.row fold.destroy() # If selected line range is followed by a fold, one line below on screen # could be multiple lines in the buffer. But at the same time, if the # next buffer row is wrapped, one line in the buffer can represent many # screen rows. followingScreenRow = @displayBuffer.lastScreenRowForBufferRow(linesRange.end.row) + 1 followingBufferRow = @bufferRowForScreenRow(followingScreenRow) insertDelta = followingBufferRow - linesRange.end.row # Any folds in the text that is moved will need to be re-created. # It includes the folds that were intersecting with the selection. rangesToRefold = selectionFoldRanges.concat( @outermostFoldsInBufferRowRange(linesRange.start.row, linesRange.end.row).map (fold) -> range = fold.getBufferRange() fold.destroy() range ).map (range) -> range.translate([insertDelta, 0]) # # Make sure the inserted text doesn't go into an existing fold # if fold = @displayBuffer.largestFoldStartingAtBufferRow(followingBufferRow) # rangesToRefold.push(fold.getBufferRange().translate([insertDelta - 1, 0])) # fold.destroy() # Delete lines spanned by selection and insert them on the following correct buffer row insertPosition = new Point(selection.translate([insertDelta, 0]).start.row, 0) lines = @buffer.getTextInRange(linesRange) if linesRange.end.row is @buffer.getLastRow() lines = "\n#{lines}" @buffer.delete(linesRange) @buffer.insert(insertPosition, lines) # Restore folds that existed before the lines were moved for rangeToRefold in rangesToRefold @displayBuffer.foldBufferRowRange(rangeToRefold.start.row, rangeToRefold.end.row) for selection in selectionsToMove newSelectionRanges.push(selection.translate([insertDelta, 0])) @setSelectedBufferRanges(newSelectionRanges, {autoscroll: false, preserveFolds: true}) @autoIndentSelectedRows() if @shouldAutoIndent() @scrollToBufferPosition([newSelectionRanges[0].start.row - 1, 0]) # Duplicate the most recent cursor's current line. duplicateLines: -> @transact => for selection in @getSelectionsOrderedByBufferPosition().reverse() selectedBufferRange = selection.getBufferRange() if selection.isEmpty() {start} = selection.getScreenRange() selection.selectToScreenPosition([start.row + 1, 0]) [startRow, endRow] = selection.getBufferRowRange() endRow++ foldedRowRanges = @outermostFoldsInBufferRowRange(startRow, endRow) .map (fold) -> fold.getBufferRowRange() rangeToDuplicate = [[startRow, 0], [endRow, 0]] textToDuplicate = @getTextInBufferRange(rangeToDuplicate) textToDuplicate = '\n' + textToDuplicate if endRow > @getLastBufferRow() @buffer.insert([endRow, 0], textToDuplicate) delta = endRow - startRow selection.setBufferRange(selectedBufferRange.translate([delta, 0])) for [foldStartRow, foldEndRow] in foldedRowRanges @foldBufferRowRange(foldStartRow + delta, foldEndRow + delta) return replaceSelectedText: (options={}, fn) -> {selectWordIfEmpty} = options @mutateSelectedText (selection) -> range = selection.getBufferRange() if selectWordIfEmpty and selection.isEmpty() selection.selectWord() text = selection.getText() selection.deleteSelectedText() selection.insertText(fn(text)) selection.setBufferRange(range) # Split multi-line selections into one selection per line. # # Operates on all selections. This method breaks apart all multi-line # selections to create multiple single-line selections that cumulatively cover # the same original area. splitSelectionsIntoLines: -> @mergeIntersectingSelections => for selection in @getSelections() range = selection.getBufferRange() continue if range.isSingleLine() {start, end} = range @addSelectionForBufferRange([start, [start.row, Infinity]]) {row} = start while ++row < end.row @addSelectionForBufferRange([[row, 0], [row, Infinity]]) @addSelectionForBufferRange([[end.row, 0], [end.row, end.column]]) unless end.column is 0 selection.destroy() return # Extended: For each selection, transpose the selected text. # # If the selection is empty, the characters preceding and following the cursor # are swapped. Otherwise, the selected characters are reversed. transpose: -> @mutateSelectedText (selection) -> if selection.isEmpty() selection.selectRight() text = selection.getText() selection.delete() selection.cursor.moveLeft() selection.insertText text else selection.insertText selection.getText().split('').reverse().join('') # Extended: Convert the selected text to upper case. # # For each selection, if the selection is empty, converts the containing word # to upper case. Otherwise convert the selected text to upper case. upperCase: -> @replaceSelectedText selectWordIfEmpty: true, (text) -> text.toUpperCase() # Extended: Convert the selected text to lower case. # # For each selection, if the selection is empty, converts the containing word # to upper case. Otherwise convert the selected text to upper case. lowerCase: -> @replaceSelectedText selectWordIfEmpty: true, (text) -> text.toLowerCase() # Extended: Toggle line comments for rows intersecting selections. # # If the current grammar doesn't support comments, does nothing. toggleLineCommentsInSelection: -> @mutateSelectedText (selection) -> selection.toggleLineComments() # Convert multiple lines to a single line. # # Operates on all selections. If the selection is empty, joins the current # line with the next line. Otherwise it joins all lines that intersect the # selection. # # Joining a line means that multiple lines are converted to a single line with # the contents of each of the original non-empty lines separated by a space. joinLines: -> @mutateSelectedText (selection) -> selection.joinLines() # Extended: For each cursor, insert a newline at beginning the following line. insertNewlineBelow: -> @transact => @moveToEndOfLine() @insertNewline() # Extended: For each cursor, insert a newline at the end of the preceding line. insertNewlineAbove: -> @transact => bufferRow = @getCursorBufferPosition().row indentLevel = @indentationForBufferRow(bufferRow) onFirstLine = bufferRow is 0 @moveToBeginningOfLine() @moveLeft() @insertNewline() if @shouldAutoIndent() and @indentationForBufferRow(bufferRow) < indentLevel @setIndentationForBufferRow(bufferRow, indentLevel) if onFirstLine @moveUp() @moveToEndOfLine() # Extended: For each selection, if the selection is empty, delete all characters # of the containing word that precede the cursor. Otherwise delete the # selected text. deleteToBeginningOfWord: -> @mutateSelectedText (selection) -> selection.deleteToBeginningOfWord() # Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the # previous word boundary. deleteToPreviousWordBoundary: -> @mutateSelectedText (selection) -> selection.deleteToPreviousWordBoundary() # Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the # next word boundary. deleteToNextWordBoundary: -> @mutateSelectedText (selection) -> selection.deleteToNextWordBoundary() # Extended: For each selection, if the selection is empty, delete all characters # of the containing subword following the cursor. Otherwise delete the selected # text. deleteToBeginningOfSubword: -> @mutateSelectedText (selection) -> selection.deleteToBeginningOfSubword() # Extended: For each selection, if the selection is empty, delete all characters # of the containing subword following the cursor. Otherwise delete the selected # text. deleteToEndOfSubword: -> @mutateSelectedText (selection) -> selection.deleteToEndOfSubword() # Extended: For each selection, if the selection is empty, delete all characters # of the containing line that precede the cursor. Otherwise delete the # selected text. deleteToBeginningOfLine: -> @mutateSelectedText (selection) -> selection.deleteToBeginningOfLine() # Extended: For each selection, if the selection is not empty, deletes the # selection; otherwise, deletes all characters of the containing line # following the cursor. If the cursor is already at the end of the line, # deletes the following newline. deleteToEndOfLine: -> @mutateSelectedText (selection) -> selection.deleteToEndOfLine() # Extended: For each selection, if the selection is empty, delete all characters # of the containing word following the cursor. Otherwise delete the selected # text. deleteToEndOfWord: -> @mutateSelectedText (selection) -> selection.deleteToEndOfWord() # Extended: Delete all lines intersecting selections. deleteLine: -> @mergeSelectionsOnSameRows() @mutateSelectedText (selection) -> selection.deleteLine() ### Section: History ### # Essential: Undo the last change. undo: -> @avoidMergingSelections => @buffer.undo() @getLastSelection().autoscroll() # Essential: Redo the last change. redo: -> @avoidMergingSelections => @buffer.redo() @getLastSelection().autoscroll() # Extended: Batch multiple operations as a single undo/redo step. # # Any group of operations that are logically grouped from the perspective of # undoing and redoing should be performed in a transaction. If you want to # abort the transaction, call {::abortTransaction} to terminate the function's # execution and revert any changes performed up to the abortion. # # * `groupingInterval` (optional) The {Number} of milliseconds for which this # transaction should be considered 'groupable' after it begins. If a transaction # with a positive `groupingInterval` is committed while the previous transaction is # still 'groupable', the two transactions are merged with respect to undo and redo. # * `fn` A {Function} to call inside the transaction. transact: (groupingInterval, fn) -> @buffer.transact(groupingInterval, fn) # Deprecated: Start an open-ended transaction. beginTransaction: (groupingInterval) -> Grim.deprecate('Transactions should be performed via TextEditor::transact only') @buffer.beginTransaction(groupingInterval) # Deprecated: Commit an open-ended transaction started with {::beginTransaction}. commitTransaction: -> Grim.deprecate('Transactions should be performed via TextEditor::transact only') @buffer.commitTransaction() # Extended: Abort an open transaction, undoing any operations performed so far # within the transaction. abortTransaction: -> @buffer.abortTransaction() # Extended: Create a pointer to the current state of the buffer for use # with {::revertToCheckpoint} and {::groupChangesSinceCheckpoint}. # # Returns a checkpoint value. createCheckpoint: -> @buffer.createCheckpoint() # Extended: Revert the buffer to the state it was in when the given # checkpoint was created. # # The redo stack will be empty following this operation, so changes since the # checkpoint will be lost. If the given checkpoint is no longer present in the # undo history, no changes will be made to the buffer and this method will # return `false`. # # Returns a {Boolean} indicating whether the operation succeeded. revertToCheckpoint: (checkpoint) -> @buffer.revertToCheckpoint(checkpoint) # Extended: Group all changes since the given checkpoint into a single # transaction for purposes of undo/redo. # # If the given checkpoint is no longer present in the undo history, no # grouping will be performed and this method will return `false`. # # Returns a {Boolean} indicating whether the operation succeeded. groupChangesSinceCheckpoint: (checkpoint) -> @buffer.groupChangesSinceCheckpoint(checkpoint) ### Section: TextEditor Coordinates ### # Essential: Convert a position in buffer-coordinates to screen-coordinates. # # The position is clipped via {::clipBufferPosition} prior to the conversion. # The position is also clipped via {::clipScreenPosition} following the # conversion, which only makes a difference when `options` are supplied. # # * `bufferPosition` A {Point} or {Array} of [row, column]. # * `options` (optional) An options hash for {::clipScreenPosition}. # # Returns a {Point}. screenPositionForBufferPosition: (bufferPosition, options) -> @displayLayer.translateBufferPosition(bufferPosition, options) # Essential: Convert a position in screen-coordinates to buffer-coordinates. # # The position is clipped via {::clipScreenPosition} prior to the conversion. # # * `bufferPosition` A {Point} or {Array} of [row, column]. # * `options` (optional) An options hash for {::clipScreenPosition}. # # Returns a {Point}. bufferPositionForScreenPosition: (screenPosition, options) -> @displayLayer.translateScreenPosition(screenPosition, options) # Essential: Convert a range in buffer-coordinates to screen-coordinates. # # * `bufferRange` {Range} in buffer coordinates to translate into screen coordinates. # # Returns a {Range}. screenRangeForBufferRange: (bufferRange) -> @displayLayer.translateBufferRange(bufferRange) # Essential: Convert a range in screen-coordinates to buffer-coordinates. # # * `screenRange` {Range} in screen coordinates to translate into buffer coordinates. # # Returns a {Range}. bufferRangeForScreenRange: (screenRange) -> @displayLayer.translateScreenRange(screenRange) # Extended: Clip the given {Point} to a valid position in the buffer. # # If the given {Point} describes a position that is actually reachable by the # cursor based on the current contents of the buffer, it is returned # unchanged. If the {Point} does not describe a valid position, the closest # valid position is returned instead. # # ## Examples # # ```coffee # editor.clipBufferPosition([-1, -1]) # -> `[0, 0]` # # # When the line at buffer row 2 is 10 characters long # editor.clipBufferPosition([2, Infinity]) # -> `[2, 10]` # ``` # # * `bufferPosition` The {Point} representing the position to clip. # # Returns a {Point}. clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition) # Extended: Clip the start and end of the given range to valid positions in the # buffer. See {::clipBufferPosition} for more information. # # * `range` The {Range} to clip. # # Returns a {Range}. clipBufferRange: (range) -> @buffer.clipRange(range) # Extended: Clip the given {Point} to a valid position on screen. # # If the given {Point} describes a position that is actually reachable by the # cursor based on the current contents of the screen, it is returned # unchanged. If the {Point} does not describe a valid position, the closest # valid position is returned instead. # # ## Examples # # ```coffee # editor.clipScreenPosition([-1, -1]) # -> `[0, 0]` # # # When the line at screen row 2 is 10 characters long # editor.clipScreenPosition([2, Infinity]) # -> `[2, 10]` # ``` # # * `screenPosition` The {Point} representing the position to clip. # * `options` (optional) {Object} # * `clipDirection` {String} If `'backward'`, returns the first valid # position preceding an invalid position. If `'forward'`, returns the # first valid position following a valid position. Defaults to # `'backward'`. # # Returns a {Point}. clipScreenPosition: (screenPosition, options) -> @displayLayer.clipScreenPosition(screenPosition, options) # Extended: Clip the start and end of the given range to valid positions on screen. # See {::clipScreenPosition} for more information. # # * `range` The {Range} to clip. # * `options` (optional) See {::clipScreenPosition} `options`. # # Returns a {Range}. clipScreenRange: (screenRange, options) -> screenRange = Range.fromObject(screenRange) start = @displayLayer.clipScreenPosition(screenRange.start, options) end = @displayLayer.clipScreenPosition(screenRange.end, options) Range(start, end) ### Section: Decorations ### # Essential: Add a decoration that tracks a {DisplayMarker}. When the # marker moves, is invalidated, or is destroyed, the decoration will be # updated to reflect the marker's state. # # The following are the supported decorations types: # # * __line__: Adds your CSS `class` to the line nodes within the range # marked by the marker # * __line-number__: Adds your CSS `class` to the line number nodes within the # range marked by the marker # * __highlight__: Adds a new highlight div to the editor surrounding the # range marked by the marker. When the user selects text, the selection is # visualized with a highlight decoration internally. The structure of this # highlight will be # ```html #
# #
#
# ``` # * __overlay__: Positions the view associated with the given item at the head # or tail of the given `DisplayMarker`. # * __gutter__: A decoration that tracks a {DisplayMarker} in a {Gutter}. Gutter # decorations are created by calling {Gutter::decorateMarker} on the # desired `Gutter` instance. # * __block__: Positions the view associated with the given item before or # after the row of the given `TextEditorMarker`. # # ## Arguments # # * `marker` A {DisplayMarker} you want this decoration to follow. # * `decorationParams` An {Object} representing the decoration e.g. # `{type: 'line-number', class: 'linter-error'}` # * `type` There are several supported decoration types. The behavior of the # types are as follows: # * `line` Adds the given `class` to the lines overlapping the rows # spanned by the `DisplayMarker`. # * `line-number` Adds the given `class` to the line numbers overlapping # the rows spanned by the `DisplayMarker`. # * `highlight` Creates a `.highlight` div with the nested class with up # to 3 nested regions that fill the area spanned by the `DisplayMarker`. # * `overlay` Positions the view associated with the given item at the # head or tail of the given `DisplayMarker`, depending on the `position` # property. # * `gutter` Tracks a {DisplayMarker} in a {Gutter}. Created by calling # {Gutter::decorateMarker} on the desired `Gutter` instance. # * `block` Positions the view associated with the given item before or # after the row of the given `TextEditorMarker`, depending on the `position` # property. # * `class` This CSS class will be applied to the decorated line number, # line, highlight, or overlay. # * `item` (optional) An {HTMLElement} or a model {Object} with a # corresponding view registered. Only applicable to the `gutter`, # `overlay` and `block` types. # * `onlyHead` (optional) If `true`, the decoration will only be applied to # the head of the `DisplayMarker`. Only applicable to the `line` and # `line-number` types. # * `onlyEmpty` (optional) If `true`, the decoration will only be applied if # the associated `DisplayMarker` is empty. Only applicable to the `gutter`, # `line`, and `line-number` types. # * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied # if the associated `DisplayMarker` is non-empty. Only applicable to the # `gutter`, `line`, and `line-number` types. # * `position` (optional) Only applicable to decorations of type `overlay` and `block`, # controls where the view is positioned relative to the `TextEditorMarker`. # Values can be `'head'` (the default) or `'tail'` for overlay decorations, and # `'before'` (the default) or `'after'` for block decorations. # # Returns a {Decoration} object decorateMarker: (marker, decorationParams) -> @displayBuffer.decorateMarker(marker, decorationParams) # Essential: *Experimental:* Add a decoration to every marker in the given # marker layer. Can be used to decorate a large number of markers without # having to create and manage many individual decorations. # # * `markerLayer` A {DisplayMarkerLayer} or {MarkerLayer} to decorate. # * `decorationParams` The same parameters that are passed to # {decorateMarker}, except the `type` cannot be `overlay` or `gutter`. # # This API is experimental and subject to change on any release. # # Returns a {LayerDecoration}. decorateMarkerLayer: (markerLayer, decorationParams) -> @displayBuffer.decorateMarkerLayer(markerLayer, decorationParams) # Deprecated: Get all the decorations within a screen row range on the default # layer. # # * `startScreenRow` the {Number} beginning screen row # * `endScreenRow` the {Number} end screen row (inclusive) # # Returns an {Object} of decorations in the form # `{1: [{id: 10, type: 'line-number', class: 'someclass'}], 2: ...}` # where the keys are {DisplayMarker} IDs, and the values are an array of decoration # params objects attached to the marker. # Returns an empty object when no decorations are found decorationsForScreenRowRange: (startScreenRow, endScreenRow) -> @displayBuffer.decorationsForScreenRowRange(startScreenRow, endScreenRow) decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) -> @displayBuffer.decorationsStateForScreenRowRange(startScreenRow, endScreenRow) # Extended: Get all decorations. # # * `propertyFilter` (optional) An {Object} containing key value pairs that # the returned decorations' properties must match. # # Returns an {Array} of {Decoration}s. getDecorations: (propertyFilter) -> @displayBuffer.getDecorations(propertyFilter) # Extended: Get all decorations of type 'line'. # # * `propertyFilter` (optional) An {Object} containing key value pairs that # the returned decorations' properties must match. # # Returns an {Array} of {Decoration}s. getLineDecorations: (propertyFilter) -> @displayBuffer.getLineDecorations(propertyFilter) # Extended: Get all decorations of type 'line-number'. # # * `propertyFilter` (optional) An {Object} containing key value pairs that # the returned decorations' properties must match. # # Returns an {Array} of {Decoration}s. getLineNumberDecorations: (propertyFilter) -> @displayBuffer.getLineNumberDecorations(propertyFilter) # Extended: Get all decorations of type 'highlight'. # # * `propertyFilter` (optional) An {Object} containing key value pairs that # the returned decorations' properties must match. # # Returns an {Array} of {Decoration}s. getHighlightDecorations: (propertyFilter) -> @displayBuffer.getHighlightDecorations(propertyFilter) # Extended: Get all decorations of type 'overlay'. # # * `propertyFilter` (optional) An {Object} containing key value pairs that # the returned decorations' properties must match. # # Returns an {Array} of {Decoration}s. getOverlayDecorations: (propertyFilter) -> @displayBuffer.getOverlayDecorations(propertyFilter) decorationForId: (id) -> @displayBuffer.decorationForId(id) decorationsForMarkerId: (id) -> @displayBuffer.decorationsForMarkerId(id) ### Section: Markers ### # Essential: Create a marker on the default marker layer with the given range # in buffer coordinates. This marker will maintain its logical location as the # buffer is changed, so if you mark a particular word, the marker will remain # over that word even if the word's location in the buffer changes. # # * `range` A {Range} or range-compatible {Array} # * `properties` A hash of key-value pairs to associate with the marker. There # are also reserved property names that have marker-specific meaning. # * `maintainHistory` (optional) {Boolean} Whether to store this marker's # range before and after each change in the undo history. This allows the # marker's position to be restored more accurately for certain undo/redo # operations, but uses more time and memory. (default: false) # * `reversed` (optional) {Boolean} Creates the marker in a reversed # orientation. (default: false) # * `persistent` (optional) {Boolean} Whether to include this marker when # serializing the buffer. (default: true) # * `invalidate` (optional) {String} Determines the rules by which changes # to the buffer *invalidate* the marker. (default: 'overlap') It can be # any of the following strategies, in order of fragility: # * __never__: The marker is never marked as invalid. This is a good choice for # markers representing selections in an editor. # * __surround__: The marker is invalidated by changes that completely surround it. # * __overlap__: The marker is invalidated by changes that surround the # start or end of the marker. This is the default. # * __inside__: The marker is invalidated by changes that extend into the # inside of the marker. Changes that end at the marker's start or # start at the marker's end do not invalidate the marker. # * __touch__: The marker is invalidated by a change that touches the marked # region in any way, including changes that end at the marker's # start or start at the marker's end. This is the most fragile strategy. # # Returns a {DisplayMarker}. markBufferRange: (args...) -> @displayBuffer.markBufferRange(args...) # Essential: Create a marker on the default marker layer with the given range # in screen coordinates. This marker will maintain its logical location as the # buffer is changed, so if you mark a particular word, the marker will remain # over that word even if the word's location in the buffer changes. # # * `range` A {Range} or range-compatible {Array} # * `properties` A hash of key-value pairs to associate with the marker. There # are also reserved property names that have marker-specific meaning. # * `maintainHistory` (optional) {Boolean} Whether to store this marker's # range before and after each change in the undo history. This allows the # marker's position to be restored more accurately for certain undo/redo # operations, but uses more time and memory. (default: false) # * `reversed` (optional) {Boolean} Creates the marker in a reversed # orientation. (default: false) # * `persistent` (optional) {Boolean} Whether to include this marker when # serializing the buffer. (default: true) # * `invalidate` (optional) {String} Determines the rules by which changes # to the buffer *invalidate* the marker. (default: 'overlap') It can be # any of the following strategies, in order of fragility: # * __never__: The marker is never marked as invalid. This is a good choice for # markers representing selections in an editor. # * __surround__: The marker is invalidated by changes that completely surround it. # * __overlap__: The marker is invalidated by changes that surround the # start or end of the marker. This is the default. # * __inside__: The marker is invalidated by changes that extend into the # inside of the marker. Changes that end at the marker's start or # start at the marker's end do not invalidate the marker. # * __touch__: The marker is invalidated by a change that touches the marked # region in any way, including changes that end at the marker's # start or start at the marker's end. This is the most fragile strategy. # # Returns a {DisplayMarker}. markScreenRange: (args...) -> @displayBuffer.markScreenRange(args...) # Essential: Mark the given position in buffer coordinates on the default # marker layer. # # * `position` A {Point} or {Array} of `[row, column]`. # * `options` (optional) See {TextBuffer::markRange}. # # Returns a {DisplayMarker}. markBufferPosition: (args...) -> @displayBuffer.markBufferPosition(args...) # Essential: Mark the given position in screen coordinates on the default # marker layer. # # * `position` A {Point} or {Array} of `[row, column]`. # * `options` (optional) See {TextBuffer::markRange}. # # Returns a {DisplayMarker}. markScreenPosition: (args...) -> @displayBuffer.markScreenPosition(args...) # Essential: Find all {DisplayMarker}s on the default marker layer that # match the given properties. # # This method finds markers based on the given properties. Markers can be # associated with custom properties that will be compared with basic equality. # In addition, there are several special properties that will be compared # with the range of the markers rather than their properties. # # * `properties` An {Object} containing properties that each returned marker # must satisfy. Markers can be associated with custom properties, which are # compared with basic equality. In addition, several reserved properties # can be used to filter markers based on their current range: # * `startBufferRow` Only include markers starting at this row in buffer # coordinates. # * `endBufferRow` Only include markers ending at this row in buffer # coordinates. # * `containsBufferRange` Only include markers containing this {Range} or # in range-compatible {Array} in buffer coordinates. # * `containsBufferPosition` Only include markers containing this {Point} # or {Array} of `[row, column]` in buffer coordinates. findMarkers: (properties) -> @displayBuffer.findMarkers(properties) # Extended: Get the {DisplayMarker} on the default layer for the given # marker id. # # * `id` {Number} id of the marker getMarker: (id) -> @displayBuffer.getMarker(id) # Extended: Get all {DisplayMarker}s on the default marker layer. Consider # using {::findMarkers} getMarkers: -> @displayBuffer.getMarkers() # Extended: Get the number of markers in the default marker layer. # # Returns a {Number}. getMarkerCount: -> @buffer.getMarkerCount() destroyMarker: (id) -> @getMarker(id)?.destroy() # Extended: *Experimental:* Create a marker layer to group related markers. # # * `options` An {Object} containing the following keys: # * `maintainHistory` A {Boolean} indicating whether marker state should be # restored on undo/redo. Defaults to `false`. # # This API is experimental and subject to change on any release. # # Returns a {DisplayMarkerLayer}. addMarkerLayer: (options) -> @displayBuffer.addMarkerLayer(options) # Public: *Experimental:* Get a {DisplayMarkerLayer} by id. # # * `id` The id of the marker layer to retrieve. # # This API is experimental and subject to change on any release. # # Returns a {MarkerLayer} or `undefined` if no layer exists with the given # id. getMarkerLayer: (id) -> @displayBuffer.getMarkerLayer(id) # Public: *Experimental:* Get the default {DisplayMarkerLayer}. # # All marker APIs not tied to an explicit layer interact with this default # layer. # # This API is experimental and subject to change on any release. # # Returns a {DisplayMarkerLayer}. getDefaultMarkerLayer: -> @displayBuffer.getDefaultMarkerLayer() ### Section: Cursors ### # Essential: Get the position of the most recently added cursor in buffer # coordinates. # # Returns a {Point} getCursorBufferPosition: -> @getLastCursor().getBufferPosition() # Essential: Get the position of all the cursor positions in buffer coordinates. # # Returns {Array} of {Point}s in the order they were added getCursorBufferPositions: -> cursor.getBufferPosition() for cursor in @getCursors() # Essential: Move the cursor to the given position in buffer coordinates. # # If there are multiple cursors, they will be consolidated to a single cursor. # # * `position` A {Point} or {Array} of `[row, column]` # * `options` (optional) An {Object} combining options for {::clipScreenPosition} with: # * `autoscroll` Determines whether the editor scrolls to the new cursor's # position. Defaults to true. setCursorBufferPosition: (position, options) -> @moveCursors (cursor) -> cursor.setBufferPosition(position, options) # Essential: Get a {Cursor} at given screen coordinates {Point} # # * `position` A {Point} or {Array} of `[row, column]` # # Returns the first matched {Cursor} or undefined getCursorAtScreenPosition: (position) -> for cursor in @cursors return cursor if cursor.getScreenPosition().isEqual(position) undefined # Essential: Get the position of the most recently added cursor in screen # coordinates. # # Returns a {Point}. getCursorScreenPosition: -> @getLastCursor().getScreenPosition() # Essential: Get the position of all the cursor positions in screen coordinates. # # Returns {Array} of {Point}s in the order the cursors were added getCursorScreenPositions: -> cursor.getScreenPosition() for cursor in @getCursors() # Essential: Move the cursor to the given position in screen coordinates. # # If there are multiple cursors, they will be consolidated to a single cursor. # # * `position` A {Point} or {Array} of `[row, column]` # * `options` (optional) An {Object} combining options for {::clipScreenPosition} with: # * `autoscroll` Determines whether the editor scrolls to the new cursor's # position. Defaults to true. setCursorScreenPosition: (position, options) -> @moveCursors (cursor) -> cursor.setScreenPosition(position, options) # Essential: Add a cursor at the given position in buffer coordinates. # # * `bufferPosition` A {Point} or {Array} of `[row, column]` # # Returns a {Cursor}. addCursorAtBufferPosition: (bufferPosition, options) -> @selectionsMarkerLayer.markBufferPosition(bufferPosition, @getSelectionMarkerAttributes()) @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false @getLastSelection().cursor # Essential: Add a cursor at the position in screen coordinates. # # * `screenPosition` A {Point} or {Array} of `[row, column]` # # Returns a {Cursor}. addCursorAtScreenPosition: (screenPosition, options) -> @selectionsMarkerLayer.markScreenPosition(screenPosition, @getSelectionMarkerAttributes()) @getLastSelection().cursor.autoscroll() unless options?.autoscroll is false @getLastSelection().cursor # Essential: Returns {Boolean} indicating whether or not there are multiple cursors. hasMultipleCursors: -> @getCursors().length > 1 # Essential: Move every cursor up one row in screen coordinates. # # * `lineCount` (optional) {Number} number of lines to move moveUp: (lineCount) -> @moveCursors (cursor) -> cursor.moveUp(lineCount, moveToEndOfSelection: true) # Essential: Move every cursor down one row in screen coordinates. # # * `lineCount` (optional) {Number} number of lines to move moveDown: (lineCount) -> @moveCursors (cursor) -> cursor.moveDown(lineCount, moveToEndOfSelection: true) # Essential: Move every cursor left one column. # # * `columnCount` (optional) {Number} number of columns to move (default: 1) moveLeft: (columnCount) -> @moveCursors (cursor) -> cursor.moveLeft(columnCount, moveToEndOfSelection: true) # Essential: Move every cursor right one column. # # * `columnCount` (optional) {Number} number of columns to move (default: 1) moveRight: (columnCount) -> @moveCursors (cursor) -> cursor.moveRight(columnCount, moveToEndOfSelection: true) # Essential: Move every cursor to the beginning of its line in buffer coordinates. moveToBeginningOfLine: -> @moveCursors (cursor) -> cursor.moveToBeginningOfLine() # Essential: Move every cursor to the beginning of its line in screen coordinates. moveToBeginningOfScreenLine: -> @moveCursors (cursor) -> cursor.moveToBeginningOfScreenLine() # Essential: Move every cursor to the first non-whitespace character of its line. moveToFirstCharacterOfLine: -> @moveCursors (cursor) -> cursor.moveToFirstCharacterOfLine() # Essential: Move every cursor to the end of its line in buffer coordinates. moveToEndOfLine: -> @moveCursors (cursor) -> cursor.moveToEndOfLine() # Essential: Move every cursor to the end of its line in screen coordinates. moveToEndOfScreenLine: -> @moveCursors (cursor) -> cursor.moveToEndOfScreenLine() # Essential: Move every cursor to the beginning of its surrounding word. moveToBeginningOfWord: -> @moveCursors (cursor) -> cursor.moveToBeginningOfWord() # Essential: Move every cursor to the end of its surrounding word. moveToEndOfWord: -> @moveCursors (cursor) -> cursor.moveToEndOfWord() # Cursor Extended # Extended: Move every cursor to the top of the buffer. # # If there are multiple cursors, they will be merged into a single cursor. moveToTop: -> @moveCursors (cursor) -> cursor.moveToTop() # Extended: Move every cursor to the bottom of the buffer. # # If there are multiple cursors, they will be merged into a single cursor. moveToBottom: -> @moveCursors (cursor) -> cursor.moveToBottom() # Extended: Move every cursor to the beginning of the next word. moveToBeginningOfNextWord: -> @moveCursors (cursor) -> cursor.moveToBeginningOfNextWord() # Extended: Move every cursor to the previous word boundary. moveToPreviousWordBoundary: -> @moveCursors (cursor) -> cursor.moveToPreviousWordBoundary() # Extended: Move every cursor to the next word boundary. moveToNextWordBoundary: -> @moveCursors (cursor) -> cursor.moveToNextWordBoundary() # Extended: Move every cursor to the previous subword boundary. moveToPreviousSubwordBoundary: -> @moveCursors (cursor) -> cursor.moveToPreviousSubwordBoundary() # Extended: Move every cursor to the next subword boundary. moveToNextSubwordBoundary: -> @moveCursors (cursor) -> cursor.moveToNextSubwordBoundary() # Extended: Move every cursor to the beginning of the next paragraph. moveToBeginningOfNextParagraph: -> @moveCursors (cursor) -> cursor.moveToBeginningOfNextParagraph() # Extended: Move every cursor to the beginning of the previous paragraph. moveToBeginningOfPreviousParagraph: -> @moveCursors (cursor) -> cursor.moveToBeginningOfPreviousParagraph() # Extended: Returns the most recently added {Cursor} getLastCursor: -> @createLastSelectionIfNeeded() _.last(@cursors) # Extended: Returns the word surrounding the most recently added cursor. # # * `options` (optional) See {Cursor::getBeginningOfCurrentWordBufferPosition}. getWordUnderCursor: (options) -> @getTextInBufferRange(@getLastCursor().getCurrentWordBufferRange(options)) # Extended: Get an Array of all {Cursor}s. getCursors: -> @createLastSelectionIfNeeded() @cursors.slice() # Extended: Get all {Cursors}s, ordered by their position in the buffer # instead of the order in which they were added. # # Returns an {Array} of {Selection}s. getCursorsOrderedByBufferPosition: -> @getCursors().sort (a, b) -> a.compare(b) cursorsForScreenRowRange: (startScreenRow, endScreenRow) -> cursors = [] for marker in @selectionsMarkerLayer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) if cursor = @cursorsByMarkerId.get(marker.id) cursors.push(cursor) cursors # Add a cursor based on the given {DisplayMarker}. addCursor: (marker) -> cursor = new Cursor(editor: this, marker: marker, config: @config) @cursors.push(cursor) @cursorsByMarkerId.set(marker.id, cursor) @decorateMarker(marker, type: 'line-number', class: 'cursor-line') @decorateMarker(marker, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true) @decorateMarker(marker, type: 'line', class: 'cursor-line', onlyEmpty: true) @emitter.emit 'did-add-cursor', cursor cursor moveCursors: (fn) -> fn(cursor) for cursor in @getCursors() @mergeCursors() cursorMoved: (event) -> @emitter.emit 'did-change-cursor-position', event # Merge cursors that have the same screen position mergeCursors: -> positions = {} for cursor in @getCursors() position = cursor.getBufferPosition().toString() if positions.hasOwnProperty(position) cursor.destroy() else positions[position] = true return preserveCursorPositionOnBufferReload: -> cursorPosition = null @disposables.add @buffer.onWillReload => cursorPosition = @getCursorBufferPosition() @disposables.add @buffer.onDidReload => @setCursorBufferPosition(cursorPosition) if cursorPosition cursorPosition = null ### Section: Selections ### # Essential: Get the selected text of the most recently added selection. # # Returns a {String}. getSelectedText: -> @getLastSelection().getText() # Essential: Get the {Range} of the most recently added selection in buffer # coordinates. # # Returns a {Range}. getSelectedBufferRange: -> @getLastSelection().getBufferRange() # Essential: Get the {Range}s of all selections in buffer coordinates. # # The ranges are sorted by when the selections were added. Most recent at the end. # # Returns an {Array} of {Range}s. getSelectedBufferRanges: -> selection.getBufferRange() for selection in @getSelections() # Essential: Set the selected range in buffer coordinates. If there are multiple # selections, they are reduced to a single selection with the given range. # # * `bufferRange` A {Range} or range-compatible {Array}. # * `options` (optional) An options {Object}: # * `reversed` A {Boolean} indicating whether to create the selection in a # reversed orientation. # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the # selection is set. setSelectedBufferRange: (bufferRange, options) -> @setSelectedBufferRanges([bufferRange], options) # Essential: Set the selected ranges in buffer coordinates. If there are multiple # selections, they are replaced by new selections with the given ranges. # # * `bufferRanges` An {Array} of {Range}s or range-compatible {Array}s. # * `options` (optional) An options {Object}: # * `reversed` A {Boolean} indicating whether to create the selection in a # reversed orientation. # * `preserveFolds` A {Boolean}, which if `true` preserves the fold settings after the # selection is set. setSelectedBufferRanges: (bufferRanges, options={}) -> throw new Error("Passed an empty array to setSelectedBufferRanges") unless bufferRanges.length selections = @getSelections() selection.destroy() for selection in selections[bufferRanges.length...] @mergeIntersectingSelections options, => for bufferRange, i in bufferRanges bufferRange = Range.fromObject(bufferRange) if selections[i] selections[i].setBufferRange(bufferRange, options) else @addSelectionForBufferRange(bufferRange, options) return # Essential: Get the {Range} of the most recently added selection in screen # coordinates. # # Returns a {Range}. getSelectedScreenRange: -> @getLastSelection().getScreenRange() # Essential: Get the {Range}s of all selections in screen coordinates. # # The ranges are sorted by when the selections were added. Most recent at the end. # # Returns an {Array} of {Range}s. getSelectedScreenRanges: -> selection.getScreenRange() for selection in @getSelections() # Essential: Set the selected range in screen coordinates. If there are multiple # selections, they are reduced to a single selection with the given range. # # * `screenRange` A {Range} or range-compatible {Array}. # * `options` (optional) An options {Object}: # * `reversed` A {Boolean} indicating whether to create the selection in a # reversed orientation. setSelectedScreenRange: (screenRange, options) -> @setSelectedBufferRange(@bufferRangeForScreenRange(screenRange, options), options) # Essential: Set the selected ranges in screen coordinates. If there are multiple # selections, they are replaced by new selections with the given ranges. # # * `screenRanges` An {Array} of {Range}s or range-compatible {Array}s. # * `options` (optional) An options {Object}: # * `reversed` A {Boolean} indicating whether to create the selection in a # reversed orientation. setSelectedScreenRanges: (screenRanges, options={}) -> throw new Error("Passed an empty array to setSelectedScreenRanges") unless screenRanges.length selections = @getSelections() selection.destroy() for selection in selections[screenRanges.length...] @mergeIntersectingSelections options, => for screenRange, i in screenRanges screenRange = Range.fromObject(screenRange) if selections[i] selections[i].setScreenRange(screenRange, options) else @addSelectionForScreenRange(screenRange, options) return # Essential: Add a selection for the given range in buffer coordinates. # # * `bufferRange` A {Range} # * `options` (optional) An options {Object}: # * `reversed` A {Boolean} indicating whether to create the selection in a # reversed orientation. # # Returns the added {Selection}. addSelectionForBufferRange: (bufferRange, options={}) -> @selectionsMarkerLayer.markBufferRange(bufferRange, _.defaults(@getSelectionMarkerAttributes(), options)) @getLastSelection().autoscroll() unless options.autoscroll is false @getLastSelection() # Essential: Add a selection for the given range in screen coordinates. # # * `screenRange` A {Range} # * `options` (optional) An options {Object}: # * `reversed` A {Boolean} indicating whether to create the selection in a # reversed orientation. # # Returns the added {Selection}. addSelectionForScreenRange: (screenRange, options={}) -> @selectionsMarkerLayer.markScreenRange(screenRange, _.defaults(@getSelectionMarkerAttributes(), options)) @getLastSelection().autoscroll() unless options.autoscroll is false @getLastSelection() # Essential: Select from the current cursor position to the given position in # buffer coordinates. # # This method may merge selections that end up intesecting. # # * `position` An instance of {Point}, with a given `row` and `column`. selectToBufferPosition: (position) -> lastSelection = @getLastSelection() lastSelection.selectToBufferPosition(position) @mergeIntersectingSelections(reversed: lastSelection.isReversed()) # Essential: Select from the current cursor position to the given position in # screen coordinates. # # This method may merge selections that end up intesecting. # # * `position` An instance of {Point}, with a given `row` and `column`. selectToScreenPosition: (position, options) -> lastSelection = @getLastSelection() lastSelection.selectToScreenPosition(position, options) unless options?.suppressSelectionMerge @mergeIntersectingSelections(reversed: lastSelection.isReversed()) # Essential: Move the cursor of each selection one character upward while # preserving the selection's tail position. # # * `rowCount` (optional) {Number} number of rows to select (default: 1) # # This method may merge selections that end up intesecting. selectUp: (rowCount) -> @expandSelectionsBackward (selection) -> selection.selectUp(rowCount) # Essential: Move the cursor of each selection one character downward while # preserving the selection's tail position. # # * `rowCount` (optional) {Number} number of rows to select (default: 1) # # This method may merge selections that end up intesecting. selectDown: (rowCount) -> @expandSelectionsForward (selection) -> selection.selectDown(rowCount) # Essential: Move the cursor of each selection one character leftward while # preserving the selection's tail position. # # * `columnCount` (optional) {Number} number of columns to select (default: 1) # # This method may merge selections that end up intesecting. selectLeft: (columnCount) -> @expandSelectionsBackward (selection) -> selection.selectLeft(columnCount) # Essential: Move the cursor of each selection one character rightward while # preserving the selection's tail position. # # * `columnCount` (optional) {Number} number of columns to select (default: 1) # # This method may merge selections that end up intesecting. selectRight: (columnCount) -> @expandSelectionsForward (selection) -> selection.selectRight(columnCount) # Essential: Select from the top of the buffer to the end of the last selection # in the buffer. # # This method merges multiple selections into a single selection. selectToTop: -> @expandSelectionsBackward (selection) -> selection.selectToTop() # Essential: Selects from the top of the first selection in the buffer to the end # of the buffer. # # This method merges multiple selections into a single selection. selectToBottom: -> @expandSelectionsForward (selection) -> selection.selectToBottom() # Essential: Select all text in the buffer. # # This method merges multiple selections into a single selection. selectAll: -> @expandSelectionsForward (selection) -> selection.selectAll() # Essential: Move the cursor of each selection to the beginning of its line # while preserving the selection's tail position. # # This method may merge selections that end up intesecting. selectToBeginningOfLine: -> @expandSelectionsBackward (selection) -> selection.selectToBeginningOfLine() # Essential: Move the cursor of each selection to the first non-whitespace # character of its line while preserving the selection's tail position. If the # cursor is already on the first character of the line, move it to the # beginning of the line. # # This method may merge selections that end up intersecting. selectToFirstCharacterOfLine: -> @expandSelectionsBackward (selection) -> selection.selectToFirstCharacterOfLine() # Essential: Move the cursor of each selection to the end of its line while # preserving the selection's tail position. # # This method may merge selections that end up intersecting. selectToEndOfLine: -> @expandSelectionsForward (selection) -> selection.selectToEndOfLine() # Essential: Expand selections to the beginning of their containing word. # # Operates on all selections. Moves the cursor to the beginning of the # containing word while preserving the selection's tail position. selectToBeginningOfWord: -> @expandSelectionsBackward (selection) -> selection.selectToBeginningOfWord() # Essential: Expand selections to the end of their containing word. # # Operates on all selections. Moves the cursor to the end of the containing # word while preserving the selection's tail position. selectToEndOfWord: -> @expandSelectionsForward (selection) -> selection.selectToEndOfWord() # Extended: For each selection, move its cursor to the preceding subword # boundary while maintaining the selection's tail position. # # This method may merge selections that end up intersecting. selectToPreviousSubwordBoundary: -> @expandSelectionsBackward (selection) -> selection.selectToPreviousSubwordBoundary() # Extended: For each selection, move its cursor to the next subword boundary # while maintaining the selection's tail position. # # This method may merge selections that end up intersecting. selectToNextSubwordBoundary: -> @expandSelectionsForward (selection) -> selection.selectToNextSubwordBoundary() # Essential: For each cursor, select the containing line. # # This method merges selections on successive lines. selectLinesContainingCursors: -> @expandSelectionsForward (selection) -> selection.selectLine() # Essential: Select the word surrounding each cursor. selectWordsContainingCursors: -> @expandSelectionsForward (selection) -> selection.selectWord() # Selection Extended # Extended: For each selection, move its cursor to the preceding word boundary # while maintaining the selection's tail position. # # This method may merge selections that end up intersecting. selectToPreviousWordBoundary: -> @expandSelectionsBackward (selection) -> selection.selectToPreviousWordBoundary() # Extended: For each selection, move its cursor to the next word boundary while # maintaining the selection's tail position. # # This method may merge selections that end up intersecting. selectToNextWordBoundary: -> @expandSelectionsForward (selection) -> selection.selectToNextWordBoundary() # Extended: Expand selections to the beginning of the next word. # # Operates on all selections. Moves the cursor to the beginning of the next # word while preserving the selection's tail position. selectToBeginningOfNextWord: -> @expandSelectionsForward (selection) -> selection.selectToBeginningOfNextWord() # Extended: Expand selections to the beginning of the next paragraph. # # Operates on all selections. Moves the cursor to the beginning of the next # paragraph while preserving the selection's tail position. selectToBeginningOfNextParagraph: -> @expandSelectionsForward (selection) -> selection.selectToBeginningOfNextParagraph() # Extended: Expand selections to the beginning of the next paragraph. # # Operates on all selections. Moves the cursor to the beginning of the next # paragraph while preserving the selection's tail position. selectToBeginningOfPreviousParagraph: -> @expandSelectionsBackward (selection) -> selection.selectToBeginningOfPreviousParagraph() # Extended: Select the range of the given marker if it is valid. # # * `marker` A {DisplayMarker} # # Returns the selected {Range} or `undefined` if the marker is invalid. selectMarker: (marker) -> if marker.isValid() range = marker.getBufferRange() @setSelectedBufferRange(range) range # Extended: Get the most recently added {Selection}. # # Returns a {Selection}. getLastSelection: -> @createLastSelectionIfNeeded() _.last(@selections) # Extended: Get current {Selection}s. # # Returns: An {Array} of {Selection}s. getSelections: -> @createLastSelectionIfNeeded() @selections.slice() # Extended: Get all {Selection}s, ordered by their position in the buffer # instead of the order in which they were added. # # Returns an {Array} of {Selection}s. getSelectionsOrderedByBufferPosition: -> @getSelections().sort (a, b) -> a.compare(b) # Extended: Determine if a given range in buffer coordinates intersects a # selection. # # * `bufferRange` A {Range} or range-compatible {Array}. # # Returns a {Boolean}. selectionIntersectsBufferRange: (bufferRange) -> _.any @getSelections(), (selection) -> selection.intersectsBufferRange(bufferRange) # Selections Private # Add a similarly-shaped selection to the next eligible line below # each selection. # # Operates on all selections. If the selection is empty, adds an empty # selection to the next following non-empty line as close to the current # selection's column as possible. If the selection is non-empty, adds a # selection to the next line that is long enough for a non-empty selection # starting at the same column as the current selection to be added to it. addSelectionBelow: -> @expandSelectionsForward (selection) -> selection.addSelectionBelow() # Add a similarly-shaped selection to the next eligible line above # each selection. # # Operates on all selections. If the selection is empty, adds an empty # selection to the next preceding non-empty line as close to the current # selection's column as possible. If the selection is non-empty, adds a # selection to the next line that is long enough for a non-empty selection # starting at the same column as the current selection to be added to it. addSelectionAbove: -> @expandSelectionsBackward (selection) -> selection.addSelectionAbove() # Calls the given function with each selection, then merges selections expandSelectionsForward: (fn) -> @mergeIntersectingSelections => fn(selection) for selection in @getSelections() return # Calls the given function with each selection, then merges selections in the # reversed orientation expandSelectionsBackward: (fn) -> @mergeIntersectingSelections reversed: true, => fn(selection) for selection in @getSelections() return finalizeSelections: -> selection.finalize() for selection in @getSelections() return selectionsForScreenRows: (startRow, endRow) -> @getSelections().filter (selection) -> selection.intersectsScreenRowRange(startRow, endRow) # Merges intersecting selections. If passed a function, it executes # the function with merging suppressed, then merges intersecting selections # afterward. mergeIntersectingSelections: (args...) -> @mergeSelections args..., (previousSelection, currentSelection) -> exclusive = not currentSelection.isEmpty() and not previousSelection.isEmpty() previousSelection.intersectsWith(currentSelection, exclusive) mergeSelectionsOnSameRows: (args...) -> @mergeSelections args..., (previousSelection, currentSelection) -> screenRange = currentSelection.getScreenRange() previousSelection.intersectsScreenRowRange(screenRange.start.row, screenRange.end.row) avoidMergingSelections: (args...) -> @mergeSelections args..., -> false mergeSelections: (args...) -> mergePredicate = args.pop() fn = args.pop() if _.isFunction(_.last(args)) options = args.pop() ? {} return fn?() if @suppressSelectionMerging if fn? @suppressSelectionMerging = true result = fn() @suppressSelectionMerging = false reducer = (disjointSelections, selection) -> adjacentSelection = _.last(disjointSelections) if mergePredicate(adjacentSelection, selection) adjacentSelection.merge(selection, options) disjointSelections else disjointSelections.concat([selection]) [head, tail...] = @getSelectionsOrderedByBufferPosition() _.reduce(tail, reducer, [head]) return result if fn? # Add a {Selection} based on the given {DisplayMarker}. # # * `marker` The {DisplayMarker} to highlight # * `options` (optional) An {Object} that pertains to the {Selection} constructor. # # Returns the new {Selection}. addSelection: (marker, options={}) -> unless marker.getProperties().preserveFolds @destroyFoldsIntersectingBufferRange(marker.getBufferRange()) cursor = @addCursor(marker) selection = new Selection(_.extend({editor: this, marker, cursor, @clipboard}, options)) @selections.push(selection) selectionBufferRange = selection.getBufferRange() @mergeIntersectingSelections(preserveFolds: marker.getProperties().preserveFolds) if selection.destroyed for selection in @getSelections() if selection.intersectsBufferRange(selectionBufferRange) return selection else @emitter.emit 'did-add-selection', selection selection # Remove the given selection. removeSelection: (selection) -> _.remove(@cursors, selection.cursor) _.remove(@selections, selection) @cursorsByMarkerId.delete(selection.cursor.marker.id) @emitter.emit 'did-remove-cursor', selection.cursor @emitter.emit 'did-remove-selection', selection # Reduce one or more selections to a single empty selection based on the most # recently added cursor. clearSelections: (options) -> @consolidateSelections() @getLastSelection().clear(options) # Reduce multiple selections to the least recently added selection. consolidateSelections: -> selections = @getSelections() if selections.length > 1 selection.destroy() for selection in selections[1...(selections.length)] selections[0].autoscroll(center: true) true else false # Called by the selection selectionRangeChanged: (event) -> @emitter.emit 'did-change-selection-range', event createLastSelectionIfNeeded: -> if @selections.length is 0 @addSelectionForBufferRange([[0, 0], [0, 0]], autoscroll: false, preserveFolds: true) ### Section: Searching and Replacing ### # Essential: Scan regular expression matches in the entire buffer, calling the # given iterator function on each match. # # `::scan` functions as the replace method as well via the `replace` # # If you're programmatically modifying the results, you may want to try # {::backwardsScanInBufferRange} to avoid tripping over your own changes. # # * `regex` A {RegExp} to search for. # * `iterator` A {Function} that's called on each match # * `object` {Object} # * `match` The current regular expression match. # * `matchText` A {String} with the text of the match. # * `range` The {Range} of the match. # * `stop` Call this {Function} to terminate the scan. # * `replace` Call this {Function} with a {String} to replace the match. scan: (regex, iterator) -> @buffer.scan(regex, iterator) # Essential: Scan regular expression matches in a given range, calling the given # iterator function on each match. # # * `regex` A {RegExp} to search for. # * `range` A {Range} in which to search. # * `iterator` A {Function} that's called on each match with an {Object} # containing the following keys: # * `match` The current regular expression match. # * `matchText` A {String} with the text of the match. # * `range` The {Range} of the match. # * `stop` Call this {Function} to terminate the scan. # * `replace` Call this {Function} with a {String} to replace the match. scanInBufferRange: (regex, range, iterator) -> @buffer.scanInRange(regex, range, iterator) # Essential: Scan regular expression matches in a given range in reverse order, # calling the given iterator function on each match. # # * `regex` A {RegExp} to search for. # * `range` A {Range} in which to search. # * `iterator` A {Function} that's called on each match with an {Object} # containing the following keys: # * `match` The current regular expression match. # * `matchText` A {String} with the text of the match. # * `range` The {Range} of the match. # * `stop` Call this {Function} to terminate the scan. # * `replace` Call this {Function} with a {String} to replace the match. backwardsScanInBufferRange: (regex, range, iterator) -> @buffer.backwardsScanInRange(regex, range, iterator) ### Section: Tab Behavior ### # Essential: Returns a {Boolean} indicating whether softTabs are enabled for this # editor. getSoftTabs: -> @softTabs # Essential: Enable or disable soft tabs for this editor. # # * `softTabs` A {Boolean} setSoftTabs: (@softTabs) -> @softTabs # Essential: Toggle soft tabs for this editor toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs()) # Essential: Get the on-screen length of tab characters. # # Returns a {Number}. getTabLength: -> @displayBuffer.getTabLength() # Essential: Set the on-screen length of tab characters. Setting this to a # {Number} This will override the `editor.tabLength` setting. # # * `tabLength` {Number} length of a single tab. Setting to `null` will # fallback to using the `editor.tabLength` config setting setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength) # Extended: Determine if the buffer uses hard or soft tabs. # # Returns `true` if the first non-comment line with leading whitespace starts # with a space character. Returns `false` if it starts with a hard tab (`\t`). # # Returns a {Boolean} or undefined if no non-comment lines had leading # whitespace. usesSoftTabs: -> for bufferRow in [0..@buffer.getLastRow()] continue if @displayBuffer.tokenizedBuffer.tokenizedLineForRow(bufferRow).isComment() line = @buffer.lineForRow(bufferRow) return true if line[0] is ' ' return false if line[0] is '\t' undefined # Extended: Get the text representing a single level of indent. # # If soft tabs are enabled, the text is composed of N spaces, where N is the # tab length. Otherwise the text is a tab character (`\t`). # # Returns a {String}. getTabText: -> @buildIndentString(1) # If soft tabs are enabled, convert all hard tabs to soft tabs in the given # {Range}. normalizeTabsInBufferRange: (bufferRange) -> return unless @getSoftTabs() @scanInBufferRange /\t/g, bufferRange, ({replace}) => replace(@getTabText()) # Private: Computes whether or not this editor should use softTabs based on # the `editor.tabType` setting. # # Returns a {Boolean} shouldUseSoftTabs: ({defaultValue}) -> tabType = @config.get('editor.tabType', scope: @getRootScopeDescriptor()) switch tabType when 'auto' @usesSoftTabs() ? defaultValue ? @config.get('editor.softTabs') ? true when 'hard' false when 'soft' true ### Section: Soft Wrap Behavior ### # Essential: Determine whether lines in this editor are soft-wrapped. # # Returns a {Boolean}. isSoftWrapped: (softWrapped) -> @displayBuffer.isSoftWrapped() # Essential: Enable or disable soft wrapping for this editor. # # * `softWrapped` A {Boolean} # # Returns a {Boolean}. setSoftWrapped: (softWrapped) -> @displayBuffer.setSoftWrapped(softWrapped) # Essential: Toggle soft wrapping for this editor # # Returns a {Boolean}. toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped()) # Essential: Gets the column at which column will soft wrap getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn() ### Section: Indentation ### # Essential: Get the indentation level of the given a buffer row. # # Returns how deeply the given row is indented based on the soft tabs and # tab length settings of this editor. Note that if soft tabs are enabled and # the tab length is 2, a row with 4 leading spaces would have an indentation # level of 2. # # * `bufferRow` A {Number} indicating the buffer row. # # Returns a {Number}. indentationForBufferRow: (bufferRow) -> @indentLevelForLine(@lineTextForBufferRow(bufferRow)) # Essential: Set the indentation level for the given buffer row. # # Inserts or removes hard tabs or spaces based on the soft tabs and tab length # settings of this editor in order to bring it to the given indentation level. # Note that if soft tabs are enabled and the tab length is 2, a row with 4 # leading spaces would have an indentation level of 2. # # * `bufferRow` A {Number} indicating the buffer row. # * `newLevel` A {Number} indicating the new indentation level. # * `options` (optional) An {Object} with the following keys: # * `preserveLeadingWhitespace` `true` to preserve any whitespace already at # the beginning of the line (default: false). setIndentationForBufferRow: (bufferRow, newLevel, {preserveLeadingWhitespace}={}) -> if preserveLeadingWhitespace endColumn = 0 else endColumn = @lineTextForBufferRow(bufferRow).match(/^\s*/)[0].length newIndentString = @buildIndentString(newLevel) @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) # Extended: Indent rows intersecting selections by one level. indentSelectedRows: -> @mutateSelectedText (selection) -> selection.indentSelectedRows() # Extended: Outdent rows intersecting selections by one level. outdentSelectedRows: -> @mutateSelectedText (selection) -> selection.outdentSelectedRows() # Extended: Get the indentation level of the given line of text. # # Returns how deeply the given line is indented based on the soft tabs and # tab length settings of this editor. Note that if soft tabs are enabled and # the tab length is 2, a row with 4 leading spaces would have an indentation # level of 2. # # * `line` A {String} representing a line of text. # # Returns a {Number}. indentLevelForLine: (line) -> @displayBuffer.indentLevelForLine(line) # Extended: Indent rows intersecting selections based on the grammar's suggested # indent level. autoIndentSelectedRows: -> @mutateSelectedText (selection) -> selection.autoIndentSelectedRows() # Indent all lines intersecting selections. See {Selection::indent} for more # information. indent: (options={}) -> options.autoIndent ?= @shouldAutoIndent() @mutateSelectedText (selection) -> selection.indent(options) # Constructs the string used for indents. buildIndentString: (level, column=0) -> if @getSoftTabs() tabStopViolation = column % @getTabLength() _.multiplyString(" ", Math.floor(level * @getTabLength()) - tabStopViolation) else excessWhitespace = _.multiplyString(' ', Math.round((level - Math.floor(level)) * @getTabLength())) _.multiplyString("\t", Math.floor(level)) + excessWhitespace ### Section: Grammars ### # Essential: Get the current {Grammar} of this editor. getGrammar: -> @displayBuffer.getGrammar() # Essential: Set the current {Grammar} of this editor. # # Assigning a grammar will cause the editor to re-tokenize based on the new # grammar. # # * `grammar` {Grammar} setGrammar: (grammar) -> @displayBuffer.setGrammar(grammar) # Reload the grammar based on the file name. reloadGrammar: -> @displayBuffer.reloadGrammar() ### Section: Managing Syntax Scopes ### # Essential: Returns a {ScopeDescriptor} that includes this editor's language. # e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with # {Config::get} to get language specific config values. getRootScopeDescriptor: -> @displayBuffer.getRootScopeDescriptor() # Essential: Get the syntactic scopeDescriptor for the given position in buffer # coordinates. Useful with {Config::get}. # # For example, if called with a position inside the parameter list of an # anonymous CoffeeScript function, the method returns the following array: # `["source.coffee", "meta.inline.function.coffee", "variable.parameter.function.coffee"]` # # * `bufferPosition` A {Point} or {Array} of [row, column]. # # Returns a {ScopeDescriptor}. scopeDescriptorForBufferPosition: (bufferPosition) -> @displayBuffer.scopeDescriptorForBufferPosition(bufferPosition) # Extended: Get the range in buffer coordinates of all tokens surrounding the # cursor that match the given scope selector. # # For example, if you wanted to find the string surrounding the cursor, you # could call `editor.bufferRangeForScopeAtCursor(".string.quoted")`. # # * `scopeSelector` {String} selector. e.g. `'.source.ruby'` # # Returns a {Range}. bufferRangeForScopeAtCursor: (scopeSelector) -> @displayBuffer.bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition()) # Extended: Determine if the given row is entirely a comment isBufferRowCommented: (bufferRow) -> if match = @lineTextForBufferRow(bufferRow).match(/\S/) @commentScopeSelector ?= new TextMateScopeSelector('comment.*') @commentScopeSelector.matches(@scopeDescriptorForBufferPosition([bufferRow, match.index]).scopes) logCursorScope: -> scopeDescriptor = @getLastCursor().getScopeDescriptor() list = scopeDescriptor.scopes.toString().split(',') list = list.map (item) -> "* #{item}" content = "Scopes at Cursor\n#{list.join('\n')}" @notificationManager.addInfo(content, dismissable: true) # {Delegates to: DisplayBuffer.tokenForBufferPosition} tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition) ### Section: Clipboard Operations ### # Essential: For each selection, copy the selected text. copySelectedText: -> maintainClipboard = false for selection in @getSelectionsOrderedByBufferPosition() if selection.isEmpty() previousRange = selection.getBufferRange() selection.selectLine() selection.copy(maintainClipboard, true) selection.setBufferRange(previousRange) else selection.copy(maintainClipboard, false) maintainClipboard = true return # Private: For each selection, only copy highlighted text. copyOnlySelectedText: -> maintainClipboard = false for selection in @getSelectionsOrderedByBufferPosition() if not selection.isEmpty() selection.copy(maintainClipboard, true) maintainClipboard = true return # Essential: For each selection, cut the selected text. cutSelectedText: -> maintainClipboard = false @mutateSelectedText (selection) -> if selection.isEmpty() selection.selectLine() selection.cut(maintainClipboard, true) else selection.cut(maintainClipboard, false) maintainClipboard = true # Essential: For each selection, replace the selected text with the contents of # the clipboard. # # If the clipboard contains the same number of selections as the current # editor, each selection will be replaced with the content of the # corresponding clipboard selection text. # # * `options` (optional) See {Selection::insertText}. pasteText: (options={}) -> {text: clipboardText, metadata} = @clipboard.readWithMetadata() return false unless @emitWillInsertTextEvent(clipboardText) metadata ?= {} options.autoIndent = @shouldAutoIndentOnPaste() @mutateSelectedText (selection, index) => if metadata.selections?.length is @getSelections().length {text, indentBasis, fullLine} = metadata.selections[index] else {indentBasis, fullLine} = metadata text = clipboardText delete options.indentBasis {cursor} = selection if indentBasis? containsNewlines = text.indexOf('\n') isnt -1 if containsNewlines or not cursor.hasPrecedingCharactersOnLine() options.indentBasis ?= indentBasis range = null if fullLine and selection.isEmpty() oldPosition = selection.getBufferRange().start selection.setBufferRange([[oldPosition.row, 0], [oldPosition.row, 0]]) range = selection.insertText(text, options) newPosition = oldPosition.translate([1, 0]) selection.setBufferRange([newPosition, newPosition]) else range = selection.insertText(text, options) didInsertEvent = {text, range} @emitter.emit 'did-insert-text', didInsertEvent # Essential: For each selection, if the selection is empty, cut all characters # of the containing screen line following the cursor. Otherwise cut the selected # text. cutToEndOfLine: -> maintainClipboard = false @mutateSelectedText (selection) -> selection.cutToEndOfLine(maintainClipboard) maintainClipboard = true # Essential: For each selection, if the selection is empty, cut all characters # of the containing buffer line following the cursor. Otherwise cut the # selected text. cutToEndOfBufferLine: -> maintainClipboard = false @mutateSelectedText (selection) -> selection.cutToEndOfBufferLine(maintainClipboard) maintainClipboard = true ### Section: Folds ### # Essential: Fold the most recent cursor's row based on its indentation level. # # The fold will extend from the nearest preceding line with a lower # indentation level up to the nearest following row with a lower indentation # level. foldCurrentRow: -> bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row @foldBufferRow(bufferRow) # Essential: Unfold the most recent cursor's row by one level. unfoldCurrentRow: -> bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row @unfoldBufferRow(bufferRow) # Essential: Fold the given row in buffer coordinates based on its indentation # level. # # If the given row is foldable, the fold will begin there. Otherwise, it will # begin at the first foldable row preceding the given row. # # * `bufferRow` A {Number}. foldBufferRow: (bufferRow) -> @languageMode.foldBufferRow(bufferRow) # Essential: Unfold all folds containing the given row in buffer coordinates. # # * `bufferRow` A {Number} unfoldBufferRow: (bufferRow) -> @displayBuffer.unfoldBufferRow(bufferRow) # Extended: For each selection, fold the rows it intersects. foldSelectedLines: -> selection.fold() for selection in @getSelections() return # Extended: Fold all foldable lines. foldAll: -> @languageMode.foldAll() # Extended: Unfold all existing folds. unfoldAll: -> @languageMode.unfoldAll() @scrollToCursorPosition() # Extended: Fold all foldable lines at the given indent level. # # * `level` A {Number}. foldAllAtIndentLevel: (level) -> @languageMode.foldAllAtIndentLevel(level) # Extended: Determine whether the given row in buffer coordinates is foldable. # # A *foldable* row is a row that *starts* a row range that can be folded. # # * `bufferRow` A {Number} # # Returns a {Boolean}. isFoldableAtBufferRow: (bufferRow) -> @displayBuffer.isFoldableAtBufferRow(bufferRow) # Extended: Determine whether the given row in screen coordinates is foldable. # # A *foldable* row is a row that *starts* a row range that can be folded. # # * `bufferRow` A {Number} # # Returns a {Boolean}. isFoldableAtScreenRow: (screenRow) -> @isFoldableAtBufferRow(@bufferRowForScreenRow(screenRow)) # Extended: Fold the given buffer row if it isn't currently folded, and unfold # it otherwise. toggleFoldAtBufferRow: (bufferRow) -> if @isFoldedAtBufferRow(bufferRow) @unfoldBufferRow(bufferRow) else @foldBufferRow(bufferRow) # Extended: Determine whether the most recently added cursor's row is folded. # # Returns a {Boolean}. isFoldedAtCursorRow: -> @isFoldedAtScreenRow(@getCursorScreenPosition().row) # Extended: Determine whether the given row in buffer coordinates is folded. # # * `bufferRow` A {Number} # # Returns a {Boolean}. isFoldedAtBufferRow: (bufferRow) -> @displayBuffer.isFoldedAtBufferRow(bufferRow) # Extended: Determine whether the given row in screen coordinates is folded. # # * `screenRow` A {Number} # # Returns a {Boolean}. isFoldedAtScreenRow: (screenRow) -> @displayBuffer.isFoldedAtScreenRow(screenRow) foldBufferRowRange: (startRow, endRow) -> @displayBuffer.foldBufferRowRange(startRow, endRow) # Remove any {Fold}s found that intersect the given buffer range. destroyFoldsIntersectingBufferRange: (bufferRange) -> @displayLayer.destroyFoldsIntersectingBufferRange(bufferRange) # {Delegates to: DisplayBuffer.outermostFoldsForBufferRowRange} outermostFoldsInBufferRowRange: (startRow, endRow) -> @displayBuffer.outermostFoldsInBufferRowRange(startRow, endRow) ### Section: Gutters ### # Essential: Add a custom {Gutter}. # # * `options` An {Object} with the following fields: # * `name` (required) A unique {String} to identify this gutter. # * `priority` (optional) A {Number} that determines stacking order between # gutters. Lower priority items are forced closer to the edges of the # window. (default: -100) # * `visible` (optional) {Boolean} specifying whether the gutter is visible # initially after being created. (default: true) # # Returns the newly-created {Gutter}. addGutter: (options) -> @gutterContainer.addGutter(options) # Essential: Get this editor's gutters. # # Returns an {Array} of {Gutter}s. getGutters: -> @gutterContainer.getGutters() # Essential: Get the gutter with the given name. # # Returns a {Gutter}, or `null` if no gutter exists for the given name. gutterWithName: (name) -> @gutterContainer.gutterWithName(name) ### Section: Scrolling the TextEditor ### # Essential: Scroll the editor to reveal the most recently added cursor if it is # off-screen. # # * `options` (optional) {Object} # * `center` Center the editor around the cursor if possible. (default: true) scrollToCursorPosition: (options) -> @getLastCursor().autoscroll(center: options?.center ? true) # Essential: 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` (optional) {Object} # * `center` Center the editor around the position if possible. (default: false) scrollToBufferPosition: (bufferPosition, options) -> @displayBuffer.scrollToBufferPosition(bufferPosition, options) # Essential: Scrolls the editor to the given screen position. # # * `screenPosition` An object that represents a screen position. It can be either # an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point} # * `options` (optional) {Object} # * `center` Center the editor around the position if possible. (default: false) scrollToScreenPosition: (screenPosition, options) -> @displayBuffer.scrollToScreenPosition(screenPosition, options) scrollToTop: -> Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") @viewRegistry.getView(this).scrollToTop() scrollToBottom: -> Grim.deprecate("This is now a view method. Call TextEditorElement::scrollToTop instead.") @viewRegistry.getView(this).scrollToBottom() scrollToScreenRange: (screenRange, options) -> @displayBuffer.scrollToScreenRange(screenRange, options) getHorizontalScrollbarHeight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getHorizontalScrollbarHeight instead.") @viewRegistry.getView(this).getHorizontalScrollbarHeight() getVerticalScrollbarWidth: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getVerticalScrollbarWidth instead.") @viewRegistry.getView(this).getVerticalScrollbarWidth() pageUp: -> @moveUp(@getRowsPerPage()) pageDown: -> @moveDown(@getRowsPerPage()) selectPageUp: -> @selectUp(@getRowsPerPage()) selectPageDown: -> @selectDown(@getRowsPerPage()) # Returns the number of rows per page getRowsPerPage: -> Math.max(@rowsPerPage ? 1, 1) setRowsPerPage: (@rowsPerPage) -> ### Section: Config ### shouldAutoIndent: -> @config.get("editor.autoIndent", scope: @getRootScopeDescriptor()) shouldAutoIndentOnPaste: -> @config.get("editor.autoIndentOnPaste", scope: @getRootScopeDescriptor()) ### Section: Event Handlers ### handleTokenization: -> @softTabs = @shouldUseSoftTabs(defaultValue: @softTabs) handleGrammarChange: -> @unfoldAll() @subscribeToTabTypeConfig() @emitter.emit 'did-change-grammar', @getGrammar() ### Section: TextEditor Rendering ### # Get the Element for the editor. getElement: -> @editorElement ?= new TextEditorElement().initialize(this, atom, @autoHeight, @scrollPastEnd) # Essential: Retrieves the greyed out placeholder of a mini editor. # # Returns a {String}. getPlaceholderText: -> @placeholderText # Essential: Set the greyed out placeholder of a mini editor. Placeholder text # will be displayed when the editor has no content. # # * `placeholderText` {String} text that is displayed when the editor has no content. setPlaceholderText: (placeholderText) -> return if @placeholderText is placeholderText @placeholderText = placeholderText @emitter.emit 'did-change-placeholder-text', @placeholderText pixelPositionForBufferPosition: (bufferPosition) -> Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForBufferPosition` instead") @viewRegistry.getView(this).pixelPositionForBufferPosition(bufferPosition) pixelPositionForScreenPosition: (screenPosition) -> Grim.deprecate("This method is deprecated on the model layer. Use `TextEditorElement::pixelPositionForScreenPosition` instead") @viewRegistry.getView(this).pixelPositionForScreenPosition(screenPosition) getSelectionMarkerAttributes: -> {type: 'selection', invalidate: 'never'} getVerticalScrollMargin: -> @displayBuffer.getVerticalScrollMargin() setVerticalScrollMargin: (verticalScrollMargin) -> @displayBuffer.setVerticalScrollMargin(verticalScrollMargin) getHorizontalScrollMargin: -> @displayBuffer.getHorizontalScrollMargin() setHorizontalScrollMargin: (horizontalScrollMargin) -> @displayBuffer.setHorizontalScrollMargin(horizontalScrollMargin) getLineHeightInPixels: -> @displayBuffer.getLineHeightInPixels() setLineHeightInPixels: (lineHeightInPixels) -> @displayBuffer.setLineHeightInPixels(lineHeightInPixels) getKoreanCharWidth: -> @displayBuffer.getKoreanCharWidth() getHalfWidthCharWidth: -> @displayBuffer.getHalfWidthCharWidth() getDoubleWidthCharWidth: -> @displayBuffer.getDoubleWidthCharWidth() getDefaultCharWidth: -> @displayBuffer.getDefaultCharWidth() setDefaultCharWidth: (defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) -> @displayBuffer.setDefaultCharWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth) setHeight: (height, reentrant=false) -> if reentrant @displayBuffer.setHeight(height) else Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.") @viewRegistry.getView(this).setHeight(height) getHeight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.") @displayBuffer.getHeight() getClientHeight: -> @displayBuffer.getClientHeight() setWidth: (width, reentrant=false) -> if reentrant @displayBuffer.setWidth(width) else Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.") @viewRegistry.getView(this).setWidth(width) getWidth: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.") @displayBuffer.getWidth() # Experimental: Scroll the editor such that the given screen row is at the # top of the visible area. setFirstVisibleScreenRow: (screenRow, fromView) -> unless fromView maxScreenRow = @getScreenLineCount() - 1 unless @config.get('editor.scrollPastEnd') and @scrollPastEnd height = @displayBuffer.getHeight() lineHeightInPixels = @displayBuffer.getLineHeightInPixels() if height? and lineHeightInPixels? maxScreenRow -= Math.floor(height / lineHeightInPixels) screenRow = Math.max(Math.min(screenRow, maxScreenRow), 0) unless screenRow is @firstVisibleScreenRow @firstVisibleScreenRow = screenRow @emitter.emit 'did-change-first-visible-screen-row', screenRow unless fromView getFirstVisibleScreenRow: -> @firstVisibleScreenRow getLastVisibleScreenRow: -> height = @displayBuffer.getHeight() lineHeightInPixels = @displayBuffer.getLineHeightInPixels() if height? and lineHeightInPixels? Math.min(@firstVisibleScreenRow + Math.floor(height / lineHeightInPixels), @getScreenLineCount() - 1) else null getVisibleRowRange: -> if lastVisibleScreenRow = @getLastVisibleScreenRow() [@firstVisibleScreenRow, lastVisibleScreenRow] else null setFirstVisibleScreenColumn: (@firstVisibleScreenColumn) -> getFirstVisibleScreenColumn: -> @firstVisibleScreenColumn getScrollTop: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollTop instead.") @viewRegistry.getView(this).getScrollTop() setScrollTop: (scrollTop) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollTop instead.") @viewRegistry.getView(this).setScrollTop(scrollTop) getScrollBottom: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollBottom instead.") @viewRegistry.getView(this).getScrollBottom() setScrollBottom: (scrollBottom) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollBottom instead.") @viewRegistry.getView(this).setScrollBottom(scrollBottom) getScrollLeft: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollLeft instead.") @viewRegistry.getView(this).getScrollLeft() setScrollLeft: (scrollLeft) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollLeft instead.") @viewRegistry.getView(this).setScrollLeft(scrollLeft) getScrollRight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollRight instead.") @viewRegistry.getView(this).getScrollRight() setScrollRight: (scrollRight) -> Grim.deprecate("This is now a view method. Call TextEditorElement::setScrollRight instead.") @viewRegistry.getView(this).setScrollRight(scrollRight) getScrollHeight: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollHeight instead.") @viewRegistry.getView(this).getScrollHeight() getScrollWidth: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollWidth instead.") @viewRegistry.getView(this).getScrollWidth() getMaxScrollTop: -> Grim.deprecate("This is now a view method. Call TextEditorElement::getMaxScrollTop instead.") @viewRegistry.getView(this).getMaxScrollTop() intersectsVisibleRowRange: (startRow, endRow) -> Grim.deprecate("This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.") @viewRegistry.getView(this).intersectsVisibleRowRange(startRow, endRow) selectionIntersectsVisibleRowRange: (selection) -> Grim.deprecate("This is now a view method. Call TextEditorElement::selectionIntersectsVisibleRowRange instead.") @viewRegistry.getView(this).selectionIntersectsVisibleRowRange(selection) screenPositionForPixelPosition: (pixelPosition) -> Grim.deprecate("This is now a view method. Call TextEditorElement::screenPositionForPixelPosition instead.") @viewRegistry.getView(this).screenPositionForPixelPosition(pixelPosition) pixelRectForScreenRange: (screenRange) -> Grim.deprecate("This is now a view method. Call TextEditorElement::pixelRectForScreenRange instead.") @viewRegistry.getView(this).pixelRectForScreenRange(screenRange) ### Section: Utility ### inspect: -> "" logScreenLines: (start, end) -> @displayBuffer.logLines(start, end) emitWillInsertTextEvent: (text) -> result = true cancel = -> result = false willInsertEvent = {cancel, text} @emitter.emit 'will-insert-text', willInsertEvent result ### Section: Language Mode Delegated Methods ### suggestedIndentForBufferRow: (bufferRow, options) -> @languageMode.suggestedIndentForBufferRow(bufferRow, options) autoIndentBufferRow: (bufferRow, options) -> @languageMode.autoIndentBufferRow(bufferRow, options) autoIndentBufferRows: (startRow, endRow) -> @languageMode.autoIndentBufferRows(startRow, endRow) autoDecreaseIndentForBufferRow: (bufferRow) -> @languageMode.autoDecreaseIndentForBufferRow(bufferRow) toggleLineCommentForBufferRow: (row) -> @languageMode.toggleLineCommentsForBufferRow(row) toggleLineCommentsForBufferRows: (start, end) -> @languageMode.toggleLineCommentsForBufferRows(start, end)