diff --git a/spec/atom-spec.coffee b/spec/atom-spec.coffee index 7b1ae1618..c71f89faa 100644 --- a/spec/atom-spec.coffee +++ b/spec/atom-spec.coffee @@ -8,6 +8,23 @@ describe "the `atom` global", -> beforeEach -> atom.workspaceView = new WorkspaceView + describe 'window sizing methods', -> + describe '::getPosition and ::setPosition', -> + it 'sets the position of the window, and can retrieve the position just set', -> + atom.setPosition(22, 45) + expect(atom.getPosition()).toEqual x: 22, y: 45 + + describe '::getSize and ::setSize', -> + originalSize = null + beforeEach -> + originalSize = atom.getSize() + afterEach -> + atom.setSize(originalSize.width, originalSize.height) + + it 'sets the size of the window, and can retrieve the size just set', -> + atom.setSize(100, 400) + expect(atom.getSize()).toEqual width: 100, height: 400 + describe "package lifecycle methods", -> describe ".loadPackage(name)", -> it "continues if the package has an invalid package.json", -> diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index f8bc55742..b2864cafe 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -2239,7 +2239,7 @@ describe "EditorComponent", -> editor.setSoftWrapped(true) callingOrder = [] - editor.onDidChangeScreenLines -> callingOrder.push 'screen-lines-changed' + editor.onDidChange -> callingOrder.push 'screen-lines-changed' wrapperView.on 'editor:display-updated', -> callingOrder.push 'editor:display-updated' editor.insertText("HELLO! HELLO!\n HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! HELLO! ") nextAnimationFrame() diff --git a/src/atom.coffee b/src/atom.coffee index 731131d22..3a84070f9 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -7,21 +7,21 @@ screen = require 'screen' shell = require 'shell' _ = require 'underscore-plus' -{deprecated} = require 'grim' +{deprecate} = require 'grim' {Model} = require 'theorist' fs = require 'fs-plus' {$} = require './space-pen-extensions' WindowEventHandler = require './window-event-handler' -# Public: Atom global for dealing with packages, themes, menus, and the window. +# Essential: Atom global for dealing with packages, themes, menus, and the window. # # An instance of this class is always available as the `atom` global. module.exports = class Atom extends Model @version: 1 # Increment this when the serialization format changes - # Public: Load or create the Atom environment in the given mode. + # Load or create the Atom environment in the given mode. # # * `mode` A {String} mode that is either 'editor' or 'spec' depending on the # kind of environment you want to build. @@ -98,6 +98,10 @@ class Atom extends Model workspaceViewParentSelector: 'body' lastUncaughtError: null + ### + Section: Properties + ### + # Public: A {Clipboard} instance clipboard: null @@ -137,13 +141,17 @@ class Atom extends Model # Public: A {WorkspaceView} instance workspaceView: null + ### + Section: Construction and Destruction + ### + # Call .loadOrCreate instead constructor: (@state) -> {@mode} = @state DeserializerManager = require './deserializer-manager' @deserializers = new DeserializerManager() - # Public: Sets up the basic services that should be available in all modes + # Sets up the basic services that should be available in all modes # (both spec and application). # # Call after this instance has been assigned to the `atom` global. @@ -203,22 +211,160 @@ class Atom extends Model @windowEventHandler = new WindowEventHandler - # Deprecated: Callers should be converted to use atom.deserializers - registerRepresentationClass: -> - deprecated("Callers should be converted to use atom.deserializers") + ### + Section: Atom Metadata + ### - # Deprecated: Callers should be converted to use atom.deserializers - registerRepresentationClasses: -> - deprecated("Callers should be converted to use atom.deserializers") + # Essential: Is the current window in development mode? + inDevMode: -> + @getLoadSettings().devMode - setBodyPlatformClass: -> - document.body.classList.add("platform-#{process.platform}") + # Essential: Is the current window running specs? + inSpecMode: -> + @getLoadSettings().isSpec - # Public: Get the current window + # Essential: Get the version of the Atom application. + # + # Returns the version text {String}. + getVersion: -> + @appVersion ?= @getLoadSettings().appVersion + + # Essential: Determine whether the current version is an official release. + isReleasedVersion: -> + not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix + + # Essential: Get the directory path to Atom's configuration area. + # + # Returns the absolute path to `~/.atom`. + getConfigDirPath: -> + @constructor.getConfigDirPath() + + # Extended: Get the load settings for the current window. + # + # Returns an {Object} containing all the load setting key/value pairs. + getLoadSettings: -> + @constructor.getLoadSettings() + + # Extended: Get the time taken to completely load the current window. + # + # This time include things like loading and activating packages, creating + # DOM elements for the editor, and reading the config. + # + # Returns the {Number} of milliseconds taken to load the window or null + # if the window hasn't finished loading yet. + getWindowLoadTime: -> + @loadTime + + ### + Section: Managing The Atom Window + ### + + # Essential: Open a new Atom window using the given options. + # + # Calling this method without an options parameter will open a prompt to pick + # a file/folder to open in the new window. + # + # * `options` An {Object} with the following keys: + # * `pathsToOpen` An {Array} of {String} paths to open. + # * `newWindow` A {Boolean}, true to always open a new window instead of + # reusing existing windows depending on the paths to open. + # * `devMode` A {Boolean}, true to open the window in development mode. + # Development mode loads the Atom source from the locally cloned + # repository and also loads all the packages in ~/.atom/dev/packages + # * `safeMode` A {Boolean}, true to open the window in safe mode. Safe + # mode prevents all packages installed to ~/.atom/packages from loading. + open: (options) -> + ipc.send('open', options) + + # Essential: Close the current window. + close: -> + @getCurrentWindow().close() + + # Essential: Get the size of current window. + # + # Returns an {Object} in the format `{width: 1000, height: 700}` + getSize: -> + [width, height] = @getCurrentWindow().getSize() + {width, height} + + # Essential: Set the size of current window. + # + # * `width` The {Number} of pixels. + # * `height` The {Number} of pixels. + setSize: (width, height) -> + @getCurrentWindow().setSize(width, height) + + # Essential: Get the position of current window. + # + # Returns an {Object} in the format `{x: 10, y: 20}` + getPosition: -> + [x, y] = @getCurrentWindow().getPosition() + {x, y} + + # Essential: Set the position of current window. + # + # * `x` The {Number} of pixels. + # * `y` The {Number} of pixels. + setPosition: (x, y) -> + ipc.send('call-window-method', 'setPosition', x, y) + + # Extended: Get the current window getCurrentWindow: -> @constructor.getCurrentWindow() - # Public: Get the dimensions of this window. + # Extended: Move current window to the center of the screen. + center: -> + ipc.send('call-window-method', 'center') + + # Extended: Focus the current window. + focus: -> + ipc.send('call-window-method', 'focus') + $(window).focus() + + # Extended: Show the current window. + show: -> + ipc.send('call-window-method', 'show') + + # Extended: Hide the current window. + hide: -> + ipc.send('call-window-method', 'hide') + + # Extended: Reload the current window. + reload: -> + ipc.send('call-window-method', 'restart') + + # Extended: Returns a {Boolean} true when the current window is maximized. + isMaximixed: -> + @getCurrentWindow().isMaximized() + + maximize: -> + ipc.send('call-window-method', 'maximize') + + # Extended: Is the current window in full screen mode? + isFullScreen: -> + @getCurrentWindow().isFullScreen() + + # Extended: Set the full screen state of the current window. + setFullScreen: (fullScreen=false) -> + ipc.send('call-window-method', 'setFullScreen', fullScreen) + if fullScreen then document.body.classList.add("fullscreen") else document.body.classList.remove("fullscreen") + + # Extended: Toggle the full screen state of the current window. + toggleFullScreen: -> + @setFullScreen(!@isFullScreen()) + + # Schedule the window to be shown and focused on the next tick. + # + # This is done in a next tick to prevent a white flicker from occurring + # if called synchronously. + displayWindow: ({maximize}={}) -> + setImmediate => + @show() + @focus() + @setFullScreen(true) if @workspace.fullScreen + @maximize() if maximize + + # Get the dimensions of this window. # # Returns an {Object} with the following keys: # * `x` The window's x-position {Number}. @@ -232,7 +378,7 @@ class Atom extends Model maximized = browserWindow.isMaximized() {x, y, width, height, maximized} - # Public: Set the dimensions of the window. + # Set the dimensions of the window. # # The window will be centered if either the x or y coordinate is not set # in the dimensions parameter. If x or y are omitted the window will be @@ -289,41 +435,6 @@ class Atom extends Model dimensions = @getWindowDimensions() @state.windowDimensions = dimensions if @isValidDimensions(dimensions) - # Public: Get the load settings for the current window. - # - # Returns an {Object} containing all the load setting key/value pairs. - getLoadSettings: -> - @constructor.getLoadSettings() - - deserializeProject: -> - Project = require './project' - - startTime = Date.now() - @project ?= @deserializers.deserialize(@state.project) ? new Project(path: @getLoadSettings().initialPath) - @deserializeTimings.project = Date.now() - startTime - - deserializeWorkspaceView: -> - Workspace = require './workspace' - WorkspaceView = require './workspace-view' - - startTime = Date.now() - @workspace = Workspace.deserialize(@state.workspace) ? new Workspace - @workspaceView = new WorkspaceView(@workspace) - @deserializeTimings.workspace = Date.now() - startTime - - @keymaps.defaultTarget = @workspaceView[0] - $(@workspaceViewParentSelector).append(@workspaceView) - - deserializePackageStates: -> - @packages.packageStates = @state.packageStates ? {} - delete @state.packageStates - - deserializeEditorWindow: -> - @deserializeTimings = {} - @deserializePackageStates() - @deserializeProject() - @deserializeWorkspaceView() - # Call this method when establishing a real application window. startEditorWindow: -> {resourcePath, safeMode} = @getLoadSettings() @@ -374,41 +485,16 @@ class Atom extends Model @windowEventHandler?.unsubscribe() - loadThemes: -> - @themes.load() + ### + Section: Messaging the User + ### - watchThemes: -> - @themes.onDidReloadAll => - # Only reload stylesheets from non-theme packages - for pack in @packages.getActivePackages() when pack.getType() isnt 'theme' - pack.reloadStylesheets?() - null + # Essential: Visually and audibly trigger a beep. + beep: -> + shell.beep() if @config.get('core.audioBeep') + @workspaceView.trigger 'beep' - # Notify the browser project of the window's current project path - watchProjectPath: -> - onProjectPathChanged = => - ipc.send('window-command', 'project-path-changed', @project.getPath()) - @subscribe @project, 'path-changed', onProjectPathChanged - onProjectPathChanged() - - # Public: Open a new Atom window using the given options. - # - # Calling this method without an options parameter will open a prompt to pick - # a file/folder to open in the new window. - # - # * `options` An {Object} with the following keys: - # * `pathsToOpen` An {Array} of {String} paths to open. - # * `newWindow` A {Boolean}, true to always open a new window instead of - # reusing existing windows depending on the paths to open. - # * `devMode` A {Boolean}, true to open the window in development mode. - # Development mode loads the Atom source from the locally cloned - # repository and also loads all the packages in ~/.atom/dev/packages - # * `safeMode` A {Boolean}, true to open the window in safe mode. Safe - # mode prevents all packages installed to ~/.atom/packages from loading. - open: (options) -> - ipc.send('open', options) - - # Public: Open a confirm dialog. + # Essential: A flexible way to open a dialog akin to an alert dialog. # # ## Examples # @@ -418,13 +504,13 @@ class Atom extends Model # detailedMessage: 'Be honest.' # buttons: # Good: -> window.alert('good to hear') - # Bad: -> window.alert('bummer') + # Bad: -> window.alert('bummer') # ``` # # * `options` An {Object} with the following keys: # * `message` The {String} message to display. - # * `detailedMessage` The {String} detailed message to display. - # * `buttons` Either an array of strings or an object where keys are + # * `detailedMessage` (optional) The {String} detailed message to display. + # * `buttons` (optional) Either an array of strings or an object where keys are # button names and the values are callbacks to invoke when clicked. # # Returns the chosen button index {Number} if the buttons option was an array. @@ -448,77 +534,71 @@ class Atom extends Model callback = buttons[buttonLabels[chosen]] callback?() - showSaveDialog: (callback) -> - callback(showSaveDialogSync()) + ### + Section: Managing the Dev Tools + ### - showSaveDialogSync: (defaultPath) -> - defaultPath ?= @project?.getPath() - currentWindow = @getCurrentWindow() - dialog = remote.require('dialog') - dialog.showSaveDialog currentWindow, {title: 'Save File', defaultPath} - - # Public: Open the dev tools for the current window. + # Extended: Open the dev tools for the current window. openDevTools: -> ipc.send('call-window-method', 'openDevTools') - # Public: Toggle the visibility of the dev tools for the current window. + # Extended: Toggle the visibility of the dev tools for the current window. toggleDevTools: -> ipc.send('call-window-method', 'toggleDevTools') - # Public: Execute code in dev tools. + # Extended: Execute code in dev tools. executeJavaScriptInDevTools: (code) -> ipc.send('call-window-method', 'executeJavaScriptInDevTools', code) - # Public: Reload the current window. - reload: -> - ipc.send('call-window-method', 'restart') + ### + Section: Private + ### - # Public: Focus the current window. - focus: -> - ipc.send('call-window-method', 'focus') - $(window).focus() + deserializeProject: -> + Project = require './project' - # Public: Show the current window. - show: -> - ipc.send('call-window-method', 'show') + startTime = Date.now() + @project ?= @deserializers.deserialize(@state.project) ? new Project(path: @getLoadSettings().initialPath) + @deserializeTimings.project = Date.now() - startTime - # Public: Hide the current window. - hide: -> - ipc.send('call-window-method', 'hide') + deserializeWorkspaceView: -> + Workspace = require './workspace' + WorkspaceView = require './workspace-view' - # Public: Set the size of current window. - # - # * `width` The {Number} of pixels. - # * `height` The {Number} of pixels. - setSize: (width, height) -> - @getCurrentWindow().setSize(width, height) + startTime = Date.now() + @workspace = Workspace.deserialize(@state.workspace) ? new Workspace + @workspaceView = new WorkspaceView(@workspace) + @deserializeTimings.workspace = Date.now() - startTime - # Public: Set the position of current window. - # - # * `x` The {Number} of pixels. - # * `y` The {Number} of pixels. - setPosition: (x, y) -> - ipc.send('call-window-method', 'setPosition', x, y) + @keymaps.defaultTarget = @workspaceView[0] + $(@workspaceViewParentSelector).append(@workspaceView) - # Public: Move current window to the center of the screen. - center: -> - ipc.send('call-window-method', 'center') + deserializePackageStates: -> + @packages.packageStates = @state.packageStates ? {} + delete @state.packageStates + deserializeEditorWindow: -> + @deserializeTimings = {} + @deserializePackageStates() + @deserializeProject() + @deserializeWorkspaceView() - # Schedule the window to be shown and focused on the next tick. - # - # This is done in a next tick to prevent a white flicker from occurring - # if called synchronously. - displayWindow: ({maximize}={}) -> - setImmediate => - @show() - @focus() - @setFullScreen(true) if @workspace.fullScreen - @maximize() if maximize + loadThemes: -> + @themes.load() - # Public: Close the current window. - close: -> - @getCurrentWindow().close() + watchThemes: -> + @themes.onDidReloadAll => + # Only reload stylesheets from non-theme packages + for pack in @packages.getActivePackages() when pack.getType() isnt 'theme' + pack.reloadStylesheets?() + null + + # Notify the browser project of the window's current project path + watchProjectPath: -> + onProjectPathChanged = => + ipc.send('window-command', 'project-path-changed', @project.getPath()) + @subscribe @project, 'path-changed', onProjectPathChanged + onProjectPathChanged() exit: (status) -> app = remote.require('app') @@ -531,45 +611,14 @@ class Atom extends Model setRepresentedFilename: (filename) -> ipc.send('call-window-method', 'setRepresentedFilename', filename) - # Public: Is the current window in development mode? - inDevMode: -> - @getLoadSettings().devMode + showSaveDialog: (callback) -> + callback(showSaveDialogSync()) - # Public: Is the current window running specs? - inSpecMode: -> - @getLoadSettings().isSpec - - # Public: Toggle the full screen state of the current window. - toggleFullScreen: -> - @setFullScreen(!@isFullScreen()) - - # Public: Set the full screen state of the current window. - setFullScreen: (fullScreen=false) -> - ipc.send('call-window-method', 'setFullScreen', fullScreen) - if fullScreen then document.body.classList.add("fullscreen") else document.body.classList.remove("fullscreen") - - # Public: Is the current window in full screen mode? - isFullScreen: -> - @getCurrentWindow().isFullScreen() - - maximize: -> - ipc.send('call-window-method', 'maximize') - - # Public: Get the version of the Atom application. - # - # Returns the version text {String}. - getVersion: -> - @appVersion ?= @getLoadSettings().appVersion - - # Public: Determine whether the current version is an official release. - isReleasedVersion: -> - not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix - - # Public: Get the directory path to Atom's configuration area. - # - # Returns the absolute path to `~/.atom`. - getConfigDirPath: -> - @constructor.getConfigDirPath() + showSaveDialogSync: (defaultPath) -> + defaultPath ?= @project?.getPath() + currentWindow = @getCurrentWindow() + dialog = remote.require('dialog') + dialog.showSaveDialog currentWindow, {title: 'Save File', defaultPath} saveSync: -> stateString = JSON.stringify(@state) @@ -578,27 +627,12 @@ class Atom extends Model else @getCurrentWindow().loadSettings.windowState = stateString - # Public: Get the time taken to completely load the current window. - # - # This time include things like loading and activating packages, creating - # DOM elements for the editor, and reading the config. - # - # Returns the {Number} of milliseconds taken to load the window or null - # if the window hasn't finished loading yet. - getWindowLoadTime: -> - @loadTime - crashMainProcess: -> remote.process.crash() crashRenderProcess: -> process.crash() - # Public: Visually and audibly trigger a beep. - beep: -> - shell.beep() if @config.get('core.audioBeep') - @workspaceView.trigger 'beep' - getUserInitScriptPath: -> initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee']) initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee') @@ -610,7 +644,7 @@ class Atom extends Model catch error console.error "Failed to load `#{userInitScriptPath}`", error.stack, error - # Public: Require the module with the given globals. + # Require the module with the given globals. # # The globals will be set on the `window` object and removed after the # require completes. @@ -630,3 +664,14 @@ class Atom extends Model delete window[key] else window[key] = value + + # Deprecated: Callers should be converted to use atom.deserializers + registerRepresentationClass: -> + deprecate("Callers should be converted to use atom.deserializers") + + # Deprecated: Callers should be converted to use atom.deserializers + registerRepresentationClasses: -> + deprecate("Callers should be converted to use atom.deserializers") + + setBodyPlatformClass: -> + document.body.classList.add("platform-#{process.platform}") diff --git a/src/buffered-node-process.coffee b/src/buffered-node-process.coffee index 808bc27a4..e9a3fbee4 100644 --- a/src/buffered-node-process.coffee +++ b/src/buffered-node-process.coffee @@ -1,7 +1,7 @@ BufferedProcess = require './buffered-process' path = require 'path' -# Public: Like {BufferedProcess}, but accepts a Node script as the command +# Extended: Like {BufferedProcess}, but accepts a Node script as the command # to run. # # This is necessary on Windows since it doesn't support shebang `#!` lines. diff --git a/src/buffered-process.coffee b/src/buffered-process.coffee index ed6474f3b..0a7751e57 100644 --- a/src/buffered-process.coffee +++ b/src/buffered-process.coffee @@ -1,7 +1,7 @@ _ = require 'underscore-plus' ChildProcess = require 'child_process' -# Public: A wrapper which provides standard error/output line buffering for +# Extended: A wrapper which provides standard error/output line buffering for # Node's ChildProcess. # # ## Examples diff --git a/src/clipboard.coffee b/src/clipboard.coffee index 48bfb8dd5..3fba6f4e8 100644 --- a/src/clipboard.coffee +++ b/src/clipboard.coffee @@ -1,7 +1,7 @@ clipboard = require 'clipboard' crypto = require 'crypto' -# Public: Represents the clipboard used for copying and pasting in Atom. +# Extended: Represents the clipboard used for copying and pasting in Atom. # # An instance of this class is always available as the `atom.clipboard` global. # diff --git a/src/config.coffee b/src/config.coffee index f3606d7bb..92b11f891 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -6,7 +6,7 @@ path = require 'path' async = require 'async' pathWatcher = require 'pathwatcher' -# Public: Used to access all of Atom's configuration details. +# Essential: Used to access all of Atom's configuration details. # # An instance of this class is always available as the `atom.config` global. # diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 5aa8392a6..a55daabf0 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -5,7 +5,7 @@ path = require 'path' CSON = require 'season' fs = require 'fs-plus' -# Public: Provides a registry for commands that you'd like to appear in the +# Extended: Provides a registry for commands that you'd like to appear in the # context menu. # # An instance of this class is always available as the `atom.contextMenu` diff --git a/src/cursor.coffee b/src/cursor.coffee index b736dd424..1848182db 100644 --- a/src/cursor.coffee +++ b/src/cursor.coffee @@ -54,11 +54,14 @@ class Cursor extends Model @emitter.dispose() @needsAutoscroll = true + destroy: -> + @marker.destroy() + ### Section: Event Subscription ### - # Essential: Calls your `callback` when the cursor has been moved. + # Public: Calls your `callback` when the cursor has been moved. # # * `callback` {Function} # * `event` {Object} @@ -72,7 +75,7 @@ class Cursor extends Model onDidChangePosition: (callback) -> @emitter.on 'did-change-position', callback - # Extended: Calls your `callback` when the cursor is destroyed + # Public: Calls your `callback` when the cursor is destroyed # # * `callback` {Function} # @@ -80,7 +83,7 @@ class Cursor extends Model onDidDestroy: (callback) -> @emitter.on 'did-destroy', callback - # Extended: Calls your `callback` when the cursor's visibility has changed + # Public: Calls your `callback` when the cursor's visibility has changed # # * `callback` {Function} # * `visibility` {Boolean} @@ -102,23 +105,9 @@ class Cursor extends Model super ### - Section: Methods + Section: Managing Cursor Position ### - destroy: -> - @marker.destroy() - - changePosition: (options, fn) -> - @clearSelection() - @needsAutoscroll = options.autoscroll ? @isLastCursor() - fn() - if @needsAutoscroll - @emit 'autoscrolled' # Support legacy editor - @autoscroll() if @needsAutoscroll and @editor.manageScrollPosition # Support react editor view - - getPixelRect: -> - @editor.pixelRectForScreenRange(@getScreenRange()) - # Public: Moves a cursor to a given screen position. # # * `screenPosition` {Array} of two numbers: the screen row, and the screen column. @@ -133,10 +122,6 @@ class Cursor extends Model getScreenPosition: -> @marker.getHeadScreenPosition() - getScreenRange: -> - {row, column} = @getScreenPosition() - new Range(new Point(row, column), new Point(row, column + 1)) - # Public: Moves a cursor to a given buffer position. # # * `bufferPosition` {Array} of two numbers: the buffer row, and the buffer column. @@ -151,47 +136,38 @@ class Cursor extends Model getBufferPosition: -> @marker.getHeadBufferPosition() - autoscroll: (options) -> - @editor.scrollToScreenRange(@getScreenRange(), options) + # Public: Returns the cursor's current screen row. + getScreenRow: -> + @getScreenPosition().row - # Public: If the marker range is empty, the cursor is marked as being visible. - updateVisibility: -> - @setVisible(@marker.getBufferRange().isEmpty()) + # Public: Returns the cursor's current screen column. + getScreenColumn: -> + @getScreenPosition().column - # Public: Sets whether the cursor is visible. - setVisible: (visible) -> - if @visible != visible - @visible = visible - @needsAutoscroll ?= true if @visible and @isLastCursor() - @emit 'visibility-changed', @visible - @emitter.emit 'did-change-visibility', @visible + # Public: Retrieves the cursor's current buffer row. + getBufferRow: -> + @getBufferPosition().row - # Public: Returns the visibility of the cursor. - isVisible: -> @visible + # Public: Returns the cursor's current buffer column. + getBufferColumn: -> + @getBufferPosition().column - # Public: Get the RegExp used by the cursor to determine what a "word" is. - # - # * `options` (optional) {Object} with the following keys: - # * `includeNonWordCharacters` A {Boolean} indicating whether to include - # non-word characters in the regex. (default: true) - # - # Returns a {RegExp}. - wordRegExp: ({includeNonWordCharacters}={}) -> - includeNonWordCharacters ?= true - nonWordCharacters = atom.config.get('editor.nonWordCharacters') - segments = ["^[\t ]*$"] - segments.push("[^\\s#{_.escapeRegExp(nonWordCharacters)}]+") - if includeNonWordCharacters - segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+") - new RegExp(segments.join("|"), "g") + # Public: Returns the cursor's current buffer row of text excluding its line + # ending. + getCurrentBufferLine: -> + @editor.lineTextForBufferRow(@getBufferRow()) - # Public: Identifies if this cursor is the last in the {Editor}. - # - # "Last" is defined as the most recently added cursor. - # - # Returns a {Boolean}. - isLastCursor: -> - this == @editor.getLastCursor() + # Public: Returns whether the cursor is at the start of a line. + isAtBeginningOfLine: -> + @getBufferPosition().column == 0 + + # Public: Returns whether the cursor is on the line return character. + isAtEndOfLine: -> + @getBufferPosition().isEqual(@getCurrentLineBufferRange().end) + + ### + Section: Info about the cursor position + ### # Public: Identifies if the cursor is surrounded by whitespace. # @@ -229,34 +205,42 @@ class Cursor extends Model range = [[row, column], [row, Infinity]] @editor.getTextInBufferRange(range).search(@wordRegExp()) == 0 - # Public: Prevents this cursor from causing scrolling. - clearAutoscroll: -> - @needsAutoscroll = null + # Public: Returns the indentation level of the current line. + getIndentLevel: -> + if @editor.getSoftTabs() + @getBufferColumn() / @editor.getTabLength() + else + @getBufferColumn() - # Public: Deselects the current selection. - clearSelection: -> - @selection?.clear() + # Public: Retrieves the grammar's token scopes for the line. + # + # Returns an {Array} of {String}s. + getScopes: -> + @editor.scopesForBufferPosition(@getBufferPosition()) - # Public: Returns the cursor's current screen row. - getScreenRow: -> - @getScreenPosition().row + # Public: Returns true if this cursor has no non-whitespace characters before + # its current position. + hasPrecedingCharactersOnLine: -> + bufferPosition = @getBufferPosition() + line = @editor.lineTextForBufferRow(bufferPosition.row) + firstCharacterColumn = line.search(/\S/) - # Public: Returns the cursor's current screen column. - getScreenColumn: -> - @getScreenPosition().column + if firstCharacterColumn is -1 + false + else + bufferPosition.column > firstCharacterColumn - # Public: Retrieves the cursor's current buffer row. - getBufferRow: -> - @getBufferPosition().row + # Public: Identifies if this cursor is the last in the {Editor}. + # + # "Last" is defined as the most recently added cursor. + # + # Returns a {Boolean}. + isLastCursor: -> + this == @editor.getLastCursor() - # Public: Returns the cursor's current buffer column. - getBufferColumn: -> - @getBufferPosition().column - - # Public: Returns the cursor's current buffer row of text excluding its line - # ending. - getCurrentBufferLine: -> - @editor.lineTextForBufferRow(@getBufferRow()) + ### + Section: Moving the Cursor + ### # Public: Moves the cursor up one screen row. # @@ -417,6 +401,20 @@ class Cursor extends Model @setBufferPosition(endOfLeadingWhitespace) if endOfLeadingWhitespace.isGreaterThan(position) + # Public: Moves the cursor to the beginning of the next paragraph + moveToBeginningOfNextParagraph: -> + if position = @getBeginningOfNextParagraphBufferPosition() + @setBufferPosition(position) + + # Public: Moves the cursor to the beginning of the previous paragraph + moveToBeginningOfPreviousParagraph: -> + if position = @getBeginningOfPreviousParagraphBufferPosition() + @setBufferPosition(position) + + ### + Section: Retrieving Positions and Ranges of local boundaries + ### + # Public: Retrieves the buffer position of where the current word starts. # # * `options` (optional) An {Object} with the following keys: @@ -553,15 +551,98 @@ class Cursor extends Model getCurrentLineBufferRange: (options) -> @editor.bufferRangeForBufferRow(@getBufferRow(), options) - # Public: Moves the cursor to the beginning of the next paragraph - moveToBeginningOfNextParagraph: -> - if position = @getBeginningOfNextParagraphBufferPosition() - @setBufferPosition(position) + # Public: Retrieves the range for the current paragraph. + # + # A paragraph is defined as a block of text surrounded by empty lines. + # + # Returns a {Range}. + getCurrentParagraphBufferRange: -> + @editor.languageMode.rowRangeForParagraphAtBufferRow(@getBufferRow()) - # Public: Moves the cursor to the beginning of the previous paragraph - moveToBeginningOfPreviousParagraph: -> - if position = @getBeginningOfPreviousParagraphBufferPosition() - @setBufferPosition(position) + # Public: Returns the characters preceding the cursor in the current word. + getCurrentWordPrefix: -> + @editor.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()]) + + ### + Section: Visibility + ### + + # Public: If the marker range is empty, the cursor is marked as being visible. + updateVisibility: -> + @setVisible(@marker.getBufferRange().isEmpty()) + + # Public: Sets whether the cursor is visible. + setVisible: (visible) -> + if @visible != visible + @visible = visible + @needsAutoscroll ?= true if @visible and @isLastCursor() + @emit 'visibility-changed', @visible + @emitter.emit 'did-change-visibility', @visible + + # Public: Returns the visibility of the cursor. + isVisible: -> @visible + + ### + Section: Comparing to another cursor + ### + + # Public: Compare this cursor's buffer position to another cursor's buffer position. + # + # See {Point::compare} for more details. + # + # * `otherCursor`{Cursor} to compare against + compare: (otherCursor) -> + @getBufferPosition().compare(otherCursor.getBufferPosition()) + + ### + Section: Utilities + ### + + # Public: Prevents this cursor from causing scrolling. + clearAutoscroll: -> + @needsAutoscroll = null + + # Public: Deselects the current selection. + clearSelection: -> + @selection?.clear() + + # Public: Get the RegExp used by the cursor to determine what a "word" is. + # + # * `options` (optional) {Object} with the following keys: + # * `includeNonWordCharacters` A {Boolean} indicating whether to include + # non-word characters in the regex. (default: true) + # + # Returns a {RegExp}. + wordRegExp: ({includeNonWordCharacters}={}) -> + includeNonWordCharacters ?= true + nonWordCharacters = atom.config.get('editor.nonWordCharacters') + segments = ["^[\t ]*$"] + segments.push("[^\\s#{_.escapeRegExp(nonWordCharacters)}]+") + if includeNonWordCharacters + segments.push("[#{_.escapeRegExp(nonWordCharacters)}]+") + new RegExp(segments.join("|"), "g") + + ### + Section: Private + ### + + changePosition: (options, fn) -> + @clearSelection() + @needsAutoscroll = options.autoscroll ? @isLastCursor() + fn() + if @needsAutoscroll + @emit 'autoscrolled' # Support legacy editor + @autoscroll() if @needsAutoscroll and @editor.manageScrollPosition # Support react editor view + + getPixelRect: -> + @editor.pixelRectForScreenRange(@getScreenRange()) + + getScreenRange: -> + {row, column} = @getScreenPosition() + new Range(new Point(row, column), new Point(row, column + 1)) + + autoscroll: (options) -> + @editor.scrollToScreenRange(@getScreenRange(), options) getBeginningOfNextParagraphBufferPosition: (editor) -> start = @getBufferPosition() @@ -589,56 +670,3 @@ class Cursor extends Model position = range.start stop() @editor.screenPositionForBufferPosition(position) - - # Public: Retrieves the range for the current paragraph. - # - # A paragraph is defined as a block of text surrounded by empty lines. - # - # Returns a {Range}. - getCurrentParagraphBufferRange: -> - @editor.languageMode.rowRangeForParagraphAtBufferRow(@getBufferRow()) - - # Public: Returns the characters preceding the cursor in the current word. - getCurrentWordPrefix: -> - @editor.getTextInBufferRange([@getBeginningOfCurrentWordBufferPosition(), @getBufferPosition()]) - - # Public: Returns whether the cursor is at the start of a line. - isAtBeginningOfLine: -> - @getBufferPosition().column == 0 - - # Public: Returns the indentation level of the current line. - getIndentLevel: -> - if @editor.getSoftTabs() - @getBufferColumn() / @editor.getTabLength() - else - @getBufferColumn() - - # Public: Returns whether the cursor is on the line return character. - isAtEndOfLine: -> - @getBufferPosition().isEqual(@getCurrentLineBufferRange().end) - - # Public: Retrieves the grammar's token scopes for the line. - # - # Returns an {Array} of {String}s. - getScopes: -> - @editor.scopesForBufferPosition(@getBufferPosition()) - - # Public: Returns true if this cursor has no non-whitespace characters before - # its current position. - hasPrecedingCharactersOnLine: -> - bufferPosition = @getBufferPosition() - line = @editor.lineTextForBufferRow(bufferPosition.row) - firstCharacterColumn = line.search(/\S/) - - if firstCharacterColumn is -1 - false - else - bufferPosition.column > firstCharacterColumn - - # Public: Compare this cursor's buffer position to another cursor's buffer position. - # - # See {Point::compare} for more details. - # - # * `otherCursor`{Cursor} to compare against - compare: (otherCursor) -> - @getBufferPosition().compare(otherCursor.getBufferPosition()) diff --git a/src/decoration.coffee b/src/decoration.coffee index 075641058..1728ce225 100644 --- a/src/decoration.coffee +++ b/src/decoration.coffee @@ -32,7 +32,7 @@ module.exports = class Decoration EmitterMixin.includeInto(this) - # Extended: Check if the `decorationProperties.type` matches `type` + # Private: Check if the `decorationProperties.type` matches `type` # # * `decorationProperties` {Object} eg. `{type: 'gutter', class: 'my-new-class'}` # * `type` {String} type like `'gutter'`, `'line'`, etc. `type` can also @@ -46,6 +46,10 @@ class Decoration else type is decorationProperties.type + ### + Section: Construction and Destruction + ### + constructor: (@marker, @displayBuffer, @properties) -> @emitter = new Emitter @id = nextId() diff --git a/src/deserializer-manager.coffee b/src/deserializer-manager.coffee index 7a4390f6a..9abefc4c6 100644 --- a/src/deserializer-manager.coffee +++ b/src/deserializer-manager.coffee @@ -1,4 +1,4 @@ -# Public: Manages the deserializers used for serialized state +# Extended: Manages the deserializers used for serialized state # # An instance of this class is always available as the `atom.deserializers` # global. diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 646ff690c..dd4095d1d 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -350,7 +350,7 @@ EditorComponent = React.createClass observeEditor: -> {editor} = @props - @subscribe editor.onDidChangeScreenLines(@onScreenLinesChanged) + @subscribe editor.onDidChange(@onScreenLinesChanged) @subscribe editor.observeCursors(@onCursorAdded) @subscribe editor.observeSelections(@onSelectionAdded) @subscribe editor.observeDecorations(@onDecorationAdded) diff --git a/src/editor-view.coffee b/src/editor-view.coffee index e91d69caa..fb8c458b4 100644 --- a/src/editor-view.coffee +++ b/src/editor-view.coffee @@ -9,6 +9,8 @@ EditorComponent = require './editor-component' # Public: Represents the entire visual pane in Atom. # # The EditorView manages the {Editor}, which manages the file buffers. +# `EditorView` is intentionally sparse. Most of the things you'll want +# to do are on {Editor}. # # ## Examples # diff --git a/src/editor.coffee b/src/editor.coffee index e66110d8e..3e0b5bd37 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -155,7 +155,7 @@ class Editor extends Model @subscribe @displayBuffer.onDidTokenize => @handleTokenization() @subscribe @displayBuffer.onDidChange (e) => @emit 'screen-lines-changed', e - @emitter.emit 'did-change-screen-lines', e + @emitter.emit 'did-change', e # TODO: remove these when we remove the deprecations. Though, no one is likely using them @subscribe @displayBuffer.onDidChangeSoftWrapped (softWrapped) => @emit 'soft-wrap-changed', softWrapped @@ -192,6 +192,53 @@ class Editor extends Model 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} + # + # 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} + # * `selection` {Selection} that moved + # + # 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} @@ -209,16 +256,6 @@ class Editor extends Model onDidChangeGrammar: (callback) -> @emitter.on 'did-change-grammar', callback - # Essential: Calls your `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) - # Extended: Calls your `callback` when the result of {::isModified} changes. # # * `callback` {Function} @@ -296,21 +333,6 @@ class Editor extends Model onDidRemoveCursor: (callback) -> @emitter.on 'did-remove-cursor', 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} - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeCursorPosition: (callback) -> - @emitter.on 'did-change-cursor-position', callback - # Extended: Calls your `callback` when a {Selection} is added to the editor. # Immediately calls your callback for each existing selection. # @@ -340,15 +362,6 @@ class Editor extends Model onDidRemoveSelection: (callback) -> @emitter.on 'did-remove-selection', callback - # Essential: Calls your `callback` when a selection's screen range changes. - # - # * `callback` {Function} - # * `selection` {Selection} that moved - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidChangeSelectionRange: (callback) -> - @emitter.on 'did-change-selection-range', callback - # Extended: Calls your `callback` with each {Decoration} added to the editor. # Calls your `callback` immediately for any existing decorations. # @@ -380,9 +393,6 @@ class Editor extends Model onDidChangeCharacterWidths: (callback) -> @displayBuffer.onDidChangeCharacterWidths(callback) - onDidChangeScreenLines: (callback) -> - @emitter.on 'did-change-screen-lines', callback - onDidChangeScrollTop: (callback) -> @emitter.on 'did-change-scroll-top', callback @@ -437,7 +447,7 @@ class Editor extends Model deprecate("Use Marker::onDidChange instead. eg. `editor::decorateMarker(...).getMarker().onDidChange()`") when 'screen-lines-changed' - deprecate("Use Editor::onDidChangeScreenLines instead") + deprecate("Use Editor::onDidChange instead") when 'scroll-top-changed' deprecate("Use Editor::onDidChangeScrollTop instead") @@ -482,7 +492,7 @@ class Editor extends Model Section: File Details ### - # Public: Get the title the editor's title for display in other parts of the + # 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 @@ -495,7 +505,7 @@ class Editor extends Model else 'untitled' - # Public: Get the editor's long title for display in other parts of the UI + # Essential: Get the editor's long title for display in other parts of the UI # such as the window title. # # If the editor's buffer is saved, its long title is formatted as @@ -511,28 +521,13 @@ class Editor extends Model else 'untitled' - # Public: Returns the {String} path of this editor's text buffer. + # Essential: Returns the {String} path of this editor's text buffer. getPath: -> @buffer.getPath() - # Public: Saves the editor's text buffer. - # - # See {TextBuffer::save} for more details. - save: -> @buffer.save() - - # Public: 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) - - # Public: Determine whether the user should be prompted to save before closing - # this editor. - shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors() - - # Public: Returns {Boolean} `true` if this editor has been modified. + # 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. @@ -540,14 +535,34 @@ class Editor extends Model if filePath = @getPath() atom.clipboard.write(filePath) + ### + Section: Saving + ### + + # Essential: Saves the editor's text buffer. + # + # See {TextBuffer::save} for more details. + save: -> @buffer.save() + + # Extended: 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) + + # Extended: Determine whether the user should be prompted to save before closing + # this editor. + shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors() + ### Section: Reading Text ### - # Public: Returns a {String} representing the entire contents of the editor. + # Essential: Returns a {String} representing the entire contents of the editor. getText: -> @buffer.getText() - # Public: Get the text in the given {Range} in buffer coordinates. + # Essential: Get the text in the given {Range} in buffer coordinates. # # * `range` A {Range} or range-compatible {Array}. # @@ -555,20 +570,22 @@ class Editor extends Model getTextInBufferRange: (range) -> @buffer.getTextInRange(range) - # Public: Returns a {Number} representing the number of lines in the editor. + # Essential: Returns a {Number} representing the number of lines in the buffer. getLineCount: -> @buffer.getLineCount() - # {Delegates to: DisplayBuffer.getLineCount} + # Essential: Returns a {Number} representing the number of screen lines in the + # editor. This accounts for folds. getScreenLineCount: -> @displayBuffer.getLineCount() - # Public: Returns a {Number} representing the last zero-indexed buffer row + # Essential: Returns a {Number} representing the last zero-indexed buffer row # number of the editor. getLastBufferRow: -> @buffer.getLastRow() - # {Delegates to: DisplayBuffer.getLastRow} + # Essential: Returns a {Number} representing the last zero-indexed screen row + # number of the editor. getLastScreenRow: -> @displayBuffer.getLastRow() - # Public: Returns a {String} representing the contents of the line at the + # Essential: Returns a {String} representing the contents of the line at the # given buffer row. # # * `bufferRow` A {Number} representing a zero-indexed buffer row. @@ -577,7 +594,7 @@ class Editor extends Model deprecate 'Use Editor::lineTextForBufferRow(bufferRow) instead' @lineTextForBufferRow(bufferRow) - # Public: Returns a {String} representing the contents of the line at the + # Essential: Returns a {String} representing the contents of the line at the # given screen row. # # * `screenRow` A {Number} representing a zero-indexed screen row. @@ -637,7 +654,7 @@ class Editor extends Model # {Delegates to: TextBuffer.getEndPosition} getEofBufferPosition: -> @buffer.getEndPosition() - # Public: Get the {Range} of the paragraph surrounding the most recently added + # Extended: Get the {Range} of the paragraph surrounding the most recently added # cursor. # # Returns a {Range}. @@ -649,10 +666,10 @@ class Editor extends Model Section: Mutating Text ### - # Public: Replaces the entire contents of the buffer with the given {String}. + # Essential: Replaces the entire contents of the buffer with the given {String}. setText: (text) -> @buffer.setText(text) - # Public: Set the text in the given {Range} in buffer coordinates. + # Essential: Set the text in the given {Range} in buffer coordinates. # # * `range` A {Range} or range-compatible {Array}. # * `text` A {String} @@ -660,7 +677,7 @@ class Editor extends Model # Returns the {Range} of the newly-inserted text. setTextInBufferRange: (range, text, normalizeLineEndings) -> @getBuffer().setTextInRange(range, text, normalizeLineEndings) - # Public: Mutate the text of all the selections in a single transaction. + # 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}. @@ -840,7 +857,7 @@ class Editor extends Model @addSelectionForBufferRange([[row, 0], [row, Infinity]]) @addSelectionForBufferRange([[end.row, 0], [end.row, end.column]]) unless end.column is 0 - # Public: For each selection, transpose the selected text. + # 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. @@ -855,20 +872,28 @@ class Editor extends Model else selection.insertText selection.getText().split('').reverse().join('') - # Public: Convert the selected text to upper case. + # 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() - # Public: Convert the selected text to lower case. + # 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. + # + # Returns an {Array} of the commented {Range}s. + toggleLineCommentsInSelection: -> + @mutateSelectedText (selection) -> selection.toggleLineComments() + # Convert multiple lines to a single line. # # Operates on all selections. If the selection is empty, joins the current @@ -880,11 +905,39 @@ class Editor extends Model joinLines: -> @mutateSelectedText (selection) -> selection.joinLines() + # 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. + # + # * `fn` A {Function} to call inside the transaction. + transact: (fn) -> @buffer.transact(fn) + + # Extended: Start an open-ended transaction. + # + # Call {::commitTransaction} or {::abortTransaction} to terminate the + # transaction. If you nest calls to transactions, only the outermost + # transaction is considered. You must match every begin with a matching + # commit, but a single call to abort will cancel all nested transactions. + beginTransaction: -> @buffer.beginTransaction() + + # Extended: Commit an open-ended transaction started with {::beginTransaction} + # and push it to the undo stack. + # + # If transactions are nested, only the outermost commit takes effect. + commitTransaction: -> @buffer.commitTransaction() + + # Extended: Abort an open transaction, undoing any operations performed so far + # within the transaction. + abortTransaction: -> @buffer.abortTransaction() + ### - Section: Adding Text + Section: Inserting Text ### - # Public: For each selection, replace the selected text with the given text. + # 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}. @@ -910,17 +963,17 @@ class Editor extends Model else false - # Public: For each selection, replace the selected text with a newline. + # Essential: For each selection, replace the selected text with a newline. insertNewline: -> @insertText('\n') - # Public: For each cursor, insert a newline at beginning the following line. + # Extended: For each cursor, insert a newline at beginning the following line. insertNewlineBelow: -> @transact => @moveToEndOfLine() @insertNewline() - # Public: For each cursor, insert a newline at the end of the preceding line. + # Extended: For each cursor, insert a newline at the end of the preceding line. insertNewlineAbove: -> @transact => bufferRow = @getCursorBufferPosition().row @@ -942,11 +995,45 @@ class Editor extends Model Section: Removing Text ### - # Public: For each selection, if the selection is empty, delete the character + # Essential: For each selection, if the selection is empty, delete the character + # preceding 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: 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: 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: -> + @mutateSelectedText (selection) -> selection.deleteLine() + # Deprecated: Use {::deleteToBeginningOfWord} instead. backspaceToBeginningOfWord: -> deprecate("Use Editor::deleteToBeginningOfWord() instead") @@ -957,59 +1044,81 @@ class Editor extends Model deprecate("Use Editor::deleteToBeginningOfLine() instead") @deleteToBeginningOfLine() - # Public: 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() - - # Public: 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() - - # Public: For each selection, if the selection is empty, delete the character - # preceding the cursor. Otherwise delete the selected text. - delete: -> - @mutateSelectedText (selection) -> selection.delete() - - # Public: 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() - - # Public: 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() - - # Public: Delete all lines intersecting selections. - deleteLine: -> - @mutateSelectedText (selection) -> selection.deleteLine() - ### Section: Searching Text ### - # {Delegates to: TextBuffer.scan} - scan: (args...) -> @buffer.scan(args...) + # Essential: Scan regular expression matches in the entire buffer, calling the + # given iterator function on each match. + # + # 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 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. + scan: (regex, iterator) -> @buffer.scan(regex, iterator) - # {Delegates to: TextBuffer.scanInRange} - scanInBufferRange: (args...) -> @buffer.scanInRange(args...) - - # {Delegates to: TextBuffer.backwardsScanInRange} - backwardsScanInBufferRange: (args...) -> @buffer.backwardsScanInRange(args...) + # Extended: 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) + # Extended: 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 ### - # Public: Determine if the buffer uses hard or soft tabs. + # 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. + # + # * `tabLength` {Number} length of a single tab + 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`). @@ -1026,19 +1135,7 @@ class Editor extends Model undefined - # Public: Returns a {Boolean} indicating whether softTabs are enabled for this - # editor. - getSoftTabs: -> @softTabs - - # Public: Enable or disable soft tabs for this editor. - # - # * `softTabs` A {Boolean} - setSoftTabs: (@softTabs) -> @softTabs - - # Public: Toggle soft tabs for this editor - toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs()) - - # Public: Get the text representing a single level of indent. + # 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`). @@ -1046,14 +1143,6 @@ class Editor extends Model # Returns a {String}. getTabText: -> @buildIndentString(1) - # Public: Get the on-screen length of tab characters. - # - # Returns a {Number}. - getTabLength: -> @displayBuffer.getTabLength() - - # Public: Set the on-screen length of tab characters. - setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength) - # If soft tabs are enabled, convert all hard tabs to soft tabs in the given # {Range}. normalizeTabsInBufferRange: (bufferRange) -> @@ -1064,10 +1153,7 @@ class Editor extends Model Section: Soft Wrap Behavior ### - # Public: Sets the column at which column will soft wrap - getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn() - - # Public: Determine whether lines in this editor are soft-wrapped. + # Essential: Determine whether lines in this editor are soft-wrapped. # # Returns a {Boolean}. isSoftWrapped: (softWrapped) -> @displayBuffer.isSoftWrapped() @@ -1075,7 +1161,7 @@ class Editor extends Model deprecate("Use Editor::isSoftWrapped instead") @displayBuffer.isSoftWrapped() - # Public: Enable or disable soft wrapping for this editor. + # Essential: Enable or disable soft wrapping for this editor. # # * `softWrapped` A {Boolean} # @@ -1085,7 +1171,7 @@ class Editor extends Model deprecate("Use Editor::setSoftWrapped instead") @setSoftWrapped(softWrapped) - # Public: Toggle soft wrapping for this editor + # Essential: Toggle soft wrapping for this editor # # Returns a {Boolean}. toggleSoftWrapped: -> @setSoftWrapped(not @isSoftWrapped()) @@ -1093,11 +1179,14 @@ class Editor extends Model deprecate("Use Editor::toggleSoftWrapped instead") @toggleSoftWrapped() + # Extended: Gets the column at which column will soft wrap + getSoftWrapColumn: -> @displayBuffer.getSoftWrapColumn() + ### Section: Indentation ### - # Public: Get the indentation level of the given a buffer row. + # 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 @@ -1110,7 +1199,7 @@ class Editor extends Model indentationForBufferRow: (bufferRow) -> @indentLevelForLine(@lineTextForBufferRow(bufferRow)) - # Public: Set the indentation level for the given buffer row. + # 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. @@ -1130,7 +1219,15 @@ class Editor extends Model newIndentString = @buildIndentString(newLevel) @buffer.setTextInRange([[bufferRow, 0], [bufferRow, endColumn]], newIndentString) - # Public: Get the indentation level of the given line of text. + # 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 @@ -1143,25 +1240,17 @@ class Editor extends Model 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) - # Public: Indent rows intersecting selections by one level. - indentSelectedRows: -> - @mutateSelectedText (selection) -> selection.indentSelectedRows() - - # Public: Outdent rows intersecting selections by one level. - outdentSelectedRows: -> - @mutateSelectedText (selection) -> selection.outdentSelectedRows() - - # Public: Indent rows intersecting selections based on the grammar's suggested - # indent level. - autoIndentSelectedRows: -> - @mutateSelectedText (selection) -> selection.autoIndentSelectedRows() - # Constructs the string used for tabs. buildIndentString: (number, column=0) -> if @getSoftTabs() @@ -1174,53 +1263,21 @@ class Editor extends Model Section: Undo Operations ### - # Public: Undo the last change. + # Essential: Undo the last change. undo: -> @getLastCursor().needsAutoscroll = true @buffer.undo(this) - # Public: Redo the last change. + # Essential: Redo the last change. redo: -> @getLastCursor().needsAutoscroll = true @buffer.redo(this) - ### - Section: Text Mutation Transactions - ### - - # Public: 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. - # - # * `fn` A {Function} to call inside the transaction. - transact: (fn) -> @buffer.transact(fn) - - # Public: Start an open-ended transaction. - # - # Call {::commitTransaction} or {::abortTransaction} to terminate the - # transaction. If you nest calls to transactions, only the outermost - # transaction is considered. You must match every begin with a matching - # commit, but a single call to abort will cancel all nested transactions. - beginTransaction: -> @buffer.beginTransaction() - - # Public: Commit an open-ended transaction started with {::beginTransaction} - # and push it to the undo stack. - # - # If transactions are nested, only the outermost commit takes effect. - commitTransaction: -> @buffer.commitTransaction() - - # Public: Abort an open transaction, undoing any operations performed so far - # within the transaction. - abortTransaction: -> @buffer.abortTransaction() - ### Section: Editor Coordinates ### - # Public: Convert a position in buffer-coordinates to screen-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 @@ -1232,7 +1289,7 @@ class Editor extends Model # Returns a {Point}. screenPositionForBufferPosition: (bufferPosition, options) -> @displayBuffer.screenPositionForBufferPosition(bufferPosition, options) - # Public: Convert a position in screen-coordinates to buffer-coordinates. + # Essential: Convert a position in screen-coordinates to buffer-coordinates. # # The position is clipped via {::clipScreenPosition} prior to the conversion. # @@ -1242,21 +1299,21 @@ class Editor extends Model # Returns a {Point}. bufferPositionForScreenPosition: (screenPosition, options) -> @displayBuffer.bufferPositionForScreenPosition(screenPosition, options) - # Public: Convert a range in buffer-coordinates to screen-coordinates. + # 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) -> @displayBuffer.screenRangeForBufferRange(bufferRange) - # Public: Convert a range in screen-coordinates to buffer-coordinates. + # 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) -> @displayBuffer.bufferRangeForScreenRange(screenRange) - # Public: Clip the given {Point} to a valid position in the buffer. + # 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 @@ -1277,7 +1334,7 @@ class Editor extends Model # Returns a {Point}. clipBufferPosition: (bufferPosition) -> @buffer.clipPosition(bufferPosition) - # Public: Clip the start and end of the given range to valid positions in the + # 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. @@ -1285,7 +1342,7 @@ class Editor extends Model # Returns a {Range}. clipBufferRange: (range) -> @buffer.clipRange(range) - # Public: Clip the given {Point} to a valid position on screen. + # 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 @@ -1310,21 +1367,20 @@ class Editor extends Model # Returns a {Point}. clipScreenPosition: (screenPosition, options) -> @displayBuffer.clipScreenPosition(screenPosition, options) - - - ### Section: Grammars ### - # Public: Get the current {Grammar} of this editor. + # Essential: Get the current {Grammar} of this editor. getGrammar: -> @displayBuffer.getGrammar() - # Public: Set the current {Grammar} of this editor. + # 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) @@ -1333,10 +1389,19 @@ class Editor extends Model @displayBuffer.reloadGrammar() ### - Section: Syntatic Queries + Section: Managing Syntax Scopes ### - # Public: Get the syntactic scopes for the given position in buffer + # Public: Get the syntactic scopes for the most recently added cursor's + # position. See {::scopesForBufferPosition} for more information. + # + # Returns an {Array} of {String}s. + scopesAtCursor: -> @getLastCursor().getScopes() + getCursorScopes: -> + deprecate 'Use Editor::scopesAtCursor() instead' + @scopesAtCursor() + + # Essential: Get the syntactic scopes for the given position in buffer # coordinates. # # For example, if called with a position inside the parameter list of an @@ -1348,7 +1413,7 @@ class Editor extends Model # Returns an {Array} of {String}s. scopesForBufferPosition: (bufferPosition) -> @displayBuffer.scopesForBufferPosition(bufferPosition) - # Public: Get the range in buffer coordinates of all tokens surrounding the + # 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 @@ -1358,52 +1423,38 @@ class Editor extends Model bufferRangeForScopeAtCursor: (selector) -> @displayBuffer.bufferRangeForScopeAtPosition(selector, @getCursorBufferPosition()) + logCursorScope: -> + console.log @scopesAtCursor() + # {Delegates to: DisplayBuffer.tokenForBufferPosition} tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition) - # Public: Get the syntactic scopes for the most recently added cursor's - # position. See {::scopesForBufferPosition} for more information. - # - # Returns an {Array} of {String}s. - getCursorScopes: -> @getLastCursor().getScopes() - - logCursorScope: -> - console.log @getCursorScopes() - - - # Public: Determine if the given row is entirely a comment + # Extended: Determine if the given row is entirely a comment isBufferRowCommented: (bufferRow) -> if match = @lineTextForBufferRow(bufferRow).match(/\S/) scopes = @tokenForBufferPosition([bufferRow, match.index]).scopes @commentScopeSelector ?= new TextMateScopeSelector('comment.*') @commentScopeSelector.matches(scopes) - # Public: Toggle line comments for rows intersecting selections. - # - # If the current grammar doesn't support comments, does nothing. - # - # Returns an {Array} of the commented {Range}s. - toggleLineCommentsInSelection: -> - @mutateSelectedText (selection) -> selection.toggleLineComments() - - - - - - - ### Section: Clipboard Operations ### - # Public: For each selection, copy the selected text. + # Essential: For each selection, copy the selected text. copySelectedText: -> maintainClipboard = false for selection in @getSelections() selection.copy(maintainClipboard) maintainClipboard = true - # Public: For each selection, replace the selected text with the contents of + # Essential: For each selection, cut the selected text. + cutSelectedText: -> + maintainClipboard = false + @mutateSelectedText (selection) -> + selection.cut(maintainClipboard) + 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 @@ -1429,14 +1480,7 @@ class Editor extends Model @insertText(text, options) - # Public: For each selection, cut the selected text. - cutSelectedText: -> - maintainClipboard = false - @mutateSelectedText (selection) -> - selection.cut(maintainClipboard) - maintainClipboard = true - - # Public: For each selection, if the selection is empty, cut all characters + # Extended: For each selection, if the selection is empty, cut all characters # of the containing line following the cursor. Otherwise cut the selected # text. cutToEndOfLine: -> @@ -1445,12 +1489,11 @@ class Editor extends Model selection.cutToEndOfLine(maintainClipboard) maintainClipboard = true - ### Section: Folds ### - # Public: Fold the most recent cursor's row based on its indentation level. + # 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 @@ -1459,30 +1502,12 @@ class Editor extends Model bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row @foldBufferRow(bufferRow) - # Public: Unfold the most recent cursor's row by one level. + # Essential: Unfold the most recent cursor's row by one level. unfoldCurrentRow: -> bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row @unfoldBufferRow(bufferRow) - # Public: For each selection, fold the rows it intersects. - foldSelectedLines: -> - selection.fold() for selection in @getSelections() - - # Public: Fold all foldable lines. - foldAll: -> - @languageMode.foldAll() - - # Public: Unfold all existing folds. - unfoldAll: -> - @languageMode.unfoldAll() - - # Public: Fold all foldable lines at the given indent level. - # - # * `level` A {Number}. - foldAllAtIndentLevel: (level) -> - @languageMode.foldAllAtIndentLevel(level) - - # Public: Fold the given row in buffer coordinates based on its indentation + # 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 @@ -1492,13 +1517,31 @@ class Editor extends Model foldBufferRow: (bufferRow) -> @languageMode.foldBufferRow(bufferRow) - # Public: Unfold all folds containing the given row in buffer coordinates. + # Essential: Unfold all folds containing the given row in buffer coordinates. # # * `bufferRow` A {Number} unfoldBufferRow: (bufferRow) -> @displayBuffer.unfoldBufferRow(bufferRow) - # Public: Determine whether the given row in buffer coordinates is foldable. + # Extended: For each selection, fold the rows it intersects. + foldSelectedLines: -> + selection.fold() for selection in @getSelections() + + # Extended: Fold all foldable lines. + foldAll: -> + @languageMode.foldAll() + + # Extended: Unfold all existing folds. + unfoldAll: -> + @languageMode.unfoldAll() + + # 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. # @@ -1508,10 +1551,47 @@ class Editor extends Model isFoldableAtBufferRow: (bufferRow) -> @languageMode.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) -> bufferRow = @displayBuffer.bufferRowForScreenRow(screenRow) @isFoldableAtBufferRow(bufferRow) + # 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) + # TODO: Rename to foldRowRange? createFold: (startRow, endRow) -> @displayBuffer.createFold(startRow, endRow) @@ -1525,36 +1605,6 @@ class Editor extends Model for row in [bufferRange.start.row..bufferRange.end.row] @unfoldBufferRow(row) - # Public: 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) - - # Public: Determine whether the most recently added cursor's row is folded. - # - # Returns a {Boolean}. - isFoldedAtCursorRow: -> - @isFoldedAtScreenRow(@getCursorScreenPosition().row) - - # Public: Determine whether the given row in buffer coordinates is folded. - # - # * `bufferRow` A {Number} - # - # Returns a {Boolean}. - isFoldedAtBufferRow: (bufferRow) -> - @displayBuffer.isFoldedAtBufferRow(bufferRow) - - # Public: Determine whether the given row in screen coordinates is folded. - # - # * `screenRow` A {Number} - # - # Returns a {Boolean}. - isFoldedAtScreenRow: (screenRow) -> - @displayBuffer.isFoldedAtScreenRow(screenRow) - # {Delegates to: DisplayBuffer.largestFoldContainingBufferRow} largestFoldContainingBufferRow: (bufferRow) -> @displayBuffer.largestFoldContainingBufferRow(bufferRow) @@ -1567,28 +1617,11 @@ class Editor extends Model outermostFoldsInBufferRowRange: (startRow, endRow) -> @displayBuffer.outermostFoldsInBufferRowRange(startRow, endRow) - - - - ### Section: Decorations ### - # Public: Get all the decorations within a screen row range. - # - # * `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: 'gutter', class: 'someclass'}], 2: ...}` - # where the keys are {Marker} 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) - - # Public: Adds a decoration that tracks a {Marker}. When the marker moves, + # Essential: Adds a decoration that tracks a {Marker}. When the marker moves, # is invalidated, or is destroyed, the decoration will be updated to reflect # the marker's state. # @@ -1629,6 +1662,19 @@ class Editor extends Model decorateMarker: (marker, decorationParams) -> @displayBuffer.decorateMarker(marker, decorationParams) + # Extended: Get all the decorations within a screen row range. + # + # * `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: 'gutter', class: 'someclass'}], 2: ...}` + # where the keys are {Marker} 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) + decorationForId: (id) -> @displayBuffer.decorationForId(id) @@ -1636,15 +1682,43 @@ class Editor extends Model Section: Markers ### - # Public: Get the {DisplayBufferMarker} for the given marker id. - getMarker: (id) -> - @displayBuffer.getMarker(id) + # Essential: Mark the given range in buffer coordinates. + # + # * `range` A {Range} or range-compatible {Array}. + # * `options` (optional) See {TextBuffer::markRange}. + # + # Returns a {DisplayBufferMarker}. + markBufferRange: (args...) -> + @displayBuffer.markBufferRange(args...) - # Public: Get all {DisplayBufferMarker}s. - getMarkers: -> - @displayBuffer.getMarkers() + # Essential: Mark the given range in screen coordinates. + # + # * `range` A {Range} or range-compatible {Array}. + # * `options` (optional) See {TextBuffer::markRange}. + # + # Returns a {DisplayBufferMarker}. + markScreenRange: (args...) -> + @displayBuffer.markScreenRange(args...) - # Public: Find all {DisplayBufferMarker}s that match the given properties. + # Essential: Mark the given position in buffer coordinates. + # + # * `position` A {Point} or {Array} of `[row, column]`. + # * `options` (optional) See {TextBuffer::markRange}. + # + # Returns a {DisplayBufferMarker}. + markBufferPosition: (args...) -> + @displayBuffer.markBufferPosition(args...) + + # Essential: Mark the given position in screen coordinates. + # + # * `position` A {Point} or {Array} of `[row, column]`. + # * `options` (optional) See {TextBuffer::markRange}. + # + # Returns a {DisplayBufferMarker}. + markScreenPosition: (args...) -> + @displayBuffer.markScreenPosition(args...) + + # Essential: Find all {DisplayBufferMarker}s 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. @@ -1666,52 +1740,23 @@ class Editor extends Model findMarkers: (properties) -> @displayBuffer.findMarkers(properties) - # Public: Mark the given range in screen coordinates. - # - # * `range` A {Range} or range-compatible {Array}. - # * `options` (optional) See {TextBuffer::markRange}. - # - # Returns a {DisplayBufferMarker}. - markScreenRange: (args...) -> - @displayBuffer.markScreenRange(args...) + # Extended: Get the {DisplayBufferMarker} for the given marker id. + getMarker: (id) -> + @displayBuffer.getMarker(id) - # Public: Mark the given range in buffer coordinates. - # - # * `range` A {Range} or range-compatible {Array}. - # * `options` (optional) See {TextBuffer::markRange}. - # - # Returns a {DisplayBufferMarker}. - markBufferRange: (args...) -> - @displayBuffer.markBufferRange(args...) + # Extended: Get all {DisplayBufferMarker}s. + getMarkers: -> + @displayBuffer.getMarkers() - # Public: Mark the given position in screen coordinates. - # - # * `position` A {Point} or {Array} of `[row, column]`. - # * `options` (optional) See {TextBuffer::markRange}. - # - # Returns a {DisplayBufferMarker}. - markScreenPosition: (args...) -> - @displayBuffer.markScreenPosition(args...) - - # Public: Mark the given position in buffer coordinates. - # - # * `position` A {Point} or {Array} of `[row, column]`. - # * `options` (optional) See {TextBuffer::markRange}. - # - # Returns a {DisplayBufferMarker}. - markBufferPosition: (args...) -> - @displayBuffer.markBufferPosition(args...) - - # {Delegates to: DisplayBuffer.destroyMarker} - destroyMarker: (args...) -> - @displayBuffer.destroyMarker(args...) - - # Public: Get the number of markers in this editor's buffer. + # Extended: Get the number of markers in this editor's buffer. # # Returns a {Number}. getMarkerCount: -> @buffer.getMarkerCount() + # {Delegates to: DisplayBuffer.destroyMarker} + destroyMarker: (args...) -> + @displayBuffer.destroyMarker(args...) ### Section: Cursors @@ -2539,7 +2584,6 @@ class Editor extends Model getRowsPerPage: -> Math.max(1, Math.ceil(@getHeight() / @getLineHeightInPixels())) - ### Section: Config ### @@ -2556,7 +2600,6 @@ class Editor extends Model else @displayBuffer.setInvisibles(null) - ### Section: Event Handlers ### diff --git a/src/git.coffee b/src/git.coffee index 78916d343..4093f8890 100644 --- a/src/git.coffee +++ b/src/git.coffee @@ -10,7 +10,7 @@ GitUtils = require 'git-utils' Task = require './task' -# Public: Represents the underlying git operations performed by Atom. +# Extended: Represents the underlying git operations performed by Atom. # # This class shouldn't be instantiated directly but instead by accessing the # `atom.project` global and calling `getRepo()`. Note that this will only be @@ -47,8 +47,15 @@ class Git EmitterMixin.includeInto(this) Subscriber.includeInto(this) + @exists: (path) -> + if git = @open(path) + git.destroy() + true + else + false + ### - Section: Class Methods + Section: Construction and Destruction ### # Public: Creates a new Git instance. @@ -66,17 +73,6 @@ class Git catch null - @exists: (path) -> - if git = @open(path) - git.destroy() - true - else - false - - ### - Section: Construction - ### - constructor: (path, options={}) -> @emitter = new Emitter @repo = GitUtils.open(path) @@ -100,11 +96,26 @@ class Git if @project? @subscribe @project.eachBuffer (buffer) => @subscribeToBuffer(buffer) + # Public: Destroy this {Git} object. + # + # This destroys any tasks and subscriptions and releases the underlying + # libgit2 repository handle. + destroy: -> + if @statusTask? + @statusTask.terminate() + @statusTask = null + + if @repo? + @repo.release() + @repo = null + + @unsubscribe() + ### Section: Event Subscription ### - # Essential: Invoke the given callback when a specific file's status has + # Public: Invoke the given callback when a specific file's status has # changed. When a file is updated, reloaded, etc, and the status changes, this # will be fired. # @@ -118,7 +129,7 @@ class Git onDidChangeStatus: (callback) -> @emitter.on 'did-change-status', callback - # Essential: Invoke the given callback when a multiple files' statuses have + # Public: Invoke the given callback when a multiple files' statuses have # changed. For example, on window focus, the status of all the paths in the # repo is checked. If any of them have changed, this will be fired. Call # {::getPathStatus(path)} to get the status for your path of choice. @@ -140,7 +151,250 @@ class Git EmitterMixin::on.apply(this, arguments) ### - Section: Instance Methods + Section: Repository Data + ### + + # Public: Returns the {String} path of the repository. + getPath: -> + @path ?= fs.absolute(@getRepo().getPath()) + + # Public: Returns the {String} working directory path of the repository. + getWorkingDirectory: -> @getRepo().getWorkingDirectory() + + # Public: Returns true if at the root, false if in a subfolder of the + # repository. + isProjectAtRoot: -> + @projectAtRoot ?= @project?.relativize(@getWorkingDirectory()) is '' + + # Public: Makes a path relative to the repository's working directory. + relativize: (path) -> @getRepo().relativize(path) + + # Public: Returns true if the given branch exists. + hasBranch: (branch) -> @getReferenceTarget("refs/heads/#{branch}")? + + # Public: Retrieves a shortened version of the HEAD reference value. + # + # This removes the leading segments of `refs/heads`, `refs/tags`, or + # `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7 + # characters. + # + # * `path` An optional {String} path in the repository to get this information + # for, only needed if the repository contains submodules. + # + # Returns a {String}. + getShortHead: (path) -> @getRepo(path).getShortHead() + + # Public: Is the given path a submodule in the repository? + # + # * `path` The {String} path to check. + # + # Returns a {Boolean}. + isSubmodule: (path) -> + return false unless path + + repo = @getRepo(path) + if repo.isSubmodule(repo.relativize(path)) + true + else + # Check if the path is a working directory in a repo that isn't the root. + repo isnt @getRepo() and repo.relativize(join(path, 'dir')) is 'dir' + + # Public: Returns the number of commits behind the current branch is from the + # its upstream remote branch. + # + # * `reference` The {String} branch reference name. + # * `path` The {String} path in the repository to get this information for, + # only needed if the repository contains submodules. + getAheadBehindCount: (reference, path) -> + @getRepo(path).getAheadBehindCount(reference) + + # Public: Get the cached ahead/behind commit counts for the current branch's + # upstream branch. + # + # * `path` An optional {String} path in the repository to get this information + # for, only needed if the repository has submodules. + # + # Returns an {Object} with the following keys: + # * `ahead` The {Number} of commits ahead. + # * `behind` The {Number} of commits behind. + getCachedUpstreamAheadBehindCount: (path) -> + @getRepo(path).upstream ? @upstream + + # Public: Returns the git configuration value specified by the key. + # + # * `path` An optional {String} path in the repository to get this information + # for, only needed if the repository has submodules. + getConfigValue: (key, path) -> @getRepo(path).getConfigValue(key) + + # Public: Returns the origin url of the repository. + # + # * `path` (optional) {String} path in the repository to get this information + # for, only needed if the repository has submodules. + getOriginUrl: (path) -> @getConfigValue('remote.origin.url', path) + + # Public: Returns the upstream branch for the current HEAD, or null if there + # is no upstream branch for the current HEAD. + # + # * `path` An optional {String} path in the repo to get this information for, + # only needed if the repository contains submodules. + # + # Returns a {String} branch name such as `refs/remotes/origin/master`. + getUpstreamBranch: (path) -> @getRepo(path).getUpstreamBranch() + + # Public: Gets all the local and remote references. + # + # * `path` An optional {String} path in the repository to get this information + # for, only needed if the repository has submodules. + # + # Returns an {Object} with the following keys: + # * `heads` An {Array} of head reference names. + # * `remotes` An {Array} of remote reference names. + # * `tags` An {Array} of tag reference names. + getReferences: (path) -> @getRepo(path).getReferences() + + # Public: Returns the current {String} SHA for the given reference. + # + # * `reference` The {String} reference to get the target of. + # * `path` An optional {String} path in the repo to get the reference target + # for. Only needed if the repository contains submodules. + getReferenceTarget: (reference, path) -> + @getRepo(path).getReferenceTarget(reference) + + ### + Section: Reading Status + ### + + # Public: Returns true if the given path is modified. + isPathModified: (path) -> @isStatusModified(@getPathStatus(path)) + + # Public: Returns true if the given path is new. + isPathNew: (path) -> @isStatusNew(@getPathStatus(path)) + + # Public: Is the given path ignored? + # + # Returns a {Boolean}. + isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path)) + + # Public: Get the status of a directory in the repository's working directory. + # + # * `path` The {String} path to check. + # + # Returns a {Number} representing the status. This value can be passed to + # {::isStatusModified} or {::isStatusNew} to get more information. + getDirectoryStatus: (directoryPath) -> + directoryPath = "#{@relativize(directoryPath)}/" + directoryStatus = 0 + for path, status of @statuses + directoryStatus |= status if path.indexOf(directoryPath) is 0 + directoryStatus + + # Public: Get the status of a single path in the repository. + # + # `path` A {String} repository-relative path. + # + # Returns a {Number} representing the status. This value can be passed to + # {::isStatusModified} or {::isStatusNew} to get more information. + getPathStatus: (path) -> + repo = @getRepo(path) + relativePath = @relativize(path) + currentPathStatus = @statuses[relativePath] ? 0 + pathStatus = repo.getStatus(repo.relativize(path)) ? 0 + pathStatus = 0 if repo.isStatusIgnored(pathStatus) + if pathStatus > 0 + @statuses[relativePath] = pathStatus + else + delete @statuses[relativePath] + if currentPathStatus isnt pathStatus + @emit 'status-changed', path, pathStatus + @emitter.emit 'did-change-status', {path, pathStatus} + + pathStatus + + # Public: Get the cached status for the given path. + # + # * `path` A {String} path in the repository, relative or absolute. + # + # Returns a status {Number} or null if the path is not in the cache. + getCachedPathStatus: (path) -> + @statuses[@relativize(path)] + + # Public: Returns true if the given status indicates modification. + isStatusModified: (status) -> @getRepo().isStatusModified(status) + + # Public: Returns true if the given status indicates a new path. + isStatusNew: (status) -> @getRepo().isStatusNew(status) + + ### + Section: Retrieving Diffs + ### + + # Public: Retrieves the number of lines added and removed to a path. + # + # This compares the working directory contents of the path to the `HEAD` + # version. + # + # * `path` The {String} path to check. + # + # Returns an {Object} with the following keys: + # * `added` The {Number} of added lines. + # * `deleted` The {Number} of deleted lines. + getDiffStats: (path) -> + repo = @getRepo(path) + repo.getDiffStats(repo.relativize(path)) + + # Public: Retrieves the line diffs comparing the `HEAD` version of the given + # path and the given text. + # + # * `path` The {String} path relative to the repository. + # * `text` The {String} to compare against the `HEAD` contents + # + # Returns an {Array} of hunk {Object}s with the following keys: + # * `oldStart` The line {Number} of the old hunk. + # * `newStart` The line {Number} of the new hunk. + # * `oldLines` The {Number} of lines in the old hunk. + # * `newLines` The {Number} of lines in the new hunk + getLineDiffs: (path, text) -> + # Ignore eol of line differences on windows so that files checked in as + # LF don't report every line modified when the text contains CRLF endings. + options = ignoreEolWhitespace: process.platform is 'win32' + repo = @getRepo(path) + repo.getLineDiffs(repo.relativize(path), text, options) + + ### + Section: Checking Out + ### + + # Public: Restore the contents of a path in the working directory and index + # to the version at `HEAD`. + # + # This is essentially the same as running: + # + # ```sh + # git reset HEAD -- + # git checkout HEAD -- + # ``` + # + # * `path` The {String} path to checkout. + # + # Returns a {Boolean} that's true if the method was successful. + checkoutHead: (path) -> + repo = @getRepo(path) + headCheckedOut = repo.checkoutHead(repo.relativize(path)) + @getPathStatus(path) if headCheckedOut + headCheckedOut + + # Public: Checks out a branch in your repository. + # + # * `reference` The {String} reference to checkout. + # * `create` A {Boolean} value which, if true creates the new reference if + # it doesn't exist. + # + # Returns a Boolean that's true if the method was successful. + checkoutReference: (reference, create) -> + @getRepo().checkoutReference(reference, create) + + ### + Section: Private ### # Subscribes to buffer events. @@ -175,21 +429,6 @@ class Git else checkoutHead() - # Public: Destroy this {Git} object. - # - # This destroys any tasks and subscriptions and releases the underlying - # libgit2 repository handle. - destroy: -> - if @statusTask? - @statusTask.terminate() - @statusTask = null - - if @repo? - @repo.release() - @repo = null - - @unsubscribe() - # Returns the corresponding {Repository} getRepo: (path) -> if @repo? @@ -201,233 +440,6 @@ class Git # last time the index was read. refreshIndex: -> @getRepo().refreshIndex() - # Public: Returns the {String} path of the repository. - getPath: -> - @path ?= fs.absolute(@getRepo().getPath()) - - # Public: Returns the {String} working directory path of the repository. - getWorkingDirectory: -> @getRepo().getWorkingDirectory() - - # Public: Get the status of a single path in the repository. - # - # `path` A {String} repository-relative path. - # - # Returns a {Number} representing the status. This value can be passed to - # {::isStatusModified} or {::isStatusNew} to get more information. - getPathStatus: (path) -> - repo = @getRepo(path) - relativePath = @relativize(path) - currentPathStatus = @statuses[relativePath] ? 0 - pathStatus = repo.getStatus(repo.relativize(path)) ? 0 - pathStatus = 0 if repo.isStatusIgnored(pathStatus) - if pathStatus > 0 - @statuses[relativePath] = pathStatus - else - delete @statuses[relativePath] - if currentPathStatus isnt pathStatus - @emit 'status-changed', path, pathStatus - @emitter.emit 'did-change-status', {path, pathStatus} - - pathStatus - - # Public: Is the given path ignored? - # - # Returns a {Boolean}. - isPathIgnored: (path) -> @getRepo().isIgnored(@relativize(path)) - - # Public: Returns true if the given status indicates modification. - isStatusModified: (status) -> @getRepo().isStatusModified(status) - - # Public: Returns true if the given path is modified. - isPathModified: (path) -> @isStatusModified(@getPathStatus(path)) - - # Public: Returns true if the given status indicates a new path. - isStatusNew: (status) -> @getRepo().isStatusNew(status) - - # Public: Returns true if the given path is new. - isPathNew: (path) -> @isStatusNew(@getPathStatus(path)) - - # Public: Returns true if at the root, false if in a subfolder of the - # repository. - isProjectAtRoot: -> - @projectAtRoot ?= @project?.relativize(@getWorkingDirectory()) is '' - - # Public: Makes a path relative to the repository's working directory. - relativize: (path) -> @getRepo().relativize(path) - - # Public: Retrieves a shortened version of the HEAD reference value. - # - # This removes the leading segments of `refs/heads`, `refs/tags`, or - # `refs/remotes`. It also shortens the SHA-1 of a detached `HEAD` to 7 - # characters. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository contains submodules. - # - # Returns a {String}. - getShortHead: (path) -> @getRepo(path).getShortHead() - - # Public: Restore the contents of a path in the working directory and index - # to the version at `HEAD`. - # - # This is essentially the same as running: - # - # ```sh - # git reset HEAD -- - # git checkout HEAD -- - # ``` - # - # * `path` The {String} path to checkout. - # - # Returns a {Boolean} that's true if the method was successful. - checkoutHead: (path) -> - repo = @getRepo(path) - headCheckedOut = repo.checkoutHead(repo.relativize(path)) - @getPathStatus(path) if headCheckedOut - headCheckedOut - - # Public: Checks out a branch in your repository. - # - # * `reference` The {String} reference to checkout. - # * `create` A {Boolean} value which, if true creates the new reference if - # it doesn't exist. - # - # Returns a Boolean that's true if the method was successful. - checkoutReference: (reference, create) -> - @getRepo().checkoutReference(reference, create) - - # Public: Retrieves the number of lines added and removed to a path. - # - # This compares the working directory contents of the path to the `HEAD` - # version. - # - # * `path` The {String} path to check. - # - # Returns an {Object} with the following keys: - # * `added` The {Number} of added lines. - # * `deleted` The {Number} of deleted lines. - getDiffStats: (path) -> - repo = @getRepo(path) - repo.getDiffStats(repo.relativize(path)) - - # Public: Is the given path a submodule in the repository? - # - # * `path` The {String} path to check. - # - # Returns a {Boolean}. - isSubmodule: (path) -> - return false unless path - - repo = @getRepo(path) - if repo.isSubmodule(repo.relativize(path)) - true - else - # Check if the path is a working directory in a repo that isn't the root. - repo isnt @getRepo() and repo.relativize(join(path, 'dir')) is 'dir' - - # Public: Get the status of a directory in the repository's working directory. - # - # * `path` The {String} path to check. - # - # Returns a {Number} representing the status. This value can be passed to - # {::isStatusModified} or {::isStatusNew} to get more information. - getDirectoryStatus: (directoryPath) -> - directoryPath = "#{@relativize(directoryPath)}/" - directoryStatus = 0 - for path, status of @statuses - directoryStatus |= status if path.indexOf(directoryPath) is 0 - directoryStatus - - # Public: Retrieves the line diffs comparing the `HEAD` version of the given - # path and the given text. - # - # * `path` The {String} path relative to the repository. - # * `text` The {String} to compare against the `HEAD` contents - # - # Returns an {Array} of hunk {Object}s with the following keys: - # * `oldStart` The line {Number} of the old hunk. - # * `newStart` The line {Number} of the new hunk. - # * `oldLines` The {Number} of lines in the old hunk. - # * `newLines` The {Number} of lines in the new hunk - getLineDiffs: (path, text) -> - # Ignore eol of line differences on windows so that files checked in as - # LF don't report every line modified when the text contains CRLF endings. - options = ignoreEolWhitespace: process.platform is 'win32' - repo = @getRepo(path) - repo.getLineDiffs(repo.relativize(path), text, options) - - # Public: Returns the git configuration value specified by the key. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. - getConfigValue: (key, path) -> @getRepo(path).getConfigValue(key) - - # Public: Returns the origin url of the repository. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. - getOriginUrl: (path) -> @getConfigValue('remote.origin.url', path) - - # Public: Returns the upstream branch for the current HEAD, or null if there - # is no upstream branch for the current HEAD. - # - # * `path` An optional {String} path in the repo to get this information for, - # only needed if the repository contains submodules. - # - # Returns a {String} branch name such as `refs/remotes/origin/master`. - getUpstreamBranch: (path) -> @getRepo(path).getUpstreamBranch() - - # Public: Returns the current {String} SHA for the given reference. - # - # * `reference` The {String} reference to get the target of. - # * `path` An optional {String} path in the repo to get the reference target - # for. Only needed if the repository contains submodules. - getReferenceTarget: (reference, path) -> - @getRepo(path).getReferenceTarget(reference) - - # Public: Gets all the local and remote references. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. - # - # Returns an {Object} with the following keys: - # * `heads` An {Array} of head reference names. - # * `remotes` An {Array} of remote reference names. - # * `tags` An {Array} of tag reference names. - getReferences: (path) -> @getRepo(path).getReferences() - - # Public: Returns the number of commits behind the current branch is from the - # its upstream remote branch. - # - # * `reference` The {String} branch reference name. - # * `path` The {String} path in the repository to get this information for, - # only needed if the repository contains submodules. - getAheadBehindCount: (reference, path) -> - @getRepo(path).getAheadBehindCount(reference) - - # Public: Get the cached ahead/behind commit counts for the current branch's - # upstream branch. - # - # * `path` An optional {String} path in the repository to get this information - # for, only needed if the repository has submodules. - # - # Returns an {Object} with the following keys: - # * `ahead` The {Number} of commits ahead. - # * `behind` The {Number} of commits behind. - getCachedUpstreamAheadBehindCount: (path) -> - @getRepo(path).upstream ? @upstream - - # Public: Get the cached status for the given path. - # - # * `path` A {String} path in the repository, relative or absolute. - # - # Returns a status {Number} or null if the path is not in the cache. - getCachedPathStatus: (path) -> - @statuses[@relativize(path)] - - # Public: Returns true if the given branch exists. - hasBranch: (branch) -> @getReferenceTarget("refs/heads/#{branch}")? - # Refreshes the current git status in an outside process and asynchronously # updates the relevant properties. refreshStatus: -> diff --git a/src/menu-manager.coffee b/src/menu-manager.coffee index da5ae6507..89462ec7a 100644 --- a/src/menu-manager.coffee +++ b/src/menu-manager.coffee @@ -5,7 +5,7 @@ ipc = require 'ipc' CSON = require 'season' fs = require 'fs-plus' -# Public: Provides a registry for menu items that you'd like to appear in the +# Extended: Provides a registry for menu items that you'd like to appear in the # application menu. # # An instance of this class is always available as the `atom.menu` global. diff --git a/src/package-manager.coffee b/src/package-manager.coffee index f95073e10..838eda68e 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -48,7 +48,7 @@ class PackageManager Section: Event Subscription ### - # Essential: Invoke the given callback when all packages have been activated. + # Public: Invoke the given callback when all packages have been activated. # # * `callback` {Function} # @@ -56,7 +56,7 @@ class PackageManager onDidLoadAll: (callback) -> @emitter.on 'did-load-all', callback - # Essential: Invoke the given callback when all packages have been activated. + # Public: Invoke the given callback when all packages have been activated. # # * `callback` {Function} # @@ -75,10 +75,10 @@ class PackageManager EmitterMixin::on.apply(this, arguments) ### - Section: Instance Methods + Section: Package system data ### - # Extended: Get the path to the apm command. + # Public: Get the path to the apm command. # # Return a {String} file path to apm. getApmPath: -> @@ -86,19 +86,43 @@ class PackageManager commandName += '.cmd' if process.platform is 'win32' @apmPath ?= path.resolve(__dirname, '..', 'apm', 'node_modules', 'atom-package-manager', 'bin', commandName) - # Extended: Get the paths being used to look for packages. + # Public: Get the paths being used to look for packages. # # Returns an {Array} of {String} directory paths. getPackageDirPaths: -> _.clone(@packageDirPaths) - getPackageState: (name) -> - @packageStates[name] + ### + Section: General package data + ### - setPackageState: (name, state) -> - @packageStates[name] = state + # Public: Resolve the given package name to a path on disk. + # + # * `name` - The {String} package name. + # + # Return a {String} folder path or undefined if it could not be resolved. + resolvePackagePath: (name) -> + return name if fs.isDirectorySync(name) - # Extended: Enable the package with the given name. + packagePath = fs.resolve(@packageDirPaths..., name) + return packagePath if fs.isDirectorySync(packagePath) + + packagePath = path.join(@resourcePath, 'node_modules', name) + return packagePath if @hasAtomEngine(packagePath) + + # Public: Is the package with the given name bundled with Atom? + # + # * `name` - The {String} package name. + # + # Returns a {Boolean}. + isBundledPackage: (name) -> + @getPackageDependencies().hasOwnProperty(name) + + ### + Section: Enabling and disabling packages + ### + + # Public: Enable the package with the given name. # # Returns the {Package} that was enabled or null if it isn't loaded. enablePackage: (name) -> @@ -106,7 +130,7 @@ class PackageManager pack?.enable() pack - # Extended: Disable the package with the given name. + # Public: Disable the package with the given name. # # Returns the {Package} that was disabled or null if it isn't loaded. disablePackage: (name) -> @@ -114,51 +138,23 @@ class PackageManager pack?.disable() pack - # Activate all the packages that should be activated. - activate: -> - for [activator, types] in @packageActivators - packages = @getLoadedPackagesForTypes(types) - activator.activatePackages(packages) - @emit 'activated' - @emitter.emit 'did-activate-all' + # Public: Is the package with the given name disabled? + # + # * `name` - The {String} package name. + # + # Returns a {Boolean}. + isPackageDisabled: (name) -> + _.include(atom.config.get('core.disabledPackages') ? [], name) - # another type of package manager can handle other package types. - # See ThemeManager - registerPackageActivator: (activator, types) -> - @packageActivators.push([activator, types]) + ### + Section: Accessing active packages + ### - activatePackages: (packages) -> - @activatePackage(pack.name) for pack in packages - @observeDisabledPackages() - - # Activate a single package by name - activatePackage: (name) -> - if pack = @getActivePackage(name) - Q(pack) - else - pack = @loadPackage(name) - pack.activate().then => - @activePackages[pack.name] = pack - pack - - # Deactivate all packages - deactivatePackages: -> - @deactivatePackage(pack.name) for pack in @getLoadedPackages() - @unobserveDisabledPackages() - - # Deactivate the package with the given name - deactivatePackage: (name) -> - pack = @getLoadedPackage(name) - if @isPackageActive(name) - @setPackageState(pack.name, state) if state = pack.serialize?() - pack.deactivate() - delete @activePackages[pack.name] - - # Essential: Get an {Array} of all the active {Package}s. + # Public: Get an {Array} of all the active {Package}s. getActivePackages: -> _.values(@activePackages) - # Essential: Get the active {Package} with the given name. + # Public: Get the active {Package} with the given name. # # * `name` - The {String} package name. # @@ -174,6 +170,91 @@ class PackageManager isPackageActive: (name) -> @getActivePackage(name)? + ### + Section: Accessing loaded packages + ### + + # Public: Get an {Array} of all the loaded {Package}s + getLoadedPackages: -> + _.values(@loadedPackages) + + # Get packages for a certain package type + # + # * `types` an {Array} of {String}s like ['atom', 'textmate']. + getLoadedPackagesForTypes: (types) -> + pack for pack in @getLoadedPackages() when pack.getType() in types + + # Public: Get the loaded {Package} with the given name. + # + # * `name` - The {String} package name. + # + # Returns a {Package} or undefined. + getLoadedPackage: (name) -> + @loadedPackages[name] + + # Public: Is the package with the given name loaded? + # + # * `name` - The {String} package name. + # + # Returns a {Boolean}. + isPackageLoaded: (name) -> + @getLoadedPackage(name)? + + ### + Section: Accessing available packages + ### + + # Public: Get an {Array} of {String}s of all the available package paths. + getAvailablePackagePaths: -> + packagePaths = [] + + for packageDirPath in @packageDirPaths + for packagePath in fs.listSync(packageDirPath) + packagePaths.push(packagePath) if fs.isDirectorySync(packagePath) + + packagesPath = path.join(@resourcePath, 'node_modules') + for packageName, packageVersion of @getPackageDependencies() + packagePath = path.join(packagesPath, packageName) + packagePaths.push(packagePath) if fs.isDirectorySync(packagePath) + + _.uniq(packagePaths) + + # Public: Get an {Array} of {String}s of all the available package names. + getAvailablePackageNames: -> + _.uniq _.map @getAvailablePackagePaths(), (packagePath) -> path.basename(packagePath) + + # Public: Get an {Array} of {String}s of all the available package metadata. + getAvailablePackageMetadata: -> + packages = [] + for packagePath in @getAvailablePackagePaths() + name = path.basename(packagePath) + metadata = @getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true) + packages.push(metadata) + packages + + ### + Section: Private + ### + + getPackageState: (name) -> + @packageStates[name] + + setPackageState: (name, state) -> + @packageStates[name] = state + + getPackageDependencies: -> + unless @packageDependencies? + try + metadataPath = path.join(@resourcePath, 'package.json') + {@packageDependencies} = JSON.parse(fs.readFileSync(metadataPath)) ? {} + @packageDependencies ?= {} + + @packageDependencies + + hasAtomEngine: (packagePath) -> + metadata = Package.loadMetadata(packagePath, true) + metadata?.engines?.atom? + unobserveDisabledPackages: -> @disabledPackagesSubscription?.off() @disabledPackagesSubscription = null @@ -234,99 +315,42 @@ class PackageManager else throw new Error("No loaded package for name '#{name}'") - # Essential: Get the loaded {Package} with the given name. - # - # * `name` - The {String} package name. - # - # Returns a {Package} or undefined. - getLoadedPackage: (name) -> - @loadedPackages[name] + # Activate all the packages that should be activated. + activate: -> + for [activator, types] in @packageActivators + packages = @getLoadedPackagesForTypes(types) + activator.activatePackages(packages) + @emit 'activated' + @emitter.emit 'did-activate-all' - # Essential: Is the package with the given name loaded? - # - # * `name` - The {String} package name. - # - # Returns a {Boolean}. - isPackageLoaded: (name) -> - @getLoadedPackage(name)? + # another type of package manager can handle other package types. + # See ThemeManager + registerPackageActivator: (activator, types) -> + @packageActivators.push([activator, types]) - # Essential: Get an {Array} of all the loaded {Package}s - getLoadedPackages: -> - _.values(@loadedPackages) + activatePackages: (packages) -> + @activatePackage(pack.name) for pack in packages + @observeDisabledPackages() - # Get packages for a certain package type - # - # * `types` an {Array} of {String}s like ['atom', 'textmate']. - getLoadedPackagesForTypes: (types) -> - pack for pack in @getLoadedPackages() when pack.getType() in types + # Activate a single package by name + activatePackage: (name) -> + if pack = @getActivePackage(name) + Q(pack) + else + pack = @loadPackage(name) + pack.activate().then => + @activePackages[pack.name] = pack + pack - # Extended: Resolve the given package name to a path on disk. - # - # * `name` - The {String} package name. - # - # Return a {String} folder path or undefined if it could not be resolved. - resolvePackagePath: (name) -> - return name if fs.isDirectorySync(name) + # Deactivate all packages + deactivatePackages: -> + @deactivatePackage(pack.name) for pack in @getLoadedPackages() + @unobserveDisabledPackages() - packagePath = fs.resolve(@packageDirPaths..., name) - return packagePath if fs.isDirectorySync(packagePath) - - packagePath = path.join(@resourcePath, 'node_modules', name) - return packagePath if @hasAtomEngine(packagePath) - - # Essential: Is the package with the given name disabled? - # - # * `name` - The {String} package name. - # - # Returns a {Boolean}. - isPackageDisabled: (name) -> - _.include(atom.config.get('core.disabledPackages') ? [], name) - - hasAtomEngine: (packagePath) -> - metadata = Package.loadMetadata(packagePath, true) - metadata?.engines?.atom? - - # Extended: Is the package with the given name bundled with Atom? - # - # * `name` - The {String} package name. - # - # Returns a {Boolean}. - isBundledPackage: (name) -> - @getPackageDependencies().hasOwnProperty(name) - - getPackageDependencies: -> - unless @packageDependencies? - try - metadataPath = path.join(@resourcePath, 'package.json') - {@packageDependencies} = JSON.parse(fs.readFileSync(metadataPath)) ? {} - @packageDependencies ?= {} - - @packageDependencies - - # Extended: Get an {Array} of {String}s of all the available package paths. - getAvailablePackagePaths: -> - packagePaths = [] - - for packageDirPath in @packageDirPaths - for packagePath in fs.listSync(packageDirPath) - packagePaths.push(packagePath) if fs.isDirectorySync(packagePath) - - packagesPath = path.join(@resourcePath, 'node_modules') - for packageName, packageVersion of @getPackageDependencies() - packagePath = path.join(packagesPath, packageName) - packagePaths.push(packagePath) if fs.isDirectorySync(packagePath) - - _.uniq(packagePaths) - - # Extended: Get an {Array} of {String}s of all the available package names. - getAvailablePackageNames: -> - _.uniq _.map @getAvailablePackagePaths(), (packagePath) -> path.basename(packagePath) - - # Extended: Get an {Array} of {String}s of all the available package metadata. - getAvailablePackageMetadata: -> - packages = [] - for packagePath in @getAvailablePackagePaths() - name = path.basename(packagePath) - metadata = @getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true) - packages.push(metadata) - packages + # Deactivate the package with the given name + deactivatePackage: (name) -> + pack = @getLoadedPackage(name) + if @isPackageActive(name) + @setPackageState(pack.name, state) if state = pack.serialize?() + pack.deactivate() + delete @activePackages[pack.name] diff --git a/src/project.coffee b/src/project.coffee index 371a7af3e..361d9c950 100644 --- a/src/project.coffee +++ b/src/project.coffee @@ -23,14 +23,16 @@ class Project extends Model atom.deserializers.add(this) Serializable.includeInto(this) - # Public: Find the local path for the given repository URL. - # - # * `repoUrl` {String} url to a git repository @pathForRepositoryUrl: (repoUrl) -> + deprecate '::pathForRepositoryUrl will be removed. Please remove from your code.' [repoName] = url.parse(repoUrl).path.split('/')[-1..] repoName = repoName.replace(/\.git$/, '') path.join(atom.config.get('core.projectHome'), repoName) + ### + Section: Construction and Destruction + ### + constructor: ({path, @buffers}={}) -> @buffers ?= [] @@ -40,14 +42,6 @@ class Project extends Model @setPath(path) - serializeParams: -> - path: @path - buffers: _.compact(@buffers.map (buffer) -> buffer.serialize() if buffer.isRetained()) - - deserializeParams: (params) -> - params.buffers = params.buffers.map (bufferState) -> atom.deserializers.deserialize(bufferState) - params - destroyed: -> buffer.destroy() for buffer in @getBuffers() @destroyRepo() @@ -60,9 +54,29 @@ class Project extends Model destroyUnretainedBuffers: -> buffer.destroy() for buffer in @getBuffers() when not buffer.isRetained() + ### + Section: Serialization + ### + + serializeParams: -> + path: @path + buffers: _.compact(@buffers.map (buffer) -> buffer.serialize() if buffer.isRetained()) + + deserializeParams: (params) -> + params.buffers = params.buffers.map (bufferState) -> atom.deserializers.deserialize(bufferState) + params + + ### + Section: Accessing the git repository + ### + # Public: Returns the {Git} repository if available. getRepo: -> @repo + ### + Section: Managing Paths + ### + # Public: Returns the project's {String} fullpath. getPath: -> @rootDirectory?.path @@ -124,6 +138,99 @@ class Project extends Model contains: (pathToCheck) -> @rootDirectory?.contains(pathToCheck) ? false + ### + Section: Searching and Replacing + ### + + # Public: Performs a search across all the files in the project. + # + # * `regex` {RegExp} to search with. + # * `options` (optional) {Object} (default: {}) + # * `paths` An {Array} of glob patterns to search within + # * `iterator` {Function} callback on each file found + scan: (regex, options={}, iterator) -> + if _.isFunction(options) + iterator = options + options = {} + + deferred = Q.defer() + + searchOptions = + ignoreCase: regex.ignoreCase + inclusions: options.paths + includeHidden: true + excludeVcsIgnores: atom.config.get('core.excludeVcsIgnoredPaths') + exclusions: atom.config.get('core.ignoredNames') + + task = Task.once require.resolve('./scan-handler'), @getPath(), regex.source, searchOptions, -> + deferred.resolve() + + task.on 'scan:result-found', (result) => + iterator(result) unless @isPathModified(result.filePath) + + task.on 'scan:file-error', (error) -> + iterator(null, error) + + if _.isFunction(options.onPathsSearched) + task.on 'scan:paths-searched', (numberOfPathsSearched) -> + options.onPathsSearched(numberOfPathsSearched) + + for buffer in @getBuffers() when buffer.isModified() + filePath = buffer.getPath() + continue unless @contains(filePath) + matches = [] + buffer.scan regex, (match) -> matches.push match + iterator {filePath, matches} if matches.length > 0 + + promise = deferred.promise + promise.cancel = -> + task.terminate() + deferred.resolve('cancelled') + promise + + # Public: Performs a replace across all the specified files in the project. + # + # * `regex` A {RegExp} to search with. + # * `replacementText` Text to replace all matches of regex with + # * `filePaths` List of file path strings to run the replace on. + # * `iterator` A {Function} callback on each file with replacements: + # * `options` {Object} with keys `filePath` and `replacements` + replace: (regex, replacementText, filePaths, iterator) -> + deferred = Q.defer() + + openPaths = (buffer.getPath() for buffer in @getBuffers()) + outOfProcessPaths = _.difference(filePaths, openPaths) + + inProcessFinished = !openPaths.length + outOfProcessFinished = !outOfProcessPaths.length + checkFinished = -> + deferred.resolve() if outOfProcessFinished and inProcessFinished + + unless outOfProcessFinished.length + flags = 'g' + flags += 'i' if regex.ignoreCase + + task = Task.once require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, -> + outOfProcessFinished = true + checkFinished() + + task.on 'replace:path-replaced', iterator + task.on 'replace:file-error', (error) -> iterator(null, error) + + for buffer in @getBuffers() + continue unless buffer.getPath() in filePaths + replacements = buffer.replace(regex, replacementText, iterator) + iterator({filePath: buffer.getPath(), replacements}) if replacements + + inProcessFinished = true + checkFinished() + + deferred.promise + + ### + Section: Private + ### + # Given a path to a file, this constructs and associates a new # {Editor}, showing the file. # @@ -222,91 +329,6 @@ class Project extends Model [buffer] = @buffers.splice(index, 1) buffer?.destroy() - # Public: Performs a search across all the files in the project. - # - # * `regex` {RegExp} to search with. - # * `options` (optional) {Object} (default: {}) - # * `paths` An {Array} of glob patterns to search within - # * `iterator` {Function} callback on each file found - scan: (regex, options={}, iterator) -> - if _.isFunction(options) - iterator = options - options = {} - - deferred = Q.defer() - - searchOptions = - ignoreCase: regex.ignoreCase - inclusions: options.paths - includeHidden: true - excludeVcsIgnores: atom.config.get('core.excludeVcsIgnoredPaths') - exclusions: atom.config.get('core.ignoredNames') - - task = Task.once require.resolve('./scan-handler'), @getPath(), regex.source, searchOptions, -> - deferred.resolve() - - task.on 'scan:result-found', (result) => - iterator(result) unless @isPathModified(result.filePath) - - task.on 'scan:file-error', (error) -> - iterator(null, error) - - if _.isFunction(options.onPathsSearched) - task.on 'scan:paths-searched', (numberOfPathsSearched) -> - options.onPathsSearched(numberOfPathsSearched) - - for buffer in @getBuffers() when buffer.isModified() - filePath = buffer.getPath() - continue unless @contains(filePath) - matches = [] - buffer.scan regex, (match) -> matches.push match - iterator {filePath, matches} if matches.length > 0 - - promise = deferred.promise - promise.cancel = -> - task.terminate() - deferred.resolve('cancelled') - promise - - # Public: Performs a replace across all the specified files in the project. - # - # * `regex` A {RegExp} to search with. - # * `replacementText` Text to replace all matches of regex with - # * `filePaths` List of file path strings to run the replace on. - # * `iterator` A {Function} callback on each file with replacements: - # * `options` {Object} with keys `filePath` and `replacements` - replace: (regex, replacementText, filePaths, iterator) -> - deferred = Q.defer() - - openPaths = (buffer.getPath() for buffer in @getBuffers()) - outOfProcessPaths = _.difference(filePaths, openPaths) - - inProcessFinished = !openPaths.length - outOfProcessFinished = !outOfProcessPaths.length - checkFinished = -> - deferred.resolve() if outOfProcessFinished and inProcessFinished - - unless outOfProcessFinished.length - flags = 'g' - flags += 'i' if regex.ignoreCase - - task = Task.once require.resolve('./replace-handler'), outOfProcessPaths, regex.source, flags, replacementText, -> - outOfProcessFinished = true - checkFinished() - - task.on 'replace:path-replaced', iterator - task.on 'replace:file-error', (error) -> iterator(null, error) - - for buffer in @getBuffers() - continue unless buffer.getPath() in filePaths - replacements = buffer.replace(regex, replacementText, iterator) - iterator({filePath: buffer.getPath(), replacements}) if replacements - - inProcessFinished = true - checkFinished() - - deferred.promise - buildEditorForBuffer: (buffer, editorOptions) -> editor = new Editor(_.extend({buffer, registerEditor: true}, editorOptions)) editor diff --git a/src/select-list-view.coffee b/src/select-list-view.coffee index ae5ffdc93..b3cc4a1bc 100644 --- a/src/select-list-view.coffee +++ b/src/select-list-view.coffee @@ -46,7 +46,11 @@ class SelectListView extends View inputThrottle: 50 cancelling: false - # Public: Initialize the select list view. + ### + Section: Construction + ### + + # Essential: Initialize the select list view. # # This method can be overridden by subclasses but `super` should always # be called. @@ -85,13 +89,39 @@ class SelectListView extends View @confirmSelection() if $(e.target).closest('li').hasClass('selected') e.preventDefault() - schedulePopulateList: -> - clearTimeout(@scheduleTimeout) - populateCallback = => - @populateList() if @isOnDom() - @scheduleTimeout = setTimeout(populateCallback, @inputThrottle) + ### + Section: Methods that must be overridden + ### - # Public: Set the array of items to display in the list. + # Essential: Create a view for the given model item. + # + # This method must be overridden by subclasses. + # + # This is called when the item is about to appended to the list view. + # + # * `item` The model item being rendered. This will always be one of the items + # previously passed to {::setItems}. + # + # Returns a String of HTML, DOM element, jQuery object, or View. + viewForItem: (item) -> + throw new Error("Subclass must implement a viewForItem(item) method") + + # Essential: Callback function for when an item is selected. + # + # This method must be overridden by subclasses. + # + # * `item` The selected model item. This will always be one of the items + # previously passed to {::setItems}. + # + # Returns a DOM element, jQuery object, or {View}. + confirmed: (item) -> + throw new Error("Subclass must implement a confirmed(item) method") + + ### + Section: Managing the list of items + ### + + # Essential: Set the array of items to display in the list. # # This should be model items not actual views. {::viewForItem} will be # called to render the item when it is being appended to the list view. @@ -101,30 +131,26 @@ class SelectListView extends View @populateList() @setLoading() - # Public: Set the error message to display. + # Essential: Get the model item that is currently selected in the list view. # - # * `message` The {String} error message (default: ''). - setError: (message='') -> - if message.length is 0 - @error.text('').hide() - else - @setLoading() - @error.text(message).show() + # Returns a model item. + getSelectedItem: -> + @getSelectedItemView().data('select-list-item') - # Public: Set the loading message to display. + # Extended: Get the property name to use when filtering items. # - # * `message` The {String} loading message (default: ''). - setLoading: (message='') -> - if message.length is 0 - @loading.text("") - @loadingBadge.text("") - @loadingArea.hide() - else - @setError() - @loading.text(message) - @loadingArea.show() + # This method may be overridden by classes to allow fuzzy filtering based + # on a specific property of the item objects. + # + # For example if the objects you pass to {::setItems} are of the type + # `{"id": 3, "name": "Atom"}` then you would return `"name"` from this method + # to fuzzy filter by that property when text is entered into this view's + # editor. + # + # Returns the property name to fuzzy filter by. + getFilterKey: -> - # Public: Get the filter query to use when fuzzy filtering the visible + # Extended: Get the filter query to use when fuzzy filtering the visible # elements. # # By default this method returns the text in the mini editor but it can be @@ -134,7 +160,12 @@ class SelectListView extends View getFilterQuery: -> @filterEditorView.getEditor().getText() - # Public: Populate the list view with the model items previously set by + # Extended: Set the maximum numbers of items to display in the list. + # + # * `maxItems` The maximum {Number} of items to display. + setMaxItems: (@maxItems) -> + + # Extended: Populate the list view with the model items previously set by # calling {::setItems}. # # Subclasses may override this method but should always call `super`. @@ -161,7 +192,34 @@ class SelectListView extends View else @setError(@getEmptyMessage(@items.length, filteredItems.length)) - # Public: Get the message to display when there are no items. + ### + Section: Messages to the user + ### + + # Essential: Set the error message to display. + # + # * `message` The {String} error message (default: ''). + setError: (message='') -> + if message.length is 0 + @error.text('').hide() + else + @setLoading() + @error.text(message).show() + + # Essential: Set the loading message to display. + # + # * `message` The {String} loading message (default: ''). + setLoading: (message='') -> + if message.length is 0 + @loading.text("") + @loadingBadge.text("") + @loadingArea.hide() + else + @setError() + @loading.text(message) + @loadingArea.show() + + # Extended: Get the message to display when there are no items. # # Subclasses may override this method to customize the message. # @@ -171,10 +229,36 @@ class SelectListView extends View # Returns a {String} message (default: 'No matches found'). getEmptyMessage: (itemCount, filteredItemCount) -> 'No matches found' - # Public: Set the maximum numbers of items to display in the list. + ### + Section: View Actions + ### + + # Essential: Cancel and close this select list view. # - # * `maxItems` The maximum {Number} of items to display. - setMaxItems: (@maxItems) -> + # This restores focus to the previously focused element if + # {::storeFocusedElement} was called prior to this view being attached. + cancel: -> + @list.empty() + @cancelling = true + filterEditorViewFocused = @filterEditorView.isFocused + @cancelled() + @detach() + @restoreFocus() if filterEditorViewFocused + @cancelling = false + clearTimeout(@scheduleTimeout) + + # Extended: Focus the fuzzy filter editor view. + focusFilterEditor: -> + @filterEditorView.focus() + + # Extended: Store the currently focused element. This element will be given + # back focus when {::cancel} is called. + storeFocusedElement: -> + @previouslyFocusedElement = $(':focus') + + ### + Section: Private + ### selectPreviousItemView: -> view = @getSelectedItemView().prev() @@ -202,68 +286,6 @@ class SelectListView extends View else if desiredBottom > @list.scrollBottom() @list.scrollBottom(desiredBottom) - getSelectedItemView: -> - @list.find('li.selected') - - # Public: Get the model item that is currently selected in the list view. - # - # Returns a model item. - getSelectedItem: -> - @getSelectedItemView().data('select-list-item') - - confirmSelection: -> - item = @getSelectedItem() - if item? - @confirmed(item) - else - @cancel() - - # Public: Create a view for the given model item. - # - # This method must be overridden by subclasses. - # - # This is called when the item is about to appended to the list view. - # - # * `item` The model item being rendered. This will always be one of the items - # previously passed to {::setItems}. - # - # Returns a String of HTML, DOM element, jQuery object, or View. - viewForItem: (item) -> - throw new Error("Subclass must implement a viewForItem(item) method") - - # Public: Callback function for when an item is selected. - # - # This method must be overridden by subclasses. - # - # * `item` The selected model item. This will always be one of the items - # previously passed to {::setItems}. - # - # Returns a DOM element, jQuery object, or {View}. - confirmed: (item) -> - throw new Error("Subclass must implement a confirmed(item) method") - - # Public: Get the property name to use when filtering items. - # - # This method may be overridden by classes to allow fuzzy filtering based - # on a specific property of the item objects. - # - # For example if the objects you pass to {::setItems} are of the type - # `{"id": 3, "name": "Atom"}` then you would return `"name"` from this method - # to fuzzy filter by that property when text is entered into this view's - # editor. - # - # Returns the property name to fuzzy filter by. - getFilterKey: -> - - # Public: Focus the fuzzy filter editor view. - focusFilterEditor: -> - @filterEditorView.focus() - - # Public: Store the currently focused element. This element will be given - # back focus when {::cancel} is called. - storeFocusedElement: -> - @previouslyFocusedElement = $(':focus') - restoreFocus: -> if @previouslyFocusedElement?.isOnDom() @previouslyFocusedElement.focus() @@ -273,16 +295,18 @@ class SelectListView extends View cancelled: -> @filterEditorView.getEditor().setText('') - # Public: Cancel and close this select list view. - # - # This restores focus to the previously focused element if - # {::storeFocusedElement} was called prior to this view being attached. - cancel: -> - @list.empty() - @cancelling = true - filterEditorViewFocused = @filterEditorView.isFocused - @cancelled() - @detach() - @restoreFocus() if filterEditorViewFocused - @cancelling = false + getSelectedItemView: -> + @list.find('li.selected') + + confirmSelection: -> + item = @getSelectedItem() + if item? + @confirmed(item) + else + @cancel() + + schedulePopulateList: -> clearTimeout(@scheduleTimeout) + populateCallback = => + @populateList() if @isOnDom() + @scheduleTimeout = setTimeout(populateCallback, @inputThrottle) diff --git a/src/selection.coffee b/src/selection.coffee index c381d4994..a220a49b6 100644 --- a/src/selection.coffee +++ b/src/selection.coffee @@ -30,6 +30,9 @@ class Selection extends Model @emitter.emit 'did-destroy' @emitter.dispose() + destroy: -> + @marker.destroy() + ### Section: Event Subscription ### @@ -60,37 +63,11 @@ class Selection extends Model super + ### - Section: Methods + Section: Managing the selection range ### - destroy: -> - @marker.destroy() - - finalize: -> - @initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange()) - if @isEmpty() - @wordwise = false - @linewise = false - - clearAutoscroll: -> - @needsAutoscroll = null - - # Public: Determines if the selection contains anything. - isEmpty: -> - @getBufferRange().isEmpty() - - # Public: Determines if the ending position of a marker is greater than the - # starting position. - # - # This can happen when, for example, you highlight text "up" in a {TextBuffer}. - isReversed: -> - @marker.isReversed() - - # Public: Returns whether the selection is a single line or not. - isSingleScreenLine: -> - @getScreenRange().isSingleLine() - # Public: Returns the screen {Range} for the selection. getScreenRange: -> @marker.getScreenRange() @@ -148,55 +125,61 @@ class Selection extends Model getHeadBufferPosition: -> @marker.getHeadBufferPosition() - autoscroll: -> - @editor.scrollToScreenRange(@getScreenRange()) + ### + Section: Info about the selection + ### + + # Public: Determines if the selection contains anything. + isEmpty: -> + @getBufferRange().isEmpty() + + # Public: Determines if the ending position of a marker is greater than the + # starting position. + # + # This can happen when, for example, you highlight text "up" in a {TextBuffer}. + isReversed: -> + @marker.isReversed() + + # Public: Returns whether the selection is a single line or not. + isSingleScreenLine: -> + @getScreenRange().isSingleLine() # Public: Returns the text in the selection. getText: -> @editor.buffer.getTextInRange(@getBufferRange()) + # Public: Identifies if a selection intersects with a given buffer range. + # + # * `bufferRange` A {Range} to check against. + # + # Returns a {Boolean} + intersectsBufferRange: (bufferRange) -> + @getBufferRange().intersectsWith(bufferRange) + + intersectsScreenRowRange: (startRow, endRow) -> + @getScreenRange().intersectsRowRange(startRow, endRow) + + intersectsScreenRow: (screenRow) -> + @getScreenRange().intersectsRow(screenRow) + + # Public: Identifies if a selection intersects with another selection. + # + # * `otherSelection` A {Selection} to check against. + # + # Returns a {Boolean} + intersectsWith: (otherSelection, exclusive) -> + @getBufferRange().intersectsWith(otherSelection.getBufferRange(), exclusive) + + ### + Section: Modifying the selected range + ### + # Public: Clears the selection, moving the marker to the head. clear: -> @marker.setAttributes(goalBufferRange: null) @marker.clearTail() unless @retainSelection @finalize() - # Public: Modifies the selection to encompass the current word. - # - # Returns a {Range}. - selectWord: -> - options = {} - options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace() - if @cursor.isBetweenWordAndNonWord() - options.includeNonWordCharacters = false - - @setBufferRange(@cursor.getCurrentWordBufferRange(options)) - @wordwise = true - @initialScreenRange = @getScreenRange() - - # Public: Expands the newest selection to include the entire word on which - # the cursors rests. - expandOverWord: -> - @setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange())) - - # Public: Selects an entire line in the buffer. - # - # * `row` The line {Number} to select (default: the row of the cursor). - selectLine: (row=@cursor.getBufferPosition().row) -> - range = @editor.bufferRangeForBufferRow(row, includeNewline: true) - @setBufferRange(@getBufferRange().union(range)) - @linewise = true - @wordwise = false - @initialScreenRange = @getScreenRange() - - # Public: Expands the newest selection to include the entire line on which - # the cursor currently rests. - # - # It also includes the newline character. - expandOverLine: -> - range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true)) - @setBufferRange(range) - # Public: Selects the text from the current cursor position to a given screen # position. # @@ -311,46 +294,45 @@ class Selection extends Model selectToBeginningOfPreviousParagraph: -> @modifySelection => @cursor.moveToBeginningOfPreviousParagraph() - # Public: Moves the selection down one row. - addSelectionBelow: -> - range = (@getGoalBufferRange() ? @getBufferRange()).copy() - nextRow = range.end.row + 1 + # Public: Modifies the selection to encompass the current word. + # + # Returns a {Range}. + selectWord: -> + options = {} + options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace() + if @cursor.isBetweenWordAndNonWord() + options.includeNonWordCharacters = false - for row in [nextRow..@editor.getLastBufferRow()] - range.start.row = row - range.end.row = row - clippedRange = @editor.clipBufferRange(range) + @setBufferRange(@cursor.getCurrentWordBufferRange(options)) + @wordwise = true + @initialScreenRange = @getScreenRange() - if range.isEmpty() - continue if range.end.column > 0 and clippedRange.end.column is 0 - else - continue if clippedRange.isEmpty() + # Public: Expands the newest selection to include the entire word on which + # the cursors rests. + expandOverWord: -> + @setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange())) - @editor.addSelectionForBufferRange(range, goalBufferRange: range) - break + # Public: Selects an entire line in the buffer. + # + # * `row` The line {Number} to select (default: the row of the cursor). + selectLine: (row=@cursor.getBufferPosition().row) -> + range = @editor.bufferRangeForBufferRow(row, includeNewline: true) + @setBufferRange(@getBufferRange().union(range)) + @linewise = true + @wordwise = false + @initialScreenRange = @getScreenRange() - # FIXME: I have no idea what this does. - getGoalBufferRange: -> - if goalBufferRange = @marker.getAttributes().goalBufferRange - Range.fromObject(goalBufferRange) + # Public: Expands the newest selection to include the entire line on which + # the cursor currently rests. + # + # It also includes the newline character. + expandOverLine: -> + range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true)) + @setBufferRange(range) - # Public: Moves the selection up one row. - addSelectionAbove: -> - range = (@getGoalBufferRange() ? @getBufferRange()).copy() - previousRow = range.end.row - 1 - - for row in [previousRow..0] - range.start.row = row - range.end.row = row - clippedRange = @editor.clipBufferRange(range) - - if range.isEmpty() - continue if range.end.column > 0 and clippedRange.end.column is 0 - else - continue if clippedRange.isEmpty() - - @editor.addSelectionForBufferRange(range, goalBufferRange: range) - break + ### + Section: Modifying the selected text + ### # Public: Replaces text at the current selection. # @@ -391,71 +373,6 @@ class Selection extends Model newBufferRange - # Public: Indents the given text to the suggested level based on the grammar. - # - # * `text` The {String} to indent within the selection. - # * `indentBasis` The beginning indent level. - normalizeIndents: (text, indentBasis) -> - textPrecedingCursor = @cursor.getCurrentBufferLine()[0...@cursor.getBufferColumn()] - isCursorInsideExistingLine = /\S/.test(textPrecedingCursor) - - lines = text.split('\n') - firstLineIndentLevel = @editor.indentLevelForLine(lines[0]) - if isCursorInsideExistingLine - minimumIndentLevel = @editor.indentationForBufferRow(@cursor.getBufferRow()) - else - minimumIndentLevel = @cursor.getIndentLevel() - - normalizedLines = [] - for line, i in lines - if i == 0 - indentLevel = 0 - else if line == '' # remove all indentation from empty lines - indentLevel = 0 - else - lineIndentLevel = @editor.indentLevelForLine(lines[i]) - indentLevel = minimumIndentLevel + (lineIndentLevel - indentBasis) - - normalizedLines.push(@setIndentationForLine(line, indentLevel)) - - normalizedLines.join('\n') - - # Indent the current line(s). - # - # If the selection is empty, indents the current line if the cursor precedes - # non-whitespace characters, and otherwise inserts a tab. If the selection is - # non empty, calls {::indentSelectedRows}. - # - # * `options` (optional) {Object} with the keys: - # * `autoIndent` If `true`, the line is indented to an automatically-inferred - # level. Otherwise, {Editor::getTabText} is inserted. - indent: ({ autoIndent }={}) -> - { row, column } = @cursor.getBufferPosition() - - if @isEmpty() - @cursor.skipLeadingWhitespace() - desiredIndent = @editor.suggestedIndentForBufferRow(row) - delta = desiredIndent - @cursor.getIndentLevel() - - if autoIndent and delta > 0 - @insertText(@editor.buildIndentString(delta)) - else - @insertText(@editor.buildIndentString(1, @cursor.getBufferColumn())) - else - @indentSelectedRows() - - # Public: If the selection spans multiple rows, indent all of them. - indentSelectedRows: -> - [start, end] = @getBufferRowRange() - for row in [start..end] - @editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) == 0 - - # Public: ? - setIndentationForLine: (line, indentLevel) -> - desiredIndentLevel = Math.max(0, indentLevel) - desiredIndentString = @editor.buildIndentString(desiredIndentLevel) - line.replace(/^[\t ]*/, desiredIndentString) - # Public: Removes the first character before the selection if the selection # is empty otherwise it deletes the selection. backspace: -> @@ -632,41 +549,109 @@ class Selection extends Model @editor.createFold(range.start.row, range.end.row) @cursor.setBufferPosition([range.end.row + 1, 0]) - modifySelection: (fn) -> - @retainSelection = true - @plantTail() - fn() - @retainSelection = false + # Public: Indents the given text to the suggested level based on the grammar. + # + # * `text` The {String} to indent within the selection. + # * `indentBasis` The beginning indent level. + normalizeIndents: (text, indentBasis) -> + textPrecedingCursor = @cursor.getCurrentBufferLine()[0...@cursor.getBufferColumn()] + isCursorInsideExistingLine = /\S/.test(textPrecedingCursor) - # Sets the marker's tail to the same position as the marker's head. - # - # This only works if there isn't already a tail position. - # - # Returns a {Point} representing the new tail position. - plantTail: -> - @marker.plantTail() + lines = text.split('\n') + firstLineIndentLevel = @editor.indentLevelForLine(lines[0]) + if isCursorInsideExistingLine + minimumIndentLevel = @editor.indentationForBufferRow(@cursor.getBufferRow()) + else + minimumIndentLevel = @cursor.getIndentLevel() - # Public: Identifies if a selection intersects with a given buffer range. - # - # * `bufferRange` A {Range} to check against. - # - # Returns a {Boolean} - intersectsBufferRange: (bufferRange) -> - @getBufferRange().intersectsWith(bufferRange) + normalizedLines = [] + for line, i in lines + if i == 0 + indentLevel = 0 + else if line == '' # remove all indentation from empty lines + indentLevel = 0 + else + lineIndentLevel = @editor.indentLevelForLine(lines[i]) + indentLevel = minimumIndentLevel + (lineIndentLevel - indentBasis) - intersectsScreenRowRange: (startRow, endRow) -> - @getScreenRange().intersectsRowRange(startRow, endRow) + normalizedLines.push(@setIndentationForLine(line, indentLevel)) - intersectsScreenRow: (screenRow) -> - @getScreenRange().intersectsRow(screenRow) + normalizedLines.join('\n') - # Public: Identifies if a selection intersects with another selection. + # Indent the current line(s). # - # * `otherSelection` A {Selection} to check against. + # If the selection is empty, indents the current line if the cursor precedes + # non-whitespace characters, and otherwise inserts a tab. If the selection is + # non empty, calls {::indentSelectedRows}. # - # Returns a {Boolean} - intersectsWith: (otherSelection, exclusive) -> - @getBufferRange().intersectsWith(otherSelection.getBufferRange(), exclusive) + # * `options` (optional) {Object} with the keys: + # * `autoIndent` If `true`, the line is indented to an automatically-inferred + # level. Otherwise, {Editor::getTabText} is inserted. + indent: ({ autoIndent }={}) -> + { row, column } = @cursor.getBufferPosition() + + if @isEmpty() + @cursor.skipLeadingWhitespace() + desiredIndent = @editor.suggestedIndentForBufferRow(row) + delta = desiredIndent - @cursor.getIndentLevel() + + if autoIndent and delta > 0 + @insertText(@editor.buildIndentString(delta)) + else + @insertText(@editor.buildIndentString(1, @cursor.getBufferColumn())) + else + @indentSelectedRows() + + # Public: If the selection spans multiple rows, indent all of them. + indentSelectedRows: -> + [start, end] = @getBufferRowRange() + for row in [start..end] + @editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) == 0 + + setIndentationForLine: (line, indentLevel) -> + desiredIndentLevel = Math.max(0, indentLevel) + desiredIndentString = @editor.buildIndentString(desiredIndentLevel) + line.replace(/^[\t ]*/, desiredIndentString) + + ### + Section: Managing multiple selections + ### + + # Public: Moves the selection down one row. + addSelectionBelow: -> + range = (@getGoalBufferRange() ? @getBufferRange()).copy() + nextRow = range.end.row + 1 + + for row in [nextRow..@editor.getLastBufferRow()] + range.start.row = row + range.end.row = row + clippedRange = @editor.clipBufferRange(range) + + if range.isEmpty() + continue if range.end.column > 0 and clippedRange.end.column is 0 + else + continue if clippedRange.isEmpty() + + @editor.addSelectionForBufferRange(range, goalBufferRange: range) + break + + # Public: Moves the selection up one row. + addSelectionAbove: -> + range = (@getGoalBufferRange() ? @getBufferRange()).copy() + previousRow = range.end.row - 1 + + for row in [previousRow..0] + range.start.row = row + range.end.row = row + clippedRange = @editor.clipBufferRange(range) + + if range.isEmpty() + continue if range.end.column > 0 and clippedRange.end.column is 0 + else + continue if clippedRange.isEmpty() + + @editor.addSelectionForBufferRange(range, goalBufferRange: range) + break # Public: Combines the given selection into this selection and then destroys # the given selection. @@ -683,6 +668,10 @@ class Selection extends Model @setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), options) otherSelection.destroy() + ### + Section: Comparing to other selections + ### + # Public: Compare this selection's buffer range to another selection's buffer # range. # @@ -692,7 +681,41 @@ class Selection extends Model compare: (otherSelection) -> @getBufferRange().compare(otherSelection.getBufferRange()) + ### + Section: Private Utilities + ### + screenRangeChanged: -> @emit 'screen-range-changed', @getScreenRange() @emitter.emit 'did-change-range' @editor.selectionRangeChanged(this) + + finalize: -> + @initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange()) + if @isEmpty() + @wordwise = false + @linewise = false + + autoscroll: -> + @editor.scrollToScreenRange(@getScreenRange()) + + clearAutoscroll: -> + @needsAutoscroll = null + + modifySelection: (fn) -> + @retainSelection = true + @plantTail() + fn() + @retainSelection = false + + # Sets the marker's tail to the same position as the marker's head. + # + # This only works if there isn't already a tail position. + # + # Returns a {Point} representing the new tail position. + plantTail: -> + @marker.plantTail() + + getGoalBufferRange: -> + if goalBufferRange = @marker.getAttributes().goalBufferRange + Range.fromObject(goalBufferRange) diff --git a/src/syntax.coffee b/src/syntax.coffee index e4ab7de03..09af37c7c 100644 --- a/src/syntax.coffee +++ b/src/syntax.coffee @@ -9,7 +9,7 @@ PropertyAccessors = require 'property-accessors' {$, $$} = require './space-pen-extensions' Token = require './token' -# Public: Syntax class holding the grammars used for tokenizing. +# Extended: Syntax class holding the grammars used for tokenizing. # # An instance of this class is always available as the `atom.syntax` global. # diff --git a/src/task.coffee b/src/task.coffee index 8d8d0316c..64ef7bb32 100644 --- a/src/task.coffee +++ b/src/task.coffee @@ -2,14 +2,38 @@ _ = require 'underscore-plus' child_process = require 'child_process' {Emitter} = require 'emissary' -# Public: Run a node script in a separate process. +# Extended: Run a node script in a separate process. # -# Used by the fuzzy-finder. +# Used by the fuzzy-finder and [find in project](https://github.com/atom/atom/blob/master/src/scan-handler.coffee). +# +# For a real-world example, see the [scan-handler](https://github.com/atom/atom/blob/master/src/scan-handler.coffee) +# and the [instantiation of the task](https://github.com/atom/atom/blob/4a20f13162f65afc816b512ad7201e528c3443d7/src/project.coffee#L245). # # ## Examples # +# In your package code: +# # ```coffee # {Task} = require 'atom' +# +# task = Task.once '/path/to/task-file.coffee', parameter1, parameter2, -> +# console.log 'task has finished' +# +# task.on 'some-event-from-the-task', (data) => +# console.log data.someString # prints 'yep this is it' +# ``` +# +# In `'/path/to/task-file.coffee'`: +# +# ```coffee +# module.exports = (parameter1, parameter2) -> +# # Indicates that this task will be async. +# # Call the `callback` to finish the task +# callback = @async() +# +# emit('some-event-from-the-task', {someString: 'yep this is it'}) +# +# callback() # ``` # # ## Events @@ -55,7 +79,7 @@ class Task # receives a completion callback, this is overridden. callback: null - # Public: Creates a task. + # Public: Creates a task. You should probably use {.once} # # * `taskPath` The {String} path to the CoffeeScript/JavaScript file that # exports a single {Function} to execute. diff --git a/src/theme-manager.coffee b/src/theme-manager.coffee index 23c630043..1aebe757b 100644 --- a/src/theme-manager.coffee +++ b/src/theme-manager.coffee @@ -85,17 +85,29 @@ class ThemeManager EmitterMixin::on.apply(this, arguments) ### - Section: Instance Methods + Section: Accessing Available Themes ### getAvailableNames: -> # TODO: Maybe should change to list all the available themes out there? @getLoadedNames() + ### + Section: Accessing Loaded Themes + ### + # Public: Get an array of all the loaded theme names. getLoadedNames: -> theme.name for theme in @getLoadedThemes() + # Public: Get an array of all the loaded themes. + getLoadedThemes: -> + pack for pack in @packageManager.getLoadedPackages() when pack.isTheme() + + ### + Section: Accessing Active Themes + ### + # Public: Get an array of all the active theme names. getActiveNames: -> theme.name for theme in @getActiveThemes() @@ -104,13 +116,13 @@ class ThemeManager getActiveThemes: -> pack for pack in @packageManager.getActivePackages() when pack.isTheme() - # Public: Get an array of all the loaded themes. - getLoadedThemes: -> - pack for pack in @packageManager.getLoadedPackages() when pack.isTheme() - activatePackages: -> @activateThemes() - # Get the enabled theme names from the config. + ### + Section: Managing Enabled Themes + ### + + # Public: Get the enabled theme names from the config. # # Returns an array of theme names in the order that they should be activated. getEnabledThemeNames: -> @@ -145,6 +157,147 @@ class ThemeManager # the first/top theme to override later themes in the stack. themeNames.reverse() + # Public: Set the list of enabled themes. + # + # * `enabledThemeNames` An {Array} of {String} theme names. + setEnabledThemes: (enabledThemeNames) -> + atom.config.set('core.themes', enabledThemeNames) + + ### + Section: Managing Stylesheets + ### + + # Public: Returns the {String} path to the user's stylesheet under ~/.atom + getUserStylesheetPath: -> + stylesheetPath = fs.resolve(path.join(@configDirPath, 'styles'), ['css', 'less']) + if fs.isFileSync(stylesheetPath) + stylesheetPath + else + path.join(@configDirPath, 'styles.less') + + # Public: Resolve and apply the stylesheet specified by the path. + # + # This supports both CSS and Less stylsheets. + # + # * `stylesheetPath` A {String} path to the stylesheet that can be an absolute + # path or a relative path that will be resolved against the load path. + # + # Returns the absolute path to the required stylesheet. + requireStylesheet: (stylesheetPath, type='bundled') -> + if fullPath = @resolveStylesheet(stylesheetPath) + content = @loadStylesheet(fullPath) + @applyStylesheet(fullPath, content, type) + else + throw new Error("Could not find a file at path '#{stylesheetPath}'") + + fullPath + + unwatchUserStylesheet: -> + @userStylesheetFile?.off() + @userStylesheetFile = null + @removeStylesheet(@userStylesheetPath) if @userStylesheetPath? + + loadUserStylesheet: -> + @unwatchUserStylesheet() + userStylesheetPath = @getUserStylesheetPath() + return unless fs.isFileSync(userStylesheetPath) + + @userStylesheetPath = userStylesheetPath + @userStylesheetFile = new File(userStylesheetPath) + @userStylesheetFile.on 'contents-changed moved removed', => + @loadUserStylesheet() + userStylesheetContents = @loadStylesheet(userStylesheetPath, true) + @applyStylesheet(userStylesheetPath, userStylesheetContents, 'userTheme') + + loadBaseStylesheets: -> + @requireStylesheet('bootstrap/less/bootstrap') + @reloadBaseStylesheets() + + reloadBaseStylesheets: -> + @requireStylesheet('../static/atom') + if nativeStylesheetPath = fs.resolveOnLoadPath(process.platform, ['css', 'less']) + @requireStylesheet(nativeStylesheetPath) + + stylesheetElementForId: (id) -> + document.head.querySelector("""style[id="#{id}"]""") + + resolveStylesheet: (stylesheetPath) -> + if path.extname(stylesheetPath).length > 0 + fs.resolveOnLoadPath(stylesheetPath) + else + fs.resolveOnLoadPath(stylesheetPath, ['css', 'less']) + + loadStylesheet: (stylesheetPath, importFallbackVariables) -> + if path.extname(stylesheetPath) is '.less' + @loadLessStylesheet(stylesheetPath, importFallbackVariables) + else + fs.readFileSync(stylesheetPath, 'utf8') + + loadLessStylesheet: (lessStylesheetPath, importFallbackVariables=false) -> + unless @lessCache? + LessCompileCache = require './less-compile-cache' + @lessCache = new LessCompileCache({@resourcePath, importPaths: @getImportPaths()}) + + try + if importFallbackVariables + baseVarImports = """ + @import "variables/ui-variables"; + @import "variables/syntax-variables"; + """ + less = fs.readFileSync(lessStylesheetPath, 'utf8') + @lessCache.cssForFile(lessStylesheetPath, [baseVarImports, less].join('\n')) + else + @lessCache.read(lessStylesheetPath) + catch error + console.error """ + Error compiling Less stylesheet: #{lessStylesheetPath} + Line number: #{error.line} + #{error.message} + """ + + removeStylesheet: (stylesheetPath) -> + fullPath = @resolveStylesheet(stylesheetPath) ? stylesheetPath + element = @stylesheetElementForId(@stringToId(fullPath)) + if element? + {sheet} = element + element.remove() + @emit 'stylesheet-removed', sheet + @emitter.emit 'did-remove-stylesheet', sheet + @emit 'stylesheets-changed' + @emitter.emit 'did-change-stylesheets' + + applyStylesheet: (path, text, type='bundled') -> + styleId = @stringToId(path) + styleElement = @stylesheetElementForId(styleId) + + if styleElement? + @emit 'stylesheet-removed', styleElement.sheet + @emitter.emit 'did-remove-stylesheet', styleElement.sheet + styleElement.textContent = text + else + styleElement = document.createElement('style') + styleElement.setAttribute('class', type) + styleElement.setAttribute('id', styleId) + styleElement.textContent = text + + elementToInsertBefore = _.last(document.head.querySelectorAll("style.#{type}"))?.nextElementSibling + if elementToInsertBefore? + document.head.insertBefore(styleElement, elementToInsertBefore) + else + document.head.appendChild(styleElement) + + @emit 'stylesheet-added', styleElement.sheet + @emitter.emit 'did-add-stylesheet', styleElement.sheet + @emit 'stylesheets-changed' + @emitter.emit 'did-change-stylesheets' + + ### + Section: Private + ### + + stringToId: (string) -> + string.replace(/\\/g, '/') + activateThemes: -> deferred = Q.defer() @@ -194,12 +347,6 @@ class ThemeManager refreshLessCache: -> @lessCache?.setImportPaths(@getImportPaths()) - # Public: Set the list of enabled themes. - # - # * `enabledThemeNames` An {Array} of {String} theme names. - setEnabledThemes: (enabledThemeNames) -> - atom.config.set('core.themes', enabledThemeNames) - getImportPaths: -> activeThemes = @getActiveThemes() if activeThemes.length > 0 @@ -212,133 +359,6 @@ class ThemeManager themePaths.filter (themePath) -> fs.isDirectorySync(themePath) - # Public: Returns the {String} path to the user's stylesheet under ~/.atom - getUserStylesheetPath: -> - stylesheetPath = fs.resolve(path.join(@configDirPath, 'styles'), ['css', 'less']) - if fs.isFileSync(stylesheetPath) - stylesheetPath - else - path.join(@configDirPath, 'styles.less') - - unwatchUserStylesheet: -> - @userStylesheetFile?.off() - @userStylesheetFile = null - @removeStylesheet(@userStylesheetPath) if @userStylesheetPath? - - loadUserStylesheet: -> - @unwatchUserStylesheet() - userStylesheetPath = @getUserStylesheetPath() - return unless fs.isFileSync(userStylesheetPath) - - @userStylesheetPath = userStylesheetPath - @userStylesheetFile = new File(userStylesheetPath) - @userStylesheetFile.on 'contents-changed moved removed', => - @loadUserStylesheet() - userStylesheetContents = @loadStylesheet(userStylesheetPath, true) - @applyStylesheet(userStylesheetPath, userStylesheetContents, 'userTheme') - - loadBaseStylesheets: -> - @requireStylesheet('bootstrap/less/bootstrap') - @reloadBaseStylesheets() - - reloadBaseStylesheets: -> - @requireStylesheet('../static/atom') - if nativeStylesheetPath = fs.resolveOnLoadPath(process.platform, ['css', 'less']) - @requireStylesheet(nativeStylesheetPath) - - stylesheetElementForId: (id) -> - document.head.querySelector("""style[id="#{id}"]""") - - resolveStylesheet: (stylesheetPath) -> - if path.extname(stylesheetPath).length > 0 - fs.resolveOnLoadPath(stylesheetPath) - else - fs.resolveOnLoadPath(stylesheetPath, ['css', 'less']) - - # Public: Resolve and apply the stylesheet specified by the path. - # - # This supports both CSS and Less stylsheets. - # - # * `stylesheetPath` A {String} path to the stylesheet that can be an absolute - # path or a relative path that will be resolved against the load path. - # - # Returns the absolute path to the required stylesheet. - requireStylesheet: (stylesheetPath, type='bundled') -> - if fullPath = @resolveStylesheet(stylesheetPath) - content = @loadStylesheet(fullPath) - @applyStylesheet(fullPath, content, type) - else - throw new Error("Could not find a file at path '#{stylesheetPath}'") - - fullPath - - loadStylesheet: (stylesheetPath, importFallbackVariables) -> - if path.extname(stylesheetPath) is '.less' - @loadLessStylesheet(stylesheetPath, importFallbackVariables) - else - fs.readFileSync(stylesheetPath, 'utf8') - - loadLessStylesheet: (lessStylesheetPath, importFallbackVariables=false) -> - unless @lessCache? - LessCompileCache = require './less-compile-cache' - @lessCache = new LessCompileCache({@resourcePath, importPaths: @getImportPaths()}) - - try - if importFallbackVariables - baseVarImports = """ - @import "variables/ui-variables"; - @import "variables/syntax-variables"; - """ - less = fs.readFileSync(lessStylesheetPath, 'utf8') - @lessCache.cssForFile(lessStylesheetPath, [baseVarImports, less].join('\n')) - else - @lessCache.read(lessStylesheetPath) - catch error - console.error """ - Error compiling Less stylesheet: #{lessStylesheetPath} - Line number: #{error.line} - #{error.message} - """ - - stringToId: (string) -> - string.replace(/\\/g, '/') - - removeStylesheet: (stylesheetPath) -> - fullPath = @resolveStylesheet(stylesheetPath) ? stylesheetPath - element = @stylesheetElementForId(@stringToId(fullPath)) - if element? - {sheet} = element - element.remove() - @emit 'stylesheet-removed', sheet - @emitter.emit 'did-remove-stylesheet', sheet - @emit 'stylesheets-changed' - @emitter.emit 'did-change-stylesheets' - - applyStylesheet: (path, text, type='bundled') -> - styleId = @stringToId(path) - styleElement = @stylesheetElementForId(styleId) - - if styleElement? - @emit 'stylesheet-removed', styleElement.sheet - @emitter.emit 'did-remove-stylesheet', styleElement.sheet - styleElement.textContent = text - else - styleElement = document.createElement('style') - styleElement.setAttribute('class', type) - styleElement.setAttribute('id', styleId) - styleElement.textContent = text - - elementToInsertBefore = _.last(document.head.querySelectorAll("style.#{type}"))?.nextElementSibling - if elementToInsertBefore? - document.head.insertBefore(styleElement, elementToInsertBefore) - else - document.head.appendChild(styleElement) - - @emit 'stylesheet-added', styleElement.sheet - @emitter.emit 'did-add-stylesheet', styleElement.sheet - @emit 'stylesheets-changed' - @emitter.emit 'did-change-stylesheets' - updateGlobalEditorStyle: (property, value) -> unless styleNode = @stylesheetElementForId('global-editor-styles') @applyStylesheet('global-editor-styles', '.editor {}') diff --git a/src/workspace-view.coffee b/src/workspace-view.coffee index f7d0936fe..a46b46434 100644 --- a/src/workspace-view.coffee +++ b/src/workspace-view.coffee @@ -167,12 +167,173 @@ class WorkspaceView extends View @command 'core:save', => @saveActivePaneItem() @command 'core:save-as', => @saveActivePaneItemAs() - # Public: Get the underlying model object. + ### + Section: Accessing the Workspace Model + ### + + # Essential: Get the underlying model object. # # Returns a {Workspace}. getModel: -> @model - # Public: Install the Atom shell commands on the user's system. + ### + Section: Accessing Views + ### + + # Essential: Register a function to be called for every current and future + # editor view in the workspace (only includes {EditorView}s that are pane + # items). + # + # * `callback` A {Function} with an {EditorView} as its only argument. + # * `editorView` {EditorView} + # + # Returns a subscription object with an `.off` method that you can call to + # unregister the callback. + eachEditorView: (callback) -> + callback(editorView) for editorView in @getEditorViews() + attachedCallback = (e, editorView) -> + callback(editorView) unless editorView.mini + @on('editor:attached', attachedCallback) + off: => @off('editor:attached', attachedCallback) + + # Essential: Register a function to be called for every current and future + # pane view in the workspace. + # + # * `callback` A {Function} with a {PaneView} as its only argument. + # * `paneView` {PaneView} + # + # Returns a subscription object with an `.off` method that you can call to + # unregister the callback. + eachPaneView: (callback) -> + @panes.eachPaneView(callback) + + # Essential: Get all existing pane views. + # + # Prefer {Workspace::getPanes} if you don't need access to the view objects. + # Also consider using {::eachPaneView} if you want to register a callback for + # all current and *future* pane views. + # + # Returns an Array of all open {PaneView}s. + getPaneViews: -> + @panes.getPaneViews() + + # Essential: Get the active pane view. + # + # Prefer {Workspace::getActivePane} if you don't actually need access to the + # view. + # + # Returns a {PaneView}. + getActivePaneView: -> + @panes.getActivePaneView() + + # Essential: Get the view associated with the active pane item. + # + # Returns a view. + getActiveView: -> + @panes.getActiveView() + + ### + Section: Adding elements to the workspace + ### + + # Essential: Prepend an element or view to the panels at the top of the + # workspace. + # + # * `element` jQuery object or DOM element + prependToTop: (element) -> + @vertical.prepend(element) + + # Essential: Append an element or view to the panels at the top of the workspace. + # + # * `element` jQuery object or DOM element + appendToTop: (element) -> + @panes.before(element) + + # Essential: Prepend an element or view to the panels at the bottom of the + # workspace. + # + # * `element` jQuery object or DOM element + prependToBottom: (element) -> + @panes.after(element) + + # Essential: Append an element or view to the panels at the bottom of the + # workspace. + # + # * `element` jQuery object or DOM element + appendToBottom: (element) -> + @vertical.append(element) + + # Essential: Prepend an element or view to the panels at the left of the + # workspace. + # + # * `element` jQuery object or DOM element + prependToLeft: (element) -> + @horizontal.prepend(element) + + # Essential: Append an element or view to the panels at the left of the + # workspace. + # + # * `element` jQuery object or DOM element + appendToLeft: (element) -> + @vertical.before(element) + + # Essential: Prepend an element or view to the panels at the right of the + # workspace. + # + # * `element` jQuery object or DOM element + prependToRight: (element) -> + @vertical.after(element) + + # Essential: Append an element or view to the panels at the right of the + # workspace. + # + # * `element` jQuery object or DOM element + appendToRight: (element) -> + @horizontal.append(element) + + ### + Section: Focusing pane views + ### + + # Focus the previous pane by id. + focusPreviousPaneView: -> @model.activatePreviousPane() + + # Focus the next pane by id. + focusNextPaneView: -> @model.activateNextPane() + + # Essential: Focus the pane directly above the active pane. + focusPaneViewAbove: -> @panes.focusPaneViewAbove() + + # Essential: Focus the pane directly below the active pane. + focusPaneViewBelow: -> @panes.focusPaneViewBelow() + + # Essential: Focus the pane directly to the left of the active pane. + focusPaneViewOnLeft: -> @panes.focusPaneViewOnLeft() + + # Essential: Focus the pane directly to the right of the active pane. + focusPaneViewOnRight: -> @panes.focusPaneViewOnRight() + + ### + Section: Private + ### + + afterAttach: (onDom) -> + @focus() if onDom + + # Called by SpacePen + beforeRemove: -> + @model.destroy() + + setEditorFontSize: (fontSize) -> + atom.themes.updateGlobalEditorStyle('font-size', fontSize + 'px') + + setEditorFontFamily: (fontFamily) -> + atom.themes.updateGlobalEditorStyle('font-family', fontFamily) + + setEditorLineHeight: (lineHeight) -> + atom.themes.updateGlobalEditorStyle('line-height', lineHeight) + + # Install the Atom shell commands on the user's system. installShellCommands: -> showErrorDialog = (error) -> installDirectory = CommandInstaller.getInstallDirectory() @@ -207,9 +368,6 @@ class WorkspaceView extends View $(document.body).focus() true - afterAttach: (onDom) -> - @focus() if onDom - # Prompts to save all unsaved items confirmClose: -> @panes.confirmClose() @@ -248,143 +406,10 @@ class WorkspaceView extends View for editorElement in @panes.element.querySelectorAll('.pane > .item-views > .editor') $(editorElement).view() - # Public: Prepend an element or view to the panels at the top of the - # workspace. - # - # * `element` jQuery object or DOM element - prependToTop: (element) -> - @vertical.prepend(element) - # Public: Append an element or view to the panels at the top of the workspace. - # - # * `element` jQuery object or DOM element - appendToTop: (element) -> - @panes.before(element) - - # Public: Prepend an element or view to the panels at the bottom of the - # workspace. - # - # * `element` jQuery object or DOM element - prependToBottom: (element) -> - @panes.after(element) - - # Public: Append an element or view to the panels at the bottom of the - # workspace. - # - # * `element` jQuery object or DOM element - appendToBottom: (element) -> - @vertical.append(element) - - # Public: Prepend an element or view to the panels at the left of the - # workspace. - # - # * `element` jQuery object or DOM element - prependToLeft: (element) -> - @horizontal.prepend(element) - - # Public: Append an element or view to the panels at the left of the - # workspace. - # - # * `element` jQuery object or DOM element - appendToLeft: (element) -> - @vertical.before(element) - - # Public: Prepend an element or view to the panels at the right of the - # workspace. - # - # * `element` jQuery object or DOM element - prependToRight: (element) -> - @vertical.after(element) - - # Public: Append an element or view to the panels at the right of the - # workspace. - # - # * `element` jQuery object or DOM element - appendToRight: (element) -> - @horizontal.append(element) - - # Public: Get the active pane view. - # - # Prefer {Workspace::getActivePane} if you don't actually need access to the - # view. - # - # Returns a {PaneView}. - getActivePaneView: -> - @panes.getActivePaneView() - - # Public: Get the view associated with the active pane item. - # - # Returns a view. - getActiveView: -> - @panes.getActiveView() - - # Focus the previous pane by id. - focusPreviousPaneView: -> @model.activatePreviousPane() - - # Focus the next pane by id. - focusNextPaneView: -> @model.activateNextPane() - - # Public: Focus the pane directly above the active pane. - focusPaneViewAbove: -> @panes.focusPaneViewAbove() - - # Public: Focus the pane directly below the active pane. - focusPaneViewBelow: -> @panes.focusPaneViewBelow() - - # Public: Focus the pane directly to the left of the active pane. - focusPaneViewOnLeft: -> @panes.focusPaneViewOnLeft() - - # Public: Focus the pane directly to the right of the active pane. - focusPaneViewOnRight: -> @panes.focusPaneViewOnRight() - - # Public: Register a function to be called for every current and future - # pane view in the workspace. - # - # * `callback` A {Function} with a {PaneView} as its only argument. - # * `paneView` {PaneView} - # - # Returns a subscription object with an `.off` method that you can call to - # unregister the callback. - eachPaneView: (callback) -> - @panes.eachPaneView(callback) - - # Public: Get all existing pane views. - # - # Prefer {Workspace::getPanes} if you don't need access to the view objects. - # Also consider using {::eachPaneView} if you want to register a callback for - # all current and *future* pane views. - # - # Returns an Array of all open {PaneView}s. - getPaneViews: -> - @panes.getPaneViews() - - # Public: Register a function to be called for every current and future - # editor view in the workspace (only includes {EditorView}s that are pane - # items). - # - # * `callback` A {Function} with an {EditorView} as its only argument. - # * `editorView` {EditorView} - # - # Returns a subscription object with an `.off` method that you can call to - # unregister the callback. - eachEditorView: (callback) -> - callback(editorView) for editorView in @getEditorViews() - attachedCallback = (e, editorView) -> - callback(editorView) unless editorView.mini - @on('editor:attached', attachedCallback) - off: => @off('editor:attached', attachedCallback) - - # Called by SpacePen - beforeRemove: -> - @model.destroy() - - setEditorFontSize: (fontSize) -> - atom.themes.updateGlobalEditorStyle('font-size', fontSize + 'px') - - setEditorFontFamily: (fontFamily) -> - atom.themes.updateGlobalEditorStyle('font-family', fontFamily) - - setEditorLineHeight: (lineHeight) -> - atom.themes.updateGlobalEditorStyle('line-height', lineHeight) + ### + Section: Deprecated + ### # Deprecated eachPane: (callback) -> diff --git a/src/workspace.coffee b/src/workspace.coffee index 64ea6404b..31b7cb261 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -10,7 +10,7 @@ Editor = require './editor' PaneContainer = require './pane-container' Pane = require './pane' -# Public: Represents the state of the user interface for the entire window. +# Essential: Represents the state of the user interface for the entire window. # An instance of this class is available via the `atom.workspace` global. # # Interact with this object to open files, be notified of current and future @@ -91,6 +91,33 @@ class Workspace extends Model Section: Event Subscription ### + # Essential: Invoke the given callback with all current and future text + # editors in the workspace. + # + # * `callback` {Function} to be called with current and future text editors. + # * `editor` An {Editor} that is present in {::getTextEditors} at the time + # of subscription or that is added at some later time. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + observeTextEditors: (callback) -> + callback(textEditor) for textEditor in @getTextEditors() + @onDidAddTextEditor ({textEditor}) -> callback(textEditor) + + # Essential: Invoke the given callback whenever an item is opened. Unlike + # ::onDidAddPaneItem, observers will be notified for items that are already + # present in the workspace when they are reopened. + # + # * `callback` {Function} to be called whenever an item is opened. + # * `event` {Object} with the following keys: + # * `uri` {String} representing the opened URI. Could be `undefined`. + # * `item` The opened item. + # * `pane` The pane in which the item was opened. + # * `index` The index of the opened item on its pane. + # + # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. + onDidOpen: (callback) -> + @emitter.on 'did-open', callback + # Extended: Invoke the given callback when a pane is added to the workspace. # # * `callback` {Function} to be called panes are added. @@ -174,33 +201,6 @@ class Workspace extends Model @onDidAddPaneItem ({item, pane, index}) -> callback({textEditor: item, pane, index}) if item instanceof Editor - # Essential: Invoke the given callback with all current and future text - # editors in the workspace. - # - # * `callback` {Function} to be called with current and future text editors. - # * `editor` An {Editor} that is present in {::getTextEditors} at the time - # of subscription or that is added at some later time. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - observeTextEditors: (callback) -> - callback(textEditor) for textEditor in @getTextEditors() - @onDidAddTextEditor ({textEditor}) -> callback(textEditor) - - # Essential: Invoke the given callback whenever an item is opened. Unlike - # ::onDidAddPaneItem, observers will be notified for items that are already - # present in the workspace when they are reopened. - # - # * `callback` {Function} to be called whenever an item is opened. - # * `event` {Object} with the following keys: - # * `uri` {String} representing the opened URI. Could be `undefined`. - # * `item` The opened item. - # * `pane` The pane in which the item was opened. - # * `index` The index of the opened item on its pane. - # - # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. - onDidOpen: (callback) -> - @emitter.on 'did-open', callback - eachEditor: (callback) -> deprecate("Use Workspace::observeTextEditors instead")