Merge branch 'master' into wl-rm-safe-clipboard

This commit is contained in:
Max Brunsfeld
2018-08-24 11:57:36 -07:00
committed by GitHub
257 changed files with 40837 additions and 6029 deletions

View File

@@ -1,10 +1,25 @@
const {ipcRenderer, remote, shell} = require('electron')
const ipcHelpers = require('./ipc-helpers')
const {Disposable} = require('event-kit')
const {Emitter, Disposable} = require('event-kit')
const getWindowLoadSettings = require('./get-window-load-settings')
module.exports =
class ApplicationDelegate {
constructor () {
this.pendingSettingsUpdateCount = 0
this._ipcMessageEmitter = null
}
ipcMessageEmitter () {
if (!this._ipcMessageEmitter) {
this._ipcMessageEmitter = new Emitter()
ipcRenderer.on('message', (event, message, detail) => {
this._ipcMessageEmitter.emit(message, detail)
})
}
return this._ipcMessageEmitter
}
getWindowLoadSettings () { return getWindowLoadSettings() }
open (params) {
@@ -175,6 +190,25 @@ class ApplicationDelegate {
return remote.systemPreferences.getUserDefault(key, type)
}
async setUserSettings (config, configFilePath) {
this.pendingSettingsUpdateCount++
try {
await ipcHelpers.call('set-user-settings', JSON.stringify(config), configFilePath)
} finally {
this.pendingSettingsUpdateCount--
}
}
onDidChangeUserSettings (callback) {
return this.ipcMessageEmitter().on('did-change-user-settings', detail => {
if (this.pendingSettingsUpdateCount === 0) callback(detail)
})
}
onDidFailToReadUserSettings (callback) {
return this.ipcMessageEmitter().on('did-fail-to-read-user-setting', callback)
}
confirm (options, callback) {
if (typeof callback === 'function') {
// Async version: pass options directly to Electron but set sane defaults
@@ -205,7 +239,7 @@ class ApplicationDelegate {
return chosen
} else {
const callback = buttons[buttonLabels[chosen]]
if (typeof callback === 'function') callback()
if (typeof callback === 'function') return callback()
}
}
}
@@ -218,7 +252,7 @@ class ApplicationDelegate {
this.getCurrentWindow().showSaveDialog(options, callback)
} else {
// Sync
if (typeof params === 'string') {
if (typeof options === 'string') {
options = {defaultPath: options}
}
return this.getCurrentWindow().showSaveDialog(options)
@@ -230,24 +264,14 @@ class ApplicationDelegate {
}
onDidOpenLocations (callback) {
const outerCallback = (event, message, detail) => {
if (message === 'open-locations') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
return this.ipcMessageEmitter().on('open-locations', callback)
}
onUpdateAvailable (callback) {
const outerCallback = (event, message, detail) => {
// TODO: Yes, this is strange that `onUpdateAvailable` is listening for
// `did-begin-downloading-update`. We currently have no mechanism to know
// if there is an update, so begin of downloading is a good proxy.
if (message === 'did-begin-downloading-update') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
// TODO: Yes, this is strange that `onUpdateAvailable` is listening for
// `did-begin-downloading-update`. We currently have no mechanism to know
// if there is an update, so begin of downloading is a good proxy.
return this.ipcMessageEmitter().on('did-begin-downloading-update', callback)
}
onDidBeginDownloadingUpdate (callback) {
@@ -255,40 +279,19 @@ class ApplicationDelegate {
}
onDidBeginCheckingForUpdate (callback) {
const outerCallback = (event, message, detail) => {
if (message === 'checking-for-update') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
return this.ipcMessageEmitter().on('checking-for-update', callback)
}
onDidCompleteDownloadingUpdate (callback) {
const outerCallback = (event, message, detail) => {
// TODO: We could rename this event to `did-complete-downloading-update`
if (message === 'update-available') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
return this.ipcMessageEmitter().on('update-available', callback)
}
onUpdateNotAvailable (callback) {
const outerCallback = (event, message, detail) => {
if (message === 'update-not-available') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
return this.ipcMessageEmitter().on('update-not-available', callback)
}
onUpdateError (callback) {
const outerCallback = (event, message, detail) => {
if (message === 'update-error') callback(detail)
}
ipcRenderer.on('message', outerCallback)
return new Disposable(() => ipcRenderer.removeListener('message', outerCallback))
return this.ipcMessageEmitter().on('update-error', callback)
}
onApplicationMenuCommand (handler) {
@@ -354,11 +357,11 @@ class ApplicationDelegate {
}
emitWillSavePath (path) {
return ipcRenderer.sendSync('will-save-path', path)
return ipcHelpers.call('will-save-path', path)
}
emitDidSavePath (path) {
return ipcRenderer.sendSync('did-save-path', path)
return ipcHelpers.call('did-save-path', path)
}
resolveProxy (requestId, url) {

View File

@@ -9,7 +9,6 @@ const fs = require('fs-plus')
const {mapSourcePosition} = require('@atom/source-map-support')
const WindowEventHandler = require('./window-event-handler')
const StateStore = require('./state-store')
const StorageFolder = require('./storage-folder')
const registerDefaultCommands = require('./register-default-commands')
const {updateProcessEnv} = require('./update-process-env')
const ConfigSchema = require('./config-schema')
@@ -51,7 +50,6 @@ let nextId = 0
//
// An instance of this class is always available as the `atom` global.
class AtomEnvironment {
/*
Section: Properties
*/
@@ -86,8 +84,11 @@ class AtomEnvironment {
// Public: A {Config} instance
this.config = new Config({
notificationManager: this.notifications,
enablePersistence: this.enablePersistence
saveCallback: settings => {
if (this.enablePersistence) {
this.applicationDelegate.setUserSettings(settings, this.config.getUserConfigPath())
}
}
})
this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)})
@@ -208,19 +209,23 @@ class AtomEnvironment {
this.blobStore = params.blobStore
this.configDirPath = params.configDirPath
const {devMode, safeMode, resourcePath, clearWindowState} = this.getLoadSettings()
if (clearWindowState) {
this.getStorageFolder().clear()
this.stateStore.clear()
}
const {devMode, safeMode, resourcePath, userSettings, projectSpecification} = this.getLoadSettings()
ConfigSchema.projectHome = {
type: 'string',
default: path.join(fs.getHomeDirectory(), 'github'),
description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.'
}
this.config.initialize({configDirPath: this.configDirPath, resourcePath, projectHomeSchema: ConfigSchema.projectHome})
this.config.initialize({
mainSource: this.enablePersistence && path.join(this.configDirPath, 'config.cson'),
projectHomeSchema: ConfigSchema.projectHome
})
this.config.resetUserSettings(userSettings)
if (projectSpecification != null && projectSpecification.config != null) {
this.project.replace(projectSpecification)
}
this.menu.initialize({resourcePath})
this.contextMenu.initialize({resourcePath, devMode})
@@ -242,8 +247,6 @@ class AtomEnvironment {
this.uriHandlerRegistry.registerHostHandler('core', CoreURIHandlers.create(this))
this.autoUpdater.initialize()
this.config.load()
this.protocolHandlerInstaller.initialize(this.config, this.notifications)
this.themes.loadBaseStylesheets()
@@ -373,8 +376,7 @@ class AtomEnvironment {
if (this.project) this.project.destroy()
this.project = null
this.commands.clear()
this.stylesElement.remove()
this.config.unobserveUserConfig()
if (this.stylesElement) this.stylesElement.remove()
this.autoUpdater.destroy()
this.uriHandlerRegistry.destroy()
@@ -485,21 +487,25 @@ class AtomEnvironment {
// Public: Gets the release channel of the Atom application.
//
// Returns the release channel as a {String}. Will return one of `dev`, `beta`, or `stable`.
// Returns the release channel as a {String}. Will return a specific release channel
// name like 'beta' or 'nightly' if one is found in the Atom version or 'stable'
// otherwise.
getReleaseChannel () {
const version = this.getVersion()
if (version.includes('beta')) {
return 'beta'
} else if (version.includes('dev')) {
return 'dev'
} else {
return 'stable'
// This matches stable, dev (with or without commit hash) and any other
// release channel following the pattern '1.00.0-channel0'
const match = this.getVersion().match(/\d+\.\d+\.\d+(-([a-z]+)(\d+|-\w{4,})?)?$/)
if (!match) {
return 'unrecognized'
} else if (match[2]) {
return match[2]
}
return 'stable'
}
// Public: Returns a {Boolean} that is `true` if the current version is an official release.
isReleasedVersion () {
return !/\w{7}/.test(this.getVersion()) // Check if the release is a 7-character SHA prefix
return this.getReleaseChannel().match(/stable|beta|nightly/) != null
}
// Public: Get the time taken to completely load the current window.
@@ -764,7 +770,11 @@ class AtomEnvironment {
}
// Call this method when establishing a real application window.
startEditorWindow () {
async startEditorWindow () {
if (this.getLoadSettings().clearWindowState) {
await this.stateStore.clear()
}
this.unloaded = false
const updateProcessEnvPromise = this.updateProcessEnvAndTriggerHooks()
@@ -779,6 +789,13 @@ class AtomEnvironment {
if (error) console.warn(error.message)
})
this.disposables.add(this.applicationDelegate.onDidChangeUserSettings(settings =>
this.config.resetUserSettings(settings)
))
this.disposables.add(this.applicationDelegate.onDidFailToReadUserSettings(message =>
this.notifications.addError(message)
))
this.disposables.add(this.applicationDelegate.onDidOpenLocations(this.openLocations.bind(this)))
this.disposables.add(this.applicationDelegate.onApplicationMenuCommand(this.dispatchApplicationMenuCommand.bind(this)))
this.disposables.add(this.applicationDelegate.onContextMenuCommand(this.dispatchContextMenuCommand.bind(this)))
@@ -1121,7 +1138,7 @@ class AtomEnvironment {
}
if (windowIsUnused()) {
this.restoreStateIntoThisEnvironment(state)
await this.restoreStateIntoThisEnvironment(state)
return Promise.all(filesToOpen.map(file => this.workspace.open(file)))
} else {
let resolveDiscardStatePromise = null
@@ -1264,11 +1281,6 @@ or use Pane::saveItemAs for programmatic saving.`)
}
}
getStorageFolder () {
if (!this.storageFolder) this.storageFolder = new StorageFolder(this.getConfigDirPath())
return this.storageFolder
}
getConfigDirPath () {
if (!this.configDirPath) this.configDirPath = process.env.ATOM_HOME
return this.configDirPath

View File

@@ -1,5 +1,3 @@
/** @babel */
const fs = require('fs-plus')
const path = require('path')

View File

@@ -1,8 +1,7 @@
'use babel'
const {Emitter, CompositeDisposable} = require('event-kit')
import {Emitter, CompositeDisposable} from 'event-kit'
export default class AutoUpdateManager {
module.exports =
class AutoUpdateManager {
constructor ({applicationDelegate}) {
this.applicationDelegate = applicationDelegate
this.subscriptions = new CompositeDisposable()

View File

@@ -11,7 +11,8 @@ var PREFIXES = [
'/** @babel */',
'"use babel"',
'\'use babel\'',
'/* @flow */'
'/* @flow */',
'// @flow'
]
var PREFIX_LENGTH = Math.max.apply(Math, PREFIXES.map(function (prefix) {

View File

@@ -1,6 +1,4 @@
/** @babel */
import BufferedProcess from './buffered-process'
const BufferedProcess = require('./buffered-process')
// Extended: Like {BufferedProcess}, but accepts a Node script as the command
// to run.
@@ -12,7 +10,8 @@ import BufferedProcess from './buffered-process'
// ```js
// const {BufferedNodeProcess} = require('atom')
// ```
export default class BufferedNodeProcess extends BufferedProcess {
module.exports =
class BufferedNodeProcess extends BufferedProcess {
// Public: Runs the given Node script by spawning a new child process.
//

View File

@@ -1,9 +1,7 @@
/** @babel */
import _ from 'underscore-plus'
import ChildProcess from 'child_process'
import {Emitter} from 'event-kit'
import path from 'path'
const _ = require('underscore-plus')
const ChildProcess = require('child_process')
const {Emitter} = require('event-kit')
const path = require('path')
// Extended: A wrapper which provides standard error/output line buffering for
// Node's ChildProcess.
@@ -19,7 +17,8 @@ import path from 'path'
// const exit = (code) => console.log("ps -ef exited with #{code}")
// const process = new BufferedProcess({command, args, stdout, exit})
// ```
export default class BufferedProcess {
module.exports =
class BufferedProcess {
/*
Section: Construction
*/
@@ -190,12 +189,12 @@ export default class BufferedProcess {
output += data
})
wmicProcess.stdout.on('close', () => {
const pidsToKill = output.split(/\s+/)
.filter((pid) => /^\d+$/.test(pid))
.map((pid) => parseInt(pid))
.filter((pid) => pid !== parentPid && pid > 0 && pid < Infinity)
for (let pid of output.split(/\s+/)) {
if (!/^\d{1,10}$/.test(pid)) continue
pid = parseInt(pid, 10)
if (!pid || pid === parentPid) continue
for (let pid of pidsToKill) {
try {
process.kill(pid)
} catch (error) {}

View File

@@ -1,10 +1,9 @@
/** @babel */
let ParsedColor = null
// Essential: A simple color class returned from {Config::get} when the value
// at the key path is of type 'color'.
export default class Color {
module.exports =
class Color {
// Essential: Parse a {String} or {Object} into a {Color}.
//
// * `value` A {String} such as `'white'`, `#ff00ff`, or
@@ -89,6 +88,10 @@ export default class Color {
return this.alpha === 1 ? this.toHexString() : this.toRGBAString()
}
toString () {
return this.toRGBAString()
}
isEqual (color) {
if (this === color) {
return true

View File

@@ -27,22 +27,36 @@ class CommandInstaller {
}, () => {})
}
this.installAtomCommand(true, error => {
this.installAtomCommand(true, (error, atomCommandName) => {
if (error) return showErrorDialog(error)
this.installApmCommand(true, error => {
this.installApmCommand(true, (error, apmCommandName) => {
if (error) return showErrorDialog(error)
this.applicationDelegate.confirm({
message: 'Commands installed.',
detail: 'The shell commands `atom` and `apm` are installed.'
detail: `The shell commands \`${atomCommandName}\` and \`${apmCommandName}\` are installed.`
}, () => {})
})
})
}
getCommandNameForChannel (commandName) {
let channelMatch = this.appVersion.match(/beta|nightly/)
let channel = channelMatch ? channelMatch[0] : ''
switch (channel) {
case 'beta':
return `${commandName}-beta`
case 'nightly':
return `${commandName}-nightly`
default:
return commandName
}
}
installAtomCommand (askForPrivilege, callback) {
this.installCommand(
path.join(this.getResourcesDirectory(), 'app', 'atom.sh'),
this.appVersion.includes('beta') ? 'atom-beta' : 'atom',
this.getCommandNameForChannel('atom'),
askForPrivilege,
callback
)
@@ -51,7 +65,7 @@ class CommandInstaller {
installApmCommand (askForPrivilege, callback) {
this.installCommand(
path.join(this.getResourcesDirectory(), 'app', 'apm', 'node_modules', '.bin', 'apm'),
this.appVersion.includes('beta') ? 'apm-beta' : 'apm',
this.getCommandNameForChannel('apm'),
askForPrivilege,
callback
)
@@ -64,11 +78,11 @@ class CommandInstaller {
fs.readlink(destinationPath, (error, realpath) => {
if (error && error.code !== 'ENOENT') return callback(error)
if (realpath === commandPath) return callback()
if (realpath === commandPath) return callback(null, commandName)
this.createSymlink(fs, commandPath, destinationPath, error => {
if (error && error.code === 'EACCES' && askForPrivilege) {
const fsAdmin = require('fs-admin')
this.createSymlink(fsAdmin, commandPath, destinationPath, callback)
this.createSymlink(fsAdmin, commandPath, destinationPath, (error) => { callback(error, commandName) })
} else {
callback(error)
}

View File

@@ -17,6 +17,7 @@ var packageTranspilationRegistry = new PackageTranspilationRegistry()
var COMPILERS = {
'.js': packageTranspilationRegistry.wrapTranspiler(require('./babel')),
'.ts': packageTranspilationRegistry.wrapTranspiler(require('./typescript')),
'.tsx': packageTranspilationRegistry.wrapTranspiler(require('./typescript')),
'.coffee': packageTranspilationRegistry.wrapTranspiler(require('./coffee-script'))
}

145
src/config-file.js Normal file
View File

@@ -0,0 +1,145 @@
const _ = require('underscore-plus')
const fs = require('fs-plus')
const dedent = require('dedent')
const {Emitter} = require('event-kit')
const {watchPath} = require('./path-watcher')
const CSON = require('season')
const Path = require('path')
const async = require('async')
const temp = require('temp')
const EVENT_TYPES = new Set([
'created',
'modified',
'renamed'
])
module.exports =
class ConfigFile {
static at (path) {
if (!this._known) {
this._known = new Map()
}
const existing = this._known.get(path)
if (existing) {
return existing
}
const created = new ConfigFile(path)
this._known.set(path, created)
return created
}
constructor (path) {
this.path = path
this.emitter = new Emitter()
this.value = {}
this.reloadCallbacks = []
// Use a queue to prevent multiple concurrent write to the same file.
const writeQueue = async.queue((data, callback) => {
(async () => {
try {
await writeCSONFileAtomically(this.path, data)
} catch (error) {
this.emitter.emit('did-error', dedent `
Failed to write \`${Path.basename(this.path)}\`.
${error.message}
`)
}
callback()
})()
})
this.requestLoad = _.debounce(() => this.reload(), 200)
this.requestSave = _.debounce((data) => writeQueue.push(data), 200)
}
get () {
return this.value
}
update (value) {
return new Promise(resolve => {
this.requestSave(value)
this.reloadCallbacks.push(resolve)
})
}
async watch (callback) {
if (!fs.existsSync(this.path)) {
fs.makeTreeSync(Path.dirname(this.path))
CSON.writeFileSync(this.path, {}, {flag: 'wx'})
}
await this.reload()
try {
const watcher = await watchPath(this.path, {}, events => {
if (events.some(event => EVENT_TYPES.has(event.action))) this.requestLoad()
})
return watcher
} catch (error) {
this.emitter.emit('did-error', dedent `
Unable to watch path: \`${Path.basename(this.path)}\`.
Make sure you have permissions to \`${this.path}\`.
On linux there are currently problems with watch sizes.
See [this document][watches] for more info.
[watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path\
`)
}
}
onDidChange (callback) {
return this.emitter.on('did-change', callback)
}
onDidError (callback) {
return this.emitter.on('did-error', callback)
}
reload () {
return new Promise(resolve => {
CSON.readFile(this.path, (error, data) => {
if (error) {
this.emitter.emit('did-error', `Failed to load \`${Path.basename(this.path)}\` - ${error.message}`)
} else {
this.value = data || {}
this.emitter.emit('did-change', this.value)
for (const callback of this.reloadCallbacks) callback()
this.reloadCallbacks.length = 0
}
resolve()
})
})
}
}
function writeCSONFile (path, data) {
return new Promise((resolve, reject) => {
CSON.writeFile(path, data, error => {
if (error) reject(error)
else resolve()
})
})
}
async function writeCSONFileAtomically (path, data) {
const tempPath = temp.path()
await writeCSONFile(tempPath, data)
await rename(tempPath, path)
}
function rename (oldPath, newPath) {
return new Promise((resolve, reject) => {
fs.rename(oldPath, newPath, error => {
if (error) reject(error)
else resolve()
})
})
}

View File

@@ -337,6 +337,14 @@ const configSchema = {
value: 'native',
description: 'Native operating system APIs'
},
{
value: 'experimental',
description: 'Experimental filesystem watching library'
},
{
value: 'poll',
description: 'Polling'
},
{
value: 'atom',
description: 'Emulated with Atom events'
@@ -346,7 +354,22 @@ const configSchema = {
useTreeSitterParsers: {
type: 'boolean',
default: false,
description: 'Use the new Tree-sitter parsing system for supported languages'
description: 'Experimental: Use the new Tree-sitter parsing system for supported languages.'
},
colorProfile: {
description: "Specify whether Atom should use the operating system's color profile (recommended) or an alternative color profile.<br>Changing this setting will require a relaunch of Atom to take effect.",
type: 'string',
default: 'default',
enum: [
{
value: 'default',
description: 'Use color profile configured in the operating system'
},
{
value: 'srgb',
description: 'Use sRGB color profile'
}
]
}
}
},
@@ -372,7 +395,7 @@ const configSchema = {
// These can be used as globals or scoped, thus defaults.
fontFamily: {
type: 'string',
default: '',
default: 'Menlo, Consolas, DejaVu Sans Mono, monospace',
description: 'The name of the font family used for editor text.'
},
fontSize: {

File diff suppressed because it is too large Load Diff

1484
src/config.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ fs = require 'fs-plus'
{Disposable} = require 'event-kit'
{remote} = require 'electron'
MenuHelpers = require './menu-helpers'
{sortMenuItems} = require './menu-sort-helpers'
platformContextMenu = require('../package.json')?._atomMenu?['context-menu']
@@ -149,7 +150,7 @@ class ContextMenuManager
@pruneRedundantSeparators(template)
@addAccelerators(template)
template
return @sortTemplate(template)
# Adds an `accelerator` property to items that have key bindings. Electron
# uses this property to surface the relevant keymaps in the context menu.
@@ -175,6 +176,13 @@ class ContextMenuManager
keepNextItemIfSeparator = true
index++
sortTemplate: (template) ->
template = sortMenuItems(template)
for id, item of template
if Array.isArray(item.submenu)
item.submenu = @sortTemplate(item.submenu)
return template
# Returns an object compatible with `::add()` or `null`.
cloneItemForEvent: (item, event) ->
return null if item.devMode and not @devMode

View File

@@ -326,7 +326,9 @@ class Cursor extends Model {
// Public: Moves the cursor to the bottom of the buffer.
moveToBottom () {
const column = this.goalColumn
this.setBufferPosition(this.editor.getEofBufferPosition())
this.goalColumn = column
}
// Public: Moves the cursor to the beginning of the line.
@@ -524,7 +526,7 @@ class Cursor extends Model {
: new Range(new Point(position.row, 0), position)
const ranges = this.editor.buffer.findAllInRangeSync(
options.wordRegex || this.wordRegExp(),
options.wordRegex || this.wordRegExp(options),
scanRange
)
@@ -556,7 +558,7 @@ class Cursor extends Model {
: new Range(position, new Point(position.row, Infinity))
const ranges = this.editor.buffer.findAllInRangeSync(
options.wordRegex || this.wordRegExp(),
options.wordRegex || this.wordRegExp(options),
scanRange
)
@@ -664,7 +666,7 @@ class Cursor extends Model {
// Returns a {RegExp}.
wordRegExp (options) {
const nonWordCharacters = _.escapeRegExp(this.getNonWordCharacters())
let source = `^[\t\r ]*$|[^\\s${nonWordCharacters}]+`
let source = `^[\t ]*$|[^\\s${nonWordCharacters}]+`
if (!options || options.includeNonWordCharacters !== false) {
source += `|${`[${nonWordCharacters}]+`}`
}
@@ -711,6 +713,7 @@ class Cursor extends Model {
changePosition (options, fn) {
this.clearSelection({autoscroll: false})
fn()
this.goalColumn = null
const autoscroll = (options && options.autoscroll != null)
? options.autoscroll
: this.isLastCursor()

View File

@@ -1,4 +1,3 @@
const _ = require('underscore-plus')
const {Emitter} = require('event-kit')
let idCounter = 0
@@ -49,7 +48,7 @@ class Decoration {
// 'line-number' is a 'gutter', but a 'gutter' is not a 'line-number'.
static isType (decorationProperties, type) {
// 'line-number' is a special case of 'gutter'.
if (_.isArray(decorationProperties.type)) {
if (Array.isArray(decorationProperties.type)) {
if (decorationProperties.type.includes(type)) {
return true
}
@@ -158,7 +157,7 @@ class Decoration {
// ## Examples
//
// ```coffee
// decoration.update({type: 'line-number', class: 'my-new-class'})
// decoration.setProperties({type: 'line-number', class: 'my-new-class'})
// ```
//
// * `newProperties` {Object} eg. `{type: 'line-number', class: 'my-new-class'}`

View File

@@ -1,6 +1,4 @@
/** @babel */
import {Disposable} from 'event-kit'
const {Disposable} = require('event-kit')
// Extended: Manages the deserializers used for serialized state
//
@@ -21,7 +19,8 @@ import {Disposable} from 'event-kit'
// serialize: ->
// @state
// ```
export default class DeserializerManager {
module.exports =
class DeserializerManager {
constructor (atomEnvironment) {
this.atomEnvironment = atomEnvironment
this.deserializers = {}
@@ -34,7 +33,7 @@ export default class DeserializerManager {
// common approach is to register a *constructor* as the deserializer for its
// instances by adding a `.deserialize()` class method. When your method is
// called, it will be passed serialized state as the first argument and the
// {Atom} environment object as the second argument, which is useful if you
// {AtomEnvironment} object as the second argument, which is useful if you
// wish to avoid referencing the `atom` global.
add (...deserializers) {
for (let i = 0; i < deserializers.length; i++) {

View File

@@ -1,11 +1,11 @@
'use strict'
const etch = require('etch')
const _ = require('underscore-plus')
const {CompositeDisposable, Emitter} = require('event-kit')
const PaneContainer = require('./pane-container')
const TextEditor = require('./text-editor')
const Grim = require('grim')
const $ = etch.dom
const MINIMUM_SIZE = 100
const DEFAULT_INITIAL_SIZE = 300
const SHOULD_ANIMATE_CLASS = 'atom-dock-should-animate'
@@ -26,6 +26,8 @@ module.exports = class Dock {
this.handleMouseUp = this.handleMouseUp.bind(this)
this.handleDrag = _.throttle(this.handleDrag.bind(this), 30)
this.handleDragEnd = this.handleDragEnd.bind(this)
this.handleToggleButtonDragEnter = this.handleToggleButtonDragEnter.bind(this)
this.toggle = this.toggle.bind(this)
this.location = params.location
this.widthOrHeight = getWidthOrHeight(this.location)
@@ -72,11 +74,16 @@ module.exports = class Dock {
// This method is called explicitly by the object which adds the Dock to the document.
elementAttached () {
// Re-render when the dock is attached to make sure we remeasure sizes defined in CSS.
this.render(this.state)
etch.updateSync(this)
}
getElement () {
if (!this.element) this.render(this.state)
// Because this code is included in the snapshot, we have to make sure we don't touch the DOM
// during initialization. Therefore, we defer initialization of the component (which creates a
// DOM element) until somebody asks for the element.
if (this.element == null) {
etch.initialize(this)
}
return this.element
}
@@ -151,88 +158,94 @@ module.exports = class Dock {
}
this.state = nextState
this.render(this.state)
const {visible} = this.state
const {hovered, visible} = this.state
// Render immediately if the dock becomes visible or the size changes in case people are
// measuring after opening, for example.
if (this.element != null) {
if ((visible && !prevState.visible) || (this.state.size !== prevState.size)) etch.updateSync(this)
else etch.update(this)
}
if (hovered !== prevState.hovered) {
this.emitter.emit('did-change-hovered', hovered)
}
if (visible !== prevState.visible) {
this.emitter.emit('did-change-visible', visible)
}
}
render (state) {
if (this.element == null) {
this.element = document.createElement('atom-dock')
this.element.classList.add(this.location)
this.innerElement = document.createElement('div')
this.innerElement.classList.add('atom-dock-inner', this.location)
this.maskElement = document.createElement('div')
this.maskElement.classList.add('atom-dock-mask')
this.wrapperElement = document.createElement('div')
this.wrapperElement.classList.add('atom-dock-content-wrapper', this.location)
this.resizeHandle = new DockResizeHandle({
location: this.location,
onResizeStart: this.handleResizeHandleDragStart,
onResizeToFit: this.handleResizeToFit
})
this.toggleButton = new DockToggleButton({
onDragEnter: this.handleToggleButtonDragEnter.bind(this),
location: this.location,
toggle: this.toggle.bind(this)
})
this.cursorOverlayElement = document.createElement('div')
this.cursorOverlayElement.classList.add('atom-dock-cursor-overlay', this.location)
render () {
const innerElementClassList = ['atom-dock-inner', this.location]
if (this.state.visible) innerElementClassList.push(VISIBLE_CLASS)
// Add the children to the DOM tree
this.element.appendChild(this.innerElement)
this.innerElement.appendChild(this.maskElement)
this.maskElement.appendChild(this.wrapperElement)
this.wrapperElement.appendChild(this.resizeHandle.getElement())
this.wrapperElement.appendChild(this.paneContainer.getElement())
this.wrapperElement.appendChild(this.cursorOverlayElement)
// The toggle button must be rendered outside the mask because (1) it shouldn't be masked and
// (2) if we made the mask larger to avoid masking it, the mask would block mouse events.
this.innerElement.appendChild(this.toggleButton.getElement())
}
const maskElementClassList = ['atom-dock-mask']
if (this.state.shouldAnimate) maskElementClassList.push(SHOULD_ANIMATE_CLASS)
if (state.visible) {
this.innerElement.classList.add(VISIBLE_CLASS)
} else {
this.innerElement.classList.remove(VISIBLE_CLASS)
}
const cursorOverlayElementClassList = ['atom-dock-cursor-overlay', this.location]
if (this.state.resizing) cursorOverlayElementClassList.push(CURSOR_OVERLAY_VISIBLE_CLASS)
if (state.shouldAnimate) {
this.maskElement.classList.add(SHOULD_ANIMATE_CLASS)
} else {
this.maskElement.classList.remove(SHOULD_ANIMATE_CLASS)
}
if (state.resizing) {
this.cursorOverlayElement.classList.add(CURSOR_OVERLAY_VISIBLE_CLASS)
} else {
this.cursorOverlayElement.classList.remove(CURSOR_OVERLAY_VISIBLE_CLASS)
}
const shouldBeVisible = state.visible || state.showDropTarget
const shouldBeVisible = this.state.visible || this.state.showDropTarget
const size = Math.max(MINIMUM_SIZE,
state.size ||
(state.draggingItem && getPreferredSize(state.draggingItem, this.location)) ||
this.state.size ||
(this.state.draggingItem && getPreferredSize(this.state.draggingItem, this.location)) ||
DEFAULT_INITIAL_SIZE
)
// We need to change the size of the mask...
this.maskElement.style[this.widthOrHeight] = `${shouldBeVisible ? size : 0}px`
const maskStyle = {[this.widthOrHeight]: `${shouldBeVisible ? size : 0}px`}
// ...but the content needs to maintain a constant size.
this.wrapperElement.style[this.widthOrHeight] = `${size}px`
const wrapperStyle = {[this.widthOrHeight]: `${size}px`}
this.resizeHandle.update({dockIsVisible: this.state.visible})
this.toggleButton.update({
dockIsVisible: shouldBeVisible,
visible:
// Don't show the toggle button if the dock is closed and empty...
(state.hovered && (this.state.visible || this.getPaneItems().length > 0)) ||
// ...or if the item can't be dropped in that dock.
(!shouldBeVisible && state.draggingItem && isItemAllowed(state.draggingItem, this.location))
})
return $(
'atom-dock',
{className: this.location},
$.div(
{ref: 'innerElement', className: innerElementClassList.join(' ')},
$.div(
{
className: maskElementClassList.join(' '),
style: maskStyle
},
$.div(
{
ref: 'wrapperElement',
className: `atom-dock-content-wrapper ${this.location}`,
style: wrapperStyle
},
$(DockResizeHandle, {
location: this.location,
onResizeStart: this.handleResizeHandleDragStart,
onResizeToFit: this.handleResizeToFit,
dockIsVisible: this.state.visible
}),
$(ElementComponent, {element: this.paneContainer.getElement()}),
$.div({className: cursorOverlayElementClassList.join(' ')})
)
),
$(DockToggleButton, {
ref: 'toggleButton',
onDragEnter: this.state.draggingItem ? this.handleToggleButtonDragEnter : null,
location: this.location,
toggle: this.toggle,
dockIsVisible: shouldBeVisible,
visible:
// Don't show the toggle button if the dock is closed and empty...
(this.state.hovered &&
(this.state.visible || this.getPaneItems().length > 0)) ||
// ...or if the item can't be dropped in that dock.
(!shouldBeVisible &&
this.state.draggingItem &&
isItemAllowed(this.state.draggingItem, this.location))
})
)
)
}
update (props) {
// Since we're interopping with non-etch stuff, this method's actually never called.
return etch.update(this)
}
handleDidAddPaneItem () {
@@ -296,7 +309,7 @@ module.exports = class Dock {
}
handleDrag (event) {
if (!this.pointWithinHoverArea({x: event.pageX, y: event.pageY}, false)) {
if (!this.pointWithinHoverArea({x: event.pageX, y: event.pageY}, true)) {
this.draggedOut()
}
}
@@ -313,9 +326,13 @@ module.exports = class Dock {
// Determine whether the cursor is within the dock hover area. This isn't as simple as just using
// mouseenter/leave because we want to be a little more forgiving. For example, if the cursor is
// over the footer, we want to show the bottom dock's toggle button.
pointWithinHoverArea (point, includeButtonWidth = this.state.hovered) {
const dockBounds = this.innerElement.getBoundingClientRect()
// over the footer, we want to show the bottom dock's toggle button. Also note that our criteria
// for detecting entry are different than detecting exit but, in order for us to avoid jitter, the
// area considered when detecting exit MUST fully encompass the area considered when detecting
// entry.
pointWithinHoverArea (point, detectingExit) {
const dockBounds = this.refs.innerElement.getBoundingClientRect()
// Copy the bounds object since we can't mutate it.
const bounds = {
top: dockBounds.top,
@@ -324,7 +341,20 @@ module.exports = class Dock {
left: dockBounds.left
}
// Include all panels that are closer to the edge than the dock in our calculations.
// To provide a minimum target, expand the area toward the center a bit.
switch (this.location) {
case 'right':
bounds.left = Math.min(bounds.left, bounds.right - 2)
break
case 'bottom':
bounds.top = Math.min(bounds.top, bounds.bottom - 1)
break
case 'left':
bounds.right = Math.max(bounds.right, bounds.left + 2)
break
}
// Further expand the area to include all panels that are closer to the edge than the dock.
switch (this.location) {
case 'right':
bounds.right = Number.POSITIVE_INFINITY
@@ -337,23 +367,41 @@ module.exports = class Dock {
break
}
// The area used when detecting "leave" events is actually larger than when detecting entrances.
if (includeButtonWidth) {
// If we're in this area, we know we're within the hover area without having to take further
// measurements.
if (rectContainsPoint(bounds, point)) return true
// If we're within the toggle button, we're definitely in the hover area. Unfortunately, we
// can't do this measurement conditionally (e.g. only if the toggle button is visible) because
// our knowledge of the toggle's button is incomplete due to CSS animations. (We may think the
// toggle button isn't visible when in actuality it is, but is animating to its hidden state.)
//
// Since `point` is always the current mouse position, one possible optimization would be to
// remove it as an argument and determine whether we're inside the toggle button using
// mouseenter/leave events on it. This class would still need to keep track of the mouse
// position (via a mousemove listener) for the other measurements, though.
const toggleButtonBounds = this.refs.toggleButton.getBounds()
if (rectContainsPoint(toggleButtonBounds, point)) return true
// The area used when detecting exit is actually larger than when detecting entrances. Expand
// our bounds and recheck them.
if (detectingExit) {
const hoverMargin = 20
const {width, height} = this.toggleButton.getBounds()
switch (this.location) {
case 'right':
bounds.left -= width + hoverMargin
bounds.left = Math.min(bounds.left, toggleButtonBounds.left) - hoverMargin
break
case 'bottom':
bounds.top -= height + hoverMargin
bounds.top = Math.min(bounds.top, toggleButtonBounds.top) - hoverMargin
break
case 'left':
bounds.right += width + hoverMargin
bounds.right = Math.max(bounds.right, toggleButtonBounds.right) + hoverMargin
break
}
if (rectContainsPoint(bounds, point)) return true
}
return rectContainsPoint(bounds, point)
return false
}
getInitialSize () {
@@ -574,6 +622,16 @@ module.exports = class Dock {
return this.paneContainer.onDidDestroyPaneItem(callback)
}
// Extended: Invoke the given callback when the hovered state of the dock changes.
//
// * `callback` {Function} to be called when the hovered state changes.
// * `hovered` {Boolean} Is the dock now hovered?
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeHovered (callback) {
return this.emitter.on('did-change-hovered', callback)
}
/*
Section: Pane Items
*/
@@ -659,13 +717,18 @@ module.exports = class Dock {
class DockResizeHandle {
constructor (props) {
this.handleMouseDown = this.handleMouseDown.bind(this)
this.element = document.createElement('div')
this.element.classList.add('atom-dock-resize-handle', props.location)
this.element.addEventListener('mousedown', this.handleMouseDown)
this.props = props
this.update(props)
etch.initialize(this)
}
render () {
const classList = ['atom-dock-resize-handle', this.props.location]
if (this.props.dockIsVisible) classList.push(RESIZE_HANDLE_RESIZABLE_CLASS)
return $.div({
className: classList.join(' '),
on: {mousedown: this.handleMouseDown}
})
}
getElement () {
@@ -681,12 +744,7 @@ class DockResizeHandle {
update (newProps) {
this.props = Object.assign({}, this.props, newProps)
if (this.props.dockIsVisible) {
this.element.classList.add(RESIZE_HANDLE_RESIZABLE_CLASS)
} else {
this.element.classList.remove(RESIZE_HANDLE_RESIZABLE_CLASS)
}
return etch.update(this)
}
handleMouseDown (event) {
@@ -700,22 +758,34 @@ class DockResizeHandle {
class DockToggleButton {
constructor (props) {
this.handleClick = this.handleClick.bind(this)
this.handleDragEnter = this.handleDragEnter.bind(this)
this.element = document.createElement('div')
this.element.classList.add('atom-dock-toggle-button', props.location)
this.element.classList.add(props.location)
this.innerElement = document.createElement('div')
this.innerElement.classList.add('atom-dock-toggle-button-inner', props.location)
this.innerElement.addEventListener('click', this.handleClick)
this.innerElement.addEventListener('dragenter', this.handleDragEnter)
this.iconElement = document.createElement('span')
this.innerElement.appendChild(this.iconElement)
this.element.appendChild(this.innerElement)
this.props = props
this.update(props)
etch.initialize(this)
}
render () {
const classList = ['atom-dock-toggle-button', this.props.location]
if (this.props.visible) classList.push(TOGGLE_BUTTON_VISIBLE_CLASS)
return $.div(
{className: classList.join(' ')},
$.div(
{
ref: 'innerElement',
className: `atom-dock-toggle-button-inner ${this.props.location}`,
on: {
click: this.handleClick,
dragenter: this.props.onDragEnter
}
},
$.span({
ref: 'iconElement',
className: `icon ${getIconName(
this.props.location,
this.props.dockIsVisible
)}`
})
)
)
}
getElement () {
@@ -723,30 +793,28 @@ class DockToggleButton {
}
getBounds () {
if (this.bounds == null) {
this.bounds = this.element.getBoundingClientRect()
}
return this.bounds
return this.refs.innerElement.getBoundingClientRect()
}
update (newProps) {
this.props = Object.assign({}, this.props, newProps)
if (this.props.visible) {
this.element.classList.add(TOGGLE_BUTTON_VISIBLE_CLASS)
} else {
this.element.classList.remove(TOGGLE_BUTTON_VISIBLE_CLASS)
}
this.iconElement.className = 'icon ' + getIconName(this.props.location, this.props.dockIsVisible)
return etch.update(this)
}
handleClick () {
this.props.toggle()
}
}
handleDragEnter () {
this.props.onDragEnter()
// An etch component that doesn't use etch, this component provides a gateway from JSX back into
// the mutable DOM world.
class ElementComponent {
constructor (props) {
this.element = props.element
}
update (props) {
this.element = props.element
}
}

View File

@@ -163,7 +163,7 @@ class GitRepository {
// 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.
// {::getPathStatus} to get the status for your path of choice.
//
// * `callback` {Function}
//

View File

@@ -10,7 +10,6 @@ const Token = require('./token')
const fs = require('fs-plus')
const {Point, Range} = require('text-buffer')
const GRAMMAR_TYPE_BONUS = 1000
const PATH_SPLIT_REGEX = new RegExp('[/.]')
// Extended: This class holds the grammars used for tokenizing.
@@ -38,6 +37,14 @@ class GrammarRegistry {
const grammarAddedOrUpdated = this.grammarAddedOrUpdated.bind(this)
this.textmateRegistry.onDidAddGrammar(grammarAddedOrUpdated)
this.textmateRegistry.onDidUpdateGrammar(grammarAddedOrUpdated)
this.subscriptions.add(this.config.onDidChange('core.useTreeSitterParsers', () => {
this.grammarScoresByBuffer.forEach((score, buffer) => {
if (!this.languageOverridesByBufferId.has(buffer.id)) {
this.autoAssignLanguageMode(buffer)
}
})
}))
}
serialize () {
@@ -115,7 +122,6 @@ class GrammarRegistry {
// found.
assignLanguageMode (buffer, languageId) {
if (buffer.getBuffer) buffer = buffer.getBuffer()
languageId = this.normalizeLanguageId(languageId)
let grammar = null
if (languageId != null) {
@@ -128,13 +134,21 @@ class GrammarRegistry {
}
this.grammarScoresByBuffer.set(buffer, null)
if (grammar.scopeName !== buffer.getLanguageMode().getLanguageId()) {
if (grammar !== buffer.getLanguageMode().grammar) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
}
return true
}
// Extended: Get the `languageId` that has been explicitly assigned to
// to the given buffer, if any.
//
// Returns a {String} id of the language
getAssignedLanguageId (buffer) {
return this.languageOverridesByBufferId.get(buffer.id)
}
// Extended: Remove any language mode override that has been set for the
// given {TextBuffer}. This will assign to the buffer the best language
// mode available.
@@ -147,14 +161,14 @@ class GrammarRegistry {
)
this.languageOverridesByBufferId.delete(buffer.id)
this.grammarScoresByBuffer.set(buffer, result.score)
if (result.grammar.scopeName !== buffer.getLanguageMode().getLanguageId()) {
if (result.grammar !== buffer.getLanguageMode().grammar) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(result.grammar, buffer))
}
}
languageModeForGrammarAndBuffer (grammar, buffer) {
if (grammar instanceof TreeSitterGrammar) {
return new TreeSitterLanguageMode({grammar, buffer, config: this.config})
return new TreeSitterLanguageMode({grammar, buffer, config: this.config, grammars: this})
} else {
return new TextMateLanguageMode({grammar, buffer, config: this.config})
}
@@ -193,16 +207,34 @@ class GrammarRegistry {
contents = fs.readFileSync(filePath, 'utf8')
}
// Initially identify matching grammars based on the filename and the first
// line of the file.
let score = this.getGrammarPathScore(grammar, filePath)
if (score > 0 && !grammar.bundledPackage) {
score += 0.125
}
if (this.grammarMatchesContents(grammar, contents)) {
score += 0.25
}
if (this.grammarMatchesPrefix(grammar, contents)) score += 0.5
if (score > 0 && this.isGrammarPreferredType(grammar)) {
score += GRAMMAR_TYPE_BONUS
// If multiple grammars match by one of the above criteria, break ties.
if (score > 0) {
// Prefer either TextMate or Tree-sitter grammars based on the user's settings.
if (grammar instanceof TreeSitterGrammar) {
if (this.config.get('core.useTreeSitterParsers')) {
score += 0.1
} else {
return -Infinity
}
}
// Prefer grammars with matching content regexes. Prefer a grammar with no content regex
// over one with a non-matching content regex.
if (grammar.contentRegex) {
if (grammar.contentRegex.test(contents)) {
score += 0.05
} else {
score -= 0.05
}
}
// Prefer grammars that the user has manually installed over bundled grammars.
if (!grammar.bundledPackage) score += 0.01
}
return score
@@ -240,12 +272,8 @@ class GrammarRegistry {
return pathScore
}
grammarMatchesContents (grammar, contents) {
if (contents == null) return false
if (grammar.contentRegExp) { // TreeSitter grammars
return grammar.contentRegExp.test(contents)
} else if (grammar.firstLineRegex) { // FirstMate grammars
grammarMatchesPrefix (grammar, contents) {
if (contents && grammar.firstLineRegex) {
let escaped = false
let numberOfNewlinesInRegex = 0
for (let character of grammar.firstLineRegex.source) {
@@ -262,8 +290,12 @@ class GrammarRegistry {
}
}
const lines = contents.split('\n')
return grammar.firstLineRegex.testSync(lines.slice(0, numberOfNewlinesInRegex + 1).join('\n'))
const prefix = contents.split('\n').slice(0, numberOfNewlinesInRegex + 1).join('\n')
if (grammar.firstLineRegex.testSync) {
return grammar.firstLineRegex.testSync(prefix)
} else {
return grammar.firstLineRegex.test(prefix)
}
} else {
return false
}
@@ -271,18 +303,25 @@ class GrammarRegistry {
forEachGrammar (callback) {
this.textmateRegistry.grammars.forEach(callback)
for (let grammarId in this.treeSitterGrammarsById) {
callback(this.treeSitterGrammarsById[grammarId])
for (const grammarId in this.treeSitterGrammarsById) {
const grammar = this.treeSitterGrammarsById[grammarId]
if (grammar.scopeName) callback(grammar)
}
}
grammarForId (languageId) {
languageId = this.normalizeLanguageId(languageId)
return (
this.textmateRegistry.grammarForScopeName(languageId) ||
this.treeSitterGrammarsById[languageId]
)
if (!languageId) return null
if (this.config.get('core.useTreeSitterParsers')) {
return (
this.treeSitterGrammarsById[languageId] ||
this.textmateRegistry.grammarForScopeName(languageId)
)
} else {
return (
this.textmateRegistry.grammarForScopeName(languageId) ||
this.treeSitterGrammarsById[languageId]
)
}
}
// Deprecated: Get the grammar override for the given file path.
@@ -293,7 +332,7 @@ class GrammarRegistry {
grammarOverrideForPath (filePath) {
Grim.deprecate('Use buffer.getLanguageMode().getLanguageId() instead')
const buffer = atom.project.findBufferForPath(filePath)
if (buffer) return this.languageOverridesByBufferId.get(buffer.id)
if (buffer) return this.getAssignedLanguageId(buffer)
}
// Deprecated: Set the grammar override for the given file path.
@@ -327,26 +366,23 @@ class GrammarRegistry {
this.grammarScoresByBuffer.forEach((score, buffer) => {
const languageMode = buffer.getLanguageMode()
if (grammar.injectionSelector) {
if (languageMode.hasTokenForSelector(grammar.injectionSelector)) {
languageMode.retokenizeLines()
}
return
}
const languageOverride = this.languageOverridesByBufferId.get(buffer.id)
if ((grammar.id === buffer.getLanguageMode().getLanguageId() ||
grammar.id === languageOverride)) {
if (grammar === buffer.getLanguageMode().grammar ||
grammar === this.grammarForId(languageOverride)) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
return
} else if (!languageOverride) {
const score = this.getGrammarScore(grammar, buffer.getPath(), getGrammarSelectionContent(buffer))
const currentScore = this.grammarScoresByBuffer.get(buffer)
if (currentScore == null || score > currentScore) {
buffer.setLanguageMode(this.languageModeForGrammarAndBuffer(grammar, buffer))
this.grammarScoresByBuffer.set(buffer, score)
return
}
}
languageMode.updateForInjection(grammar)
})
}
@@ -371,6 +407,32 @@ class GrammarRegistry {
return this.textmateRegistry.onDidUpdateGrammar(callback)
}
// Experimental: Specify a type of syntax node that may embed other languages.
//
// * `grammarId` The {String} id of the parent language
// * `injectionPoint` An {Object} with the following keys:
// * `type` The {String} type of syntax node that may embed other languages
// * `language` A {Function} that is called with syntax nodes of the specified `type` and
// returns a {String} that will be tested against other grammars' `injectionRegex` in
// order to determine what language should be embedded.
// * `content` A {Function} that is called with syntax nodes of the specified `type` and
// returns another syntax node or array of syntax nodes that contain the embedded source code.
addInjectionPoint (grammarId, injectionPoint) {
const grammar = this.treeSitterGrammarsById[grammarId]
if (grammar) {
grammar.injectionPoints.push(injectionPoint)
} else {
this.treeSitterGrammarsById[grammarId] = {
injectionPoints: [injectionPoint]
}
}
return new Disposable(() => {
const grammar = this.treeSitterGrammarsById[grammarId]
const index = grammar.injectionPoints.indexOf(injectionPoint)
if (index !== -1) grammar.injectionPoints.splice(index, 1)
})
}
get nullGrammar () {
return this.textmateRegistry.nullGrammar
}
@@ -389,12 +451,9 @@ class GrammarRegistry {
addGrammar (grammar) {
if (grammar instanceof TreeSitterGrammar) {
this.treeSitterGrammarsById[grammar.id] = grammar
if (grammar.legacyScopeName) {
this.config.addLegacyScopeAlias(grammar.id, grammar.legacyScopeName)
this.textMateScopeNamesByTreeSitterLanguageId.set(grammar.id, grammar.legacyScopeName)
this.treeSitterLanguageIdsByTextMateScopeName.set(grammar.legacyScopeName, grammar.id)
}
const existingParams = this.treeSitterGrammarsById[grammar.scopeName] || {}
if (grammar.scopeName) this.treeSitterGrammarsById[grammar.scopeName] = grammar
if (existingParams.injectionPoints) grammar.injectionPoints.push(...existingParams.injectionPoints)
this.grammarAddedOrUpdated(grammar)
return new Disposable(() => this.removeGrammar(grammar))
} else {
@@ -404,12 +463,7 @@ class GrammarRegistry {
removeGrammar (grammar) {
if (grammar instanceof TreeSitterGrammar) {
delete this.treeSitterGrammarsById[grammar.id]
if (grammar.legacyScopeName) {
this.config.removeLegacyScopeAlias(grammar.id)
this.textMateScopeNamesByTreeSitterLanguageId.delete(grammar.id)
this.treeSitterLanguageIdsByTextMateScopeName.delete(grammar.legacyScopeName)
}
delete this.treeSitterGrammarsById[grammar.scopeName]
} else {
return this.textmateRegistry.removeGrammar(grammar)
}
@@ -429,7 +483,7 @@ class GrammarRegistry {
this.readGrammar(grammarPath, (error, grammar) => {
if (error) return callback(error)
this.addGrammar(grammar)
callback(grammar)
callback(null, grammar)
})
}
@@ -495,10 +549,23 @@ class GrammarRegistry {
return this.textmateRegistry.scopeForId(id)
}
isGrammarPreferredType (grammar) {
return this.config.get('core.useTreeSitterParsers')
? grammar instanceof TreeSitterGrammar
: grammar instanceof FirstMate.Grammar
treeSitterGrammarForLanguageString (languageString) {
let longestMatchLength = 0
let grammarWithLongestMatch = null
for (const id in this.treeSitterGrammarsById) {
const grammar = this.treeSitterGrammarsById[id]
if (grammar.injectionRegex) {
const match = languageString.match(grammar.injectionRegex)
if (match) {
const {length} = match[0]
if (length > longestMatchLength) {
grammarWithLongestMatch = grammar
longestMatchLength = length
}
}
}
}
return grammarWithLongestMatch
}
normalizeLanguageId (languageId) {

View File

@@ -97,7 +97,7 @@ module.exports = class GutterContainer {
// The public interface is Gutter::decorateMarker or TextEditor::decorateMarker.
addGutterDecoration (gutter, marker, options) {
if (gutter.name === 'line-number') {
if (gutter.type === 'line-number') {
options.type = 'line-number'
} else {
options.type = 'gutter'

View File

@@ -11,6 +11,12 @@ module.exports = class Gutter {
this.name = options && options.name
this.priority = (options && options.priority != null) ? options.priority : DefaultPriority
this.visible = (options && options.visible != null) ? options.visible : true
this.type = (options && options.type != null) ? options.type : 'decorated'
this.labelFn = options && options.labelFn
this.className = options && options.class
this.onMouseDown = options && options.onMouseDown
this.onMouseMove = options && options.onMouseMove
this.emitter = new Emitter()
}

View File

@@ -1,13 +1,11 @@
/** @babel */
import {Emitter, CompositeDisposable} from 'event-kit'
const {Emitter, CompositeDisposable} = require('event-kit')
// Extended: History manager for remembering which projects have been opened.
//
// An instance of this class is always available as the `atom.history` global.
//
// The project history is used to enable the 'Reopen Project' menu.
export class HistoryManager {
class HistoryManager {
constructor ({project, commands, stateStore}) {
this.stateStore = stateStore
this.emitter = new Emitter()
@@ -116,7 +114,7 @@ function arrayEquivalent (a, b) {
return true
}
export class HistoryProject {
class HistoryProject {
constructor (paths, lastOpened) {
this.paths = paths
this.lastOpened = lastOpened || new Date()
@@ -128,3 +126,5 @@ export class HistoryProject {
set lastOpened (lastOpened) { this._lastOpened = lastOpened }
get lastOpened () { return this._lastOpened }
}
module.exports = {HistoryManager, HistoryProject}

View File

@@ -36,6 +36,9 @@ if global.isGeneratingSnapshot
require('image-view')
require('incompatible-packages')
require('keybinding-resolver')
require('language-html')
require('language-javascript')
require('language-ruby')
require('line-ending-selector')
require('link')
require('markdown-preview')

View File

@@ -1,11 +1,9 @@
/** @babel */
const {remote} = require('electron')
const path = require('path')
const ipcHelpers = require('./ipc-helpers')
const util = require('util')
import {remote} from 'electron'
import path from 'path'
import ipcHelpers from './ipc-helpers'
import util from 'util'
export default async function () {
module.exports = async function () {
const getWindowLoadSettings = require('./get-window-load-settings')
const {test, headless, resourcePath, benchmarkPaths} = getWindowLoadSettings()
try {

View File

@@ -24,9 +24,13 @@ module.exports = ({blobStore}) ->
ApplicationDelegate = require '../src/application-delegate'
Clipboard = require '../src/clipboard'
TextEditor = require '../src/text-editor'
{updateProcessEnv} = require('./update-process-env')
require './electron-shims'
{testRunnerPath, legacyTestRunnerPath, headless, logFile, testPaths} = getWindowLoadSettings()
ipcRenderer.on 'environment', (event, env) ->
updateProcessEnv(env)
{testRunnerPath, legacyTestRunnerPath, headless, logFile, testPaths, env} = getWindowLoadSettings()
unless headless
# Show window synchronously so a focusout doesn't fire on input elements
@@ -59,6 +63,8 @@ module.exports = ({blobStore}) ->
require('module').globalPaths.push(exportsPath)
process.env.NODE_PATH = exportsPath # Set NODE_PATH env variable since tasks may need it.
updateProcessEnv(env)
# Set up optional transpilation for packages under test if any
FindParentDir = require 'find-parent-dir'
if packageRoot = FindParentDir.sync(testPaths[0], 'package.json')

View File

@@ -1,15 +1,13 @@
'use strict'
const Disposable = require('event-kit').Disposable
let ipcRenderer = null
let ipcMain = null
let BrowserWindow = null
let nextResponseChannelId = 0
exports.on = function (emitter, eventName, callback) {
emitter.on(eventName, callback)
return new Disposable(function () {
emitter.removeListener(eventName, callback)
})
return new Disposable(() => emitter.removeListener(eventName, callback))
}
exports.call = function (channel, ...args) {
@@ -18,34 +16,28 @@ exports.call = function (channel, ...args) {
ipcRenderer.setMaxListeners(20)
}
var responseChannel = getResponseChannel(channel)
const responseChannel = `ipc-helpers-response-${nextResponseChannelId++}`
return new Promise(function (resolve) {
ipcRenderer.on(responseChannel, function (event, result) {
return new Promise(resolve => {
ipcRenderer.on(responseChannel, (event, result) => {
ipcRenderer.removeAllListeners(responseChannel)
resolve(result)
})
ipcRenderer.send(channel, ...args)
ipcRenderer.send(channel, responseChannel, ...args)
})
}
exports.respondTo = function (channel, callback) {
if (!ipcMain) {
var electron = require('electron')
const electron = require('electron')
ipcMain = electron.ipcMain
BrowserWindow = electron.BrowserWindow
}
var responseChannel = getResponseChannel(channel)
return exports.on(ipcMain, channel, function (event, ...args) {
var browserWindow = BrowserWindow.fromWebContents(event.sender)
var result = callback(browserWindow, ...args)
return exports.on(ipcMain, channel, async (event, responseChannel, ...args) => {
const browserWindow = BrowserWindow.fromWebContents(event.sender)
const result = await callback(browserWindow, ...args)
event.sender.send(responseChannel, result)
})
}
function getResponseChannel (channel) {
return 'ipc-helpers-' + channel + '-response'
}

View File

@@ -1,15 +0,0 @@
module.exports =
class ItemRegistry
constructor: ->
@items = new WeakSet
addItem: (item) ->
if @hasItem(item)
throw new Error("The workspace can only contain one instance of item #{item}")
@items.add(item)
removeItem: (item) ->
@items.delete(item)
hasItem: (item) ->
@items.has(item)

21
src/item-registry.js Normal file
View File

@@ -0,0 +1,21 @@
module.exports =
class ItemRegistry {
constructor () {
this.items = new WeakSet()
}
addItem (item) {
if (this.hasItem(item)) {
throw new Error(`The workspace can only contain one instance of item ${item}`)
}
return this.items.add(item)
}
removeItem (item) {
return this.items.delete(item)
}
hasItem (item) {
return this.items.has(item)
}
}

View File

@@ -201,7 +201,7 @@ class ApplicationMenu {
if (item.command) {
item.accelerator = this.acceleratorForCommand(item.command, keystrokesByCommand)
item.click = () => global.atomApplication.sendCommand(item.command, item.commandDetail)
if (!/^application:/.test(item.command, item.commandDetail)) {
if (!/^application:/.test(item.command)) {
item.metadata.windowSpecific = true
}
}

View File

@@ -4,6 +4,7 @@ const AtomProtocolHandler = require('./atom-protocol-handler')
const AutoUpdateManager = require('./auto-update-manager')
const StorageFolder = require('../storage-folder')
const Config = require('../config')
const ConfigFile = require('../config-file')
const FileRecoveryService = require('./file-recovery-service')
const ipcHelpers = require('../ipc-helpers')
const {BrowserWindow, Menu, app, clipboard, dialog, ipcMain, shell, screen} = require('electron')
@@ -32,7 +33,7 @@ class AtomApplication extends EventEmitter {
// Public: The entry point into the Atom application.
static open (options) {
if (!options.socketPath) {
const username = process.platform === 'win32' ? process.env.USERNAME : process.env.USER
const {username} = os.userInfo()
// Lowercasing the ATOM_HOME to make sure that we don't get multiple sockets
// on case-insensitive filesystems due to arbitrary case differences in paths.
@@ -43,7 +44,7 @@ class AtomApplication extends EventEmitter {
.update('|')
.update(process.arch)
.update('|')
.update(username)
.update(username || '')
.update('|')
.update(atomHomeUnique)
@@ -92,7 +93,6 @@ class AtomApplication extends EventEmitter {
this.quitting = false
this.getAllWindows = this.getAllWindows.bind(this)
this.getLastFocusedWindow = this.getLastFocusedWindow.bind(this)
this.resourcePath = options.resourcePath
this.devResourcePath = options.devResourcePath
this.version = options.version
@@ -107,20 +107,21 @@ class AtomApplication extends EventEmitter {
this.waitSessionsByWindow = new Map()
this.windowStack = new WindowStack()
this.config = new Config({enablePersistence: true})
this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)})
ConfigSchema.projectHome = {
type: 'string',
default: path.join(fs.getHomeDirectory(), 'github'),
description:
'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.'
}
this.config.initialize({
configDirPath: process.env.ATOM_HOME,
resourcePath: this.resourcePath,
projectHomeSchema: ConfigSchema.projectHome
this.initializeAtomHome(process.env.ATOM_HOME)
const configFilePath = fs.existsSync(path.join(process.env.ATOM_HOME, 'config.json'))
? path.join(process.env.ATOM_HOME, 'config.json')
: path.join(process.env.ATOM_HOME, 'config.cson')
this.configFile = ConfigFile.at(configFilePath)
this.config = new Config({
saveCallback: settings => {
if (!this.quitting) {
return this.configFile.update(settings)
}
}
})
this.config.load()
this.config.setSchema(null, {type: 'object', properties: _.clone(ConfigSchema)})
this.fileRecoveryService = new FileRecoveryService(path.join(process.env.ATOM_HOME, 'recovery'))
this.storageFolder = new StorageFolder(process.env.ATOM_HOME)
@@ -138,7 +139,7 @@ class AtomApplication extends EventEmitter {
// for testing purposes without booting up the world. As you add tests, feel free to move instantiation
// of these various sub-objects into the constructor, but you'll need to remove the side-effects they
// perform during their construction, adding an initialize method that you call here.
initialize (options) {
async initialize (options) {
global.atomApplication = this
// DEPRECATED: This can be removed at some point (added in 1.13)
@@ -148,16 +149,15 @@ class AtomApplication extends EventEmitter {
this.config.set('core.titleBar', 'custom')
}
this.config.onDidChange('core.titleBar', this.promptForRestart.bind(this))
process.nextTick(() => this.autoUpdateManager.initialize())
this.applicationMenu = new ApplicationMenu(this.version, this.autoUpdateManager)
this.atomProtocolHandler = new AtomProtocolHandler(this.resourcePath, this.safeMode)
this.listenForArgumentsFromNewProcess()
this.setupDockMenu()
return this.launch(options)
const result = await this.launch(options)
this.autoUpdateManager.initialize()
return result
}
async destroy () {
@@ -169,18 +169,39 @@ class AtomApplication extends EventEmitter {
this.disposable.dispose()
}
launch (options) {
async launch (options) {
if (!this.configFilePromise) {
this.configFilePromise = this.configFile.watch()
this.disposable.add(await this.configFilePromise)
this.config.onDidChange('core.titleBar', () => this.promptForRestart())
this.config.onDidChange('core.colorProfile', () => this.promptForRestart())
}
const optionsForWindowsToOpen = []
let shouldReopenPreviousWindows = false
if (options.test || options.benchmark || options.benchmarkTest) {
return this.openWithOptions(options)
optionsForWindowsToOpen.push(options)
} else if ((options.pathsToOpen && options.pathsToOpen.length > 0) ||
(options.urlsToOpen && options.urlsToOpen.length > 0)) {
if (this.config.get('core.restorePreviousWindowsOnStart') === 'always') {
this.loadState(_.deepClone(options))
}
return this.openWithOptions(options)
optionsForWindowsToOpen.push(options)
shouldReopenPreviousWindows = this.config.get('core.restorePreviousWindowsOnStart') === 'always'
} else {
return this.loadState(options) || this.openPath(options)
shouldReopenPreviousWindows = this.config.get('core.restorePreviousWindowsOnStart') !== 'no'
}
if (shouldReopenPreviousWindows) {
for (const previousOptions of await this.loadPreviousWindowOptions()) {
optionsForWindowsToOpen.push(Object.assign({}, options, previousOptions))
}
}
if (optionsForWindowsToOpen.length === 0) {
optionsForWindowsToOpen.push(options)
}
return optionsForWindowsToOpen.map(options => this.openWithOptions(options))
}
openWithOptions (options) {
@@ -271,7 +292,7 @@ class AtomApplication extends EventEmitter {
return
}
}
if (!window.isSpec) this.saveState(true)
if (!window.isSpec) this.saveCurrentWindowOptions(true)
}
// Public: Adds the {AtomWindow} to the global window list.
@@ -285,7 +306,7 @@ class AtomApplication extends EventEmitter {
if (!window.isSpec) {
const focusHandler = () => this.windowStack.touch(window)
const blurHandler = () => this.saveState(false)
const blurHandler = () => this.saveCurrentWindowOptions(false)
window.browserWindow.on('focus', focusHandler)
window.browserWindow.on('blur', blurHandler)
window.browserWindow.once('closed', () => {
@@ -397,6 +418,18 @@ class AtomApplication extends EventEmitter {
this.openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet')
this.openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md'))
this.configFile.onDidChange(settings => {
for (let window of this.getAllWindows()) {
window.didChangeUserSettings(settings)
}
this.config.resetUserSettings(settings)
})
this.configFile.onDidError(message => {
const window = this.focusedWindow() || this.getLastFocusedWindow()
if (window) window.didFailToReadUserSettings(message)
})
this.disposable.add(ipcHelpers.on(app, 'before-quit', async event => {
let resolveBeforeQuitPromise
this.lastBeforeQuitPromise = new Promise(resolve => { resolveBeforeQuitPromise = resolve })
@@ -406,7 +439,11 @@ class AtomApplication extends EventEmitter {
event.preventDefault()
const windowUnloadPromises = this.getAllWindows().map(window => window.prepareToUnload())
const windowUnloadedResults = await Promise.all(windowUnloadPromises)
if (windowUnloadedResults.every(Boolean)) app.quit()
if (windowUnloadedResults.every(Boolean)) {
app.quit()
} else {
this.quitting = false
}
}
resolveBeforeQuitPromise()
@@ -474,12 +511,12 @@ class AtomApplication extends EventEmitter {
if (this.applicationMenu) this.applicationMenu.update(window, template, menu)
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'run-package-specs', (event, packageSpecPath) => {
this.runTests({
this.disposable.add(ipcHelpers.on(ipcMain, 'run-package-specs', (event, packageSpecPath, options = {}) => {
this.runTests(Object.assign({
resourcePath: this.devResourcePath,
pathsToOpen: [packageSpecPath],
headless: false
})
}, options))
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'run-benchmarks', (event, benchmarksPath) => {
@@ -530,6 +567,12 @@ class AtomApplication extends EventEmitter {
window.setPosition(x, y)
}))
this.disposable.add(ipcHelpers.respondTo('set-user-settings', (window, settings, filePath) => {
if (!this.quitting) {
ConfigFile.at(filePath || this.configFilePath).update(JSON.parse(settings))
}
}))
this.disposable.add(ipcHelpers.respondTo('center-window', window => window.center()))
this.disposable.add(ipcHelpers.respondTo('focus-window', window => window.focus()))
this.disposable.add(ipcHelpers.respondTo('show-window', window => window.show()))
@@ -568,18 +611,16 @@ class AtomApplication extends EventEmitter {
event.returnValue = this.autoUpdateManager.getErrorMessage()
}))
this.disposable.add(ipcHelpers.on(ipcMain, 'will-save-path', (event, path) => {
this.fileRecoveryService.willSavePath(this.atomWindowForEvent(event), path)
event.returnValue = true
}))
this.disposable.add(ipcHelpers.respondTo('will-save-path', (window, path) =>
this.fileRecoveryService.willSavePath(window, path)
))
this.disposable.add(ipcHelpers.on(ipcMain, 'did-save-path', (event, path) => {
this.fileRecoveryService.didSavePath(this.atomWindowForEvent(event), path)
event.returnValue = true
}))
this.disposable.add(ipcHelpers.respondTo('did-save-path', (window, path) =>
this.fileRecoveryService.didSavePath(window, path)
))
this.disposable.add(ipcHelpers.on(ipcMain, 'did-change-paths', () =>
this.saveState(false)
this.saveCurrentWindowOptions(false)
))
this.disposable.add(this.disableZoomOnDisplayChange())
@@ -593,6 +634,13 @@ class AtomApplication extends EventEmitter {
}
}
initializeAtomHome (configDirPath) {
if (!fs.existsSync(configDirPath)) {
const templateConfigDirPath = fs.resolve(this.resourcePath, 'dot-atom')
fs.copySync(templateConfigDirPath, configDirPath)
}
}
// Public: Executes the given command.
//
// If it isn't handled globally, delegate to the currently focused window.
@@ -800,13 +848,12 @@ class AtomApplication extends EventEmitter {
let existingWindow
if (!newWindow) {
existingWindow = this.windowForPaths(pathsToOpen, devMode)
const stats = pathsToOpen.map(pathToOpen => fs.statSyncNoException(pathToOpen))
if (!existingWindow) {
let lastWindow = window || this.getLastFocusedWindow()
if (lastWindow && lastWindow.devMode === devMode) {
if (addToLastWindow || (
stats.every(s => s.isFile && s.isFile()) ||
(stats.some(s => s.isDirectory && s.isDirectory()) && !lastWindow.hasProjectPath()))) {
locationsToOpen.every(({stat}) => stat && stat.isFile()) ||
(locationsToOpen.some(({stat}) => stat && stat.isDirectory()) && !lastWindow.hasProjectPath()))) {
existingWindow = lastWindow
}
}
@@ -839,6 +886,7 @@ class AtomApplication extends EventEmitter {
}
if (!resourcePath) resourcePath = this.resourcePath
if (!windowDimensions) windowDimensions = this.getDimensionsForNewWindow()
openedWindow = new AtomWindow(this, this.fileRecoveryService, {
initialPaths,
locationsToOpen,
@@ -910,7 +958,7 @@ class AtomApplication extends EventEmitter {
}
}
saveState (allowEmpty = false) {
async saveCurrentWindowOptions (allowEmpty = false) {
if (this.quitting) return
const states = []
@@ -920,28 +968,23 @@ class AtomApplication extends EventEmitter {
states.reverse()
if (states.length > 0 || allowEmpty) {
this.storageFolder.storeSync('application.json', states)
await this.storageFolder.store('application.json', states)
this.emit('application:did-save-state')
}
}
loadState (options) {
const states = this.storageFolder.load('application.json')
if (
['yes', 'always'].includes(this.config.get('core.restorePreviousWindowsOnStart')) &&
states && states.length > 0
) {
return states.map(state =>
this.openWithOptions(Object.assign(options, {
initialPaths: state.initialPaths,
pathsToOpen: state.initialPaths.filter(p => fs.isDirectorySync(p)),
urlsToOpen: [],
devMode: this.devMode,
safeMode: this.safeMode
}))
)
async loadPreviousWindowOptions () {
const states = await this.storageFolder.load('application.json')
if (states) {
return states.map(state => ({
initialPaths: state.initialPaths,
pathsToOpen: state.initialPaths.filter(p => fs.isDirectorySync(p)),
urlsToOpen: [],
devMode: this.devMode,
safeMode: this.safeMode
}))
} else {
return null
return []
}
}
@@ -1124,6 +1167,7 @@ class AtomApplication extends EventEmitter {
env
})
this.addWindow(window)
if (env) window.replaceEnvironment(env)
return window
}
@@ -1234,11 +1278,11 @@ class AtomApplication extends EventEmitter {
initialLine = initialColumn = null
}
if (url.parse(pathToOpen).protocol == null) {
pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen))
}
const normalizedPath = path.normalize(path.resolve(executedFrom, fs.normalize(pathToOpen)))
const stat = fs.statSyncNoException(normalizedPath)
if (stat || !url.parse(pathToOpen).protocol) pathToOpen = normalizedPath
return {pathToOpen, initialLine, initialColumn}
return {pathToOpen, stat, initialLine, initialColumn}
}
// Opens a native dialog to prompt the user for a path.
@@ -1292,17 +1336,16 @@ class AtomApplication extends EventEmitter {
// File dialog defaults to project directory of currently active editor
if (path) openOptions.defaultPath = path
return dialog.showOpenDialog(parentWindow, openOptions, callback)
dialog.showOpenDialog(parentWindow, openOptions, callback)
}
promptForRestart () {
const chosen = dialog.showMessageBox(BrowserWindow.getFocusedWindow(), {
dialog.showMessageBox(BrowserWindow.getFocusedWindow(), {
type: 'warning',
title: 'Restart required',
message: 'You will need to restart Atom for this change to take effect.',
buttons: ['Restart Atom', 'Cancel']
})
if (chosen === 0) return this.restart()
}, response => { if (response === 0) this.restart() })
}
restart () {

View File

@@ -20,6 +20,7 @@ class AtomProtocolHandler {
if (!safeMode) {
this.loadPaths.push(path.join(process.env.ATOM_HOME, 'dev', 'packages'))
this.loadPaths.push(path.join(resourcePath, 'packages'))
}
this.loadPaths.push(path.join(process.env.ATOM_HOME, 'packages'))

View File

@@ -51,10 +51,18 @@ class AtomWindow extends EventEmitter {
// taskbar's icon. See https://github.com/atom/atom/issues/4811 for more.
if (process.platform === 'linux') options.icon = ICON_PATH
if (this.shouldAddCustomTitleBar()) options.titleBarStyle = 'hidden'
if (this.shouldAddCustomInsetTitleBar()) options.titleBarStyle = 'hidden-inset'
if (this.shouldAddCustomInsetTitleBar()) options.titleBarStyle = 'hiddenInset'
if (this.shouldHideTitleBar()) options.frame = false
this.browserWindow = new BrowserWindow(options)
Object.defineProperty(this.browserWindow, 'loadSettingsJSON', {
get: () => JSON.stringify(Object.assign({
userSettings: !this.isSpec
? this.atomApplication.configFile.get()
: null
}, this.loadSettings))
})
this.handleEvents()
this.loadSettings = Object.assign({}, settings)
@@ -67,14 +75,13 @@ class AtomWindow extends EventEmitter {
if (!this.loadSettings.initialPaths) {
this.loadSettings.initialPaths = []
for (const {pathToOpen} of locationsToOpen) {
for (const {pathToOpen, stat} of locationsToOpen) {
if (!pathToOpen) continue
const stat = fs.statSyncNoException(pathToOpen) || null
if (stat && stat.isDirectory()) {
this.loadSettings.initialPaths.push(pathToOpen)
} else {
const parentDirectory = path.dirname(pathToOpen)
if ((stat && stat.isFile()) || fs.existsSync(parentDirectory)) {
if (stat && stat.isFile() || fs.existsSync(parentDirectory)) {
this.loadSettings.initialPaths.push(parentDirectory)
} else {
this.loadSettings.initialPaths.push(pathToOpen)
@@ -96,8 +103,6 @@ class AtomWindow extends EventEmitter {
this.representedDirectoryPaths = this.loadSettings.initialPaths
if (!this.loadSettings.env) this.env = this.loadSettings.env
this.browserWindow.loadSettingsJSON = JSON.stringify(this.loadSettings)
this.browserWindow.on('window:loaded', () => {
this.disableZoom()
this.emit('window:loaded')
@@ -150,12 +155,13 @@ class AtomWindow extends EventEmitter {
containsPath (pathToCheck) {
if (!pathToCheck) return false
const stat = fs.statSyncNoException(pathToCheck)
if (stat && stat.isDirectory()) return false
return this.representedDirectoryPaths.some(projectPath =>
pathToCheck === projectPath || pathToCheck.startsWith(path.join(projectPath, path.sep))
)
let stat
return this.representedDirectoryPaths.some(projectPath => {
if (pathToCheck === projectPath) return true
if (!pathToCheck.startsWith(path.join(projectPath, path.sep))) return false
if (stat === undefined) stat = fs.statSyncNoException(pathToCheck)
return !stat || !stat.isDirectory()
})
}
handleEvents () {
@@ -163,7 +169,7 @@ class AtomWindow extends EventEmitter {
if (!this.atomApplication.quitting && !this.unloading) {
event.preventDefault()
this.unloading = true
this.atomApplication.saveState(false)
this.atomApplication.saveCurrentWindowOptions(false)
if (await this.prepareToUnload()) this.close()
}
})
@@ -176,34 +182,36 @@ class AtomWindow extends EventEmitter {
this.browserWindow.on('unresponsive', () => {
if (this.isSpec) return
const chosen = dialog.showMessageBox(this.browserWindow, {
dialog.showMessageBox(this.browserWindow, {
type: 'warning',
buttons: ['Force Close', 'Keep Waiting'],
cancelId: 1, // Canceling should be the least destructive action
message: 'Editor is not responding',
detail:
'The editor is not responding. Would you like to force close it or just keep waiting?'
})
if (chosen === 0) this.browserWindow.destroy()
}, response => { if (response === 0) this.browserWindow.destroy() })
})
this.browserWindow.webContents.on('crashed', () => {
this.browserWindow.webContents.on('crashed', async () => {
if (this.headless) {
console.log('Renderer process crashed, exiting')
this.atomApplication.exit(100)
return
}
this.fileRecoveryService.didCrashWindow(this)
const chosen = dialog.showMessageBox(this.browserWindow, {
await this.fileRecoveryService.didCrashWindow(this)
dialog.showMessageBox(this.browserWindow, {
type: 'warning',
buttons: ['Close Window', 'Reload', 'Keep It Open'],
cancelId: 2, // Canceling should be the least destructive action
message: 'The editor has crashed',
detail: 'Please report this issue to https://github.com/atom/atom'
}, response => {
switch (response) {
case 0: return this.browserWindow.destroy()
case 1: return this.browserWindow.reload()
}
})
switch (chosen) {
case 0: return this.browserWindow.destroy()
case 1: return this.browserWindow.reload()
}
})
this.browserWindow.webContents.on('will-navigate', (event, url) => {
@@ -246,6 +254,14 @@ class AtomWindow extends EventEmitter {
this.sendMessage('open-locations', locationsToOpen)
}
didChangeUserSettings (settings) {
this.sendMessage('did-change-user-settings', settings)
}
didFailToReadUserSettings (message) {
this.sendMessage('did-fail-to-read-user-settings', message)
}
replaceEnvironment (env) {
this.browserWindow.webContents.send('environment', env)
}
@@ -414,8 +430,7 @@ class AtomWindow extends EventEmitter {
this.representedDirectoryPaths = representedDirectoryPaths
this.representedDirectoryPaths.sort()
this.loadSettings.initialPaths = this.representedDirectoryPaths
this.browserWindow.loadSettingsJSON = JSON.stringify(this.loadSettings)
return this.atomApplication.saveState()
return this.atomApplication.saveCurrentWindowOptions()
}
didClosePathWithWaitSession (path) {

View File

@@ -94,7 +94,7 @@ class AutoUpdateManager
scheduleUpdateCheck: ->
# Only schedule update check periodically if running in release version and
# and there is no existing scheduled update check.
unless /\w{7}/.test(@version) or @checkForUpdatesIntervalID
unless /-dev/.test(@version) or @checkForUpdatesIntervalID
checkForUpdates = => @check(hidePopups: true)
fourHours = 1000 * 60 * 60 * 4
@checkForUpdatesIntervalID = setInterval(checkForUpdates, fourHours)
@@ -118,24 +118,26 @@ class AutoUpdateManager
onUpdateNotAvailable: =>
autoUpdater.removeListener 'error', @onUpdateError
{dialog} = require 'electron'
dialog.showMessageBox
dialog.showMessageBox {
type: 'info'
buttons: ['OK']
icon: @iconPath
message: 'No update available.'
title: 'No Update Available'
detail: "Version #{@version} is the latest version."
}, -> # noop callback to get async behavior
onUpdateError: (event, message) =>
autoUpdater.removeListener 'update-not-available', @onUpdateNotAvailable
{dialog} = require 'electron'
dialog.showMessageBox
dialog.showMessageBox {
type: 'warning'
buttons: ['OK']
icon: @iconPath
message: 'There was an error checking for updates.'
title: 'Update Error'
detail: message
}, -> # noop callback to get async behavior
getWindows: ->
global.atomApplication.getAllWindows()

View File

@@ -1,11 +1,11 @@
'use babel'
const {dialog} = require('electron')
const crypto = require('crypto')
const Path = require('path')
const fs = require('fs-plus')
const mkdirp = require('mkdirp')
import {dialog} from 'electron'
import crypto from 'crypto'
import Path from 'path'
import fs from 'fs-plus'
export default class FileRecoveryService {
module.exports =
class FileRecoveryService {
constructor (recoveryDirectory) {
this.recoveryDirectory = recoveryDirectory
this.recoveryFilesByFilePath = new Map()
@@ -13,15 +13,16 @@ export default class FileRecoveryService {
this.windowsByRecoveryFile = new Map()
}
willSavePath (window, path) {
if (!fs.existsSync(path)) return
async willSavePath (window, path) {
const stats = await tryStatFile(path)
if (!stats) return
const recoveryPath = Path.join(this.recoveryDirectory, RecoveryFile.fileNameForPath(path))
const recoveryFile =
this.recoveryFilesByFilePath.get(path) || new RecoveryFile(path, recoveryPath)
this.recoveryFilesByFilePath.get(path) || new RecoveryFile(path, stats.mode, recoveryPath)
try {
recoveryFile.retain()
await recoveryFile.retain()
} catch (err) {
console.log(`Couldn't retain ${recoveryFile.recoveryPath}. Code: ${err.code}. Message: ${err.message}`)
return
@@ -39,11 +40,11 @@ export default class FileRecoveryService {
this.recoveryFilesByFilePath.set(path, recoveryFile)
}
didSavePath (window, path) {
async didSavePath (window, path) {
const recoveryFile = this.recoveryFilesByFilePath.get(path)
if (recoveryFile != null) {
try {
recoveryFile.release()
await recoveryFile.release()
} catch (err) {
console.log(`Couldn't release ${recoveryFile.recoveryPath}. Code: ${err.code}. Message: ${err.message}`)
}
@@ -53,27 +54,31 @@ export default class FileRecoveryService {
}
}
didCrashWindow (window) {
async didCrashWindow (window) {
if (!this.recoveryFilesByWindow.has(window)) return
const promises = []
for (const recoveryFile of this.recoveryFilesByWindow.get(window)) {
try {
recoveryFile.recoverSync()
} catch (error) {
const message = 'A file that Atom was saving could be corrupted'
const detail =
`Error ${error.code}. There was a crash while saving "${recoveryFile.originalPath}", so this file might be blank or corrupted.\n` +
`Atom couldn't recover it automatically, but a recovery file has been saved at: "${recoveryFile.recoveryPath}".`
console.log(detail)
dialog.showMessageBox(window.browserWindow, {type: 'info', buttons: ['OK'], message, detail})
} finally {
for (let window of this.windowsByRecoveryFile.get(recoveryFile)) {
this.recoveryFilesByWindow.get(window).delete(recoveryFile)
}
this.windowsByRecoveryFile.delete(recoveryFile)
this.recoveryFilesByFilePath.delete(recoveryFile.originalPath)
}
promises.push(recoveryFile.recover()
.catch(error => {
const message = 'A file that Atom was saving could be corrupted'
const detail =
`Error ${error.code}. There was a crash while saving "${recoveryFile.originalPath}", so this file might be blank or corrupted.\n` +
`Atom couldn't recover it automatically, but a recovery file has been saved at: "${recoveryFile.recoveryPath}".`
console.log(detail)
dialog.showMessageBox(window, {type: 'info', buttons: ['OK'], message, detail}, () => { /* noop callback to get async behavior */ })
})
.then(() => {
for (let window of this.windowsByRecoveryFile.get(recoveryFile)) {
this.recoveryFilesByWindow.get(window).delete(recoveryFile)
}
this.windowsByRecoveryFile.delete(recoveryFile)
this.recoveryFilesByFilePath.delete(recoveryFile.originalPath)
})
)
}
await Promise.all(promises)
}
didCloseWindow (window) {
@@ -94,36 +99,67 @@ class RecoveryFile {
return `${basename}-${randomSuffix}${extension}`
}
constructor (originalPath, recoveryPath) {
constructor (originalPath, fileMode, recoveryPath) {
this.originalPath = originalPath
this.fileMode = fileMode
this.recoveryPath = recoveryPath
this.refCount = 0
}
storeSync () {
fs.copyFileSync(this.originalPath, this.recoveryPath)
async store () {
await copyFile(this.originalPath, this.recoveryPath, this.fileMode)
}
recoverSync () {
fs.copyFileSync(this.recoveryPath, this.originalPath)
this.removeSync()
async recover () {
await copyFile(this.recoveryPath, this.originalPath, this.fileMode)
await this.remove()
}
removeSync () {
fs.unlinkSync(this.recoveryPath)
async remove () {
return new Promise((resolve, reject) =>
fs.unlink(this.recoveryPath, error =>
error && error.code !== 'ENOENT' ? reject(error) : resolve()
)
)
}
retain () {
if (this.isReleased()) this.storeSync()
async retain () {
if (this.isReleased()) await this.store()
this.refCount++
}
release () {
async release () {
this.refCount--
if (this.isReleased()) this.removeSync()
if (this.isReleased()) await this.remove()
}
isReleased () {
return this.refCount === 0
}
}
async function tryStatFile (path) {
return new Promise((resolve, reject) =>
fs.stat(path, (error, result) =>
resolve(error == null && result)
)
)
}
async function copyFile (source, destination, mode) {
return new Promise((resolve, reject) => {
mkdirp(Path.dirname(destination), (error) => {
if (error) return reject(error)
const readStream = fs.createReadStream(source)
readStream
.on('error', reject)
.once('open', () => {
const writeStream = fs.createWriteStream(destination, {mode})
writeStream
.on('error', reject)
.on('open', () => readStream.pipe(writeStream))
.once('close', () => resolve())
})
})
})
}

View File

@@ -4,37 +4,55 @@ if (typeof snapshotResult !== 'undefined') {
const startTime = Date.now()
const electron = require('electron')
const fs = require('fs')
const path = require('path')
const fs = require('fs-plus')
const CSON = require('season')
const yargs = require('yargs')
const electron = require('electron')
const args =
yargs(process.argv)
.alias('d', 'dev')
.alias('t', 'test')
.alias('r', 'resource-path')
.argv
function isAtomRepoPath (repoPath) {
let packageJsonPath = path.join(repoPath, 'package.json')
if (fs.statSyncNoException(packageJsonPath)) {
let packageJson = CSON.readFileSync(packageJsonPath)
return packageJson.name === 'atom'
}
return false
}
let resourcePath
let devResourcePath
if (args.resourcePath) {
resourcePath = args.resourcePath
devResourcePath = resourcePath
} else {
const stableResourcePath = path.dirname(path.dirname(__dirname))
const defaultRepositoryPath = path.join(electron.app.getPath('home'), 'github', 'atom')
if (process.env.ATOM_DEV_RESOURCE_PATH) {
devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH
} else if (isAtomRepoPath(process.cwd())) {
devResourcePath = process.cwd()
} else if (fs.statSyncNoException(defaultRepositoryPath)) {
devResourcePath = defaultRepositoryPath
} else {
devResourcePath = stableResourcePath
}
if (args.dev || args.test || args.benchmark || args.benchmarkTest) {
if (process.env.ATOM_DEV_RESOURCE_PATH) {
resourcePath = process.env.ATOM_DEV_RESOURCE_PATH
} else if (fs.statSyncNoException(defaultRepositoryPath)) {
resourcePath = defaultRepositoryPath
} else {
resourcePath = stableResourcePath
}
resourcePath = devResourcePath
} else {
resourcePath = stableResourcePath
}
}
const start = require(path.join(resourcePath, 'src', 'main-process', 'start'))
start(resourcePath, startTime)
start(resourcePath, devResourcePath, startTime)

View File

@@ -3,8 +3,6 @@
const dedent = require('dedent')
const yargs = require('yargs')
const {app} = require('electron')
const path = require('path')
const fs = require('fs-plus')
module.exports = function parseCommandLine (processArgs) {
const options = yargs(processArgs).wrap(yargs.terminalWidth())
@@ -12,13 +10,18 @@ module.exports = function parseCommandLine (processArgs) {
options.usage(
dedent`Atom Editor v${version}
Usage: atom [options] [path ...]
Usage:
atom [options] [path ...]
atom file[:line[:column]]
One or more paths to files or folders may be specified. If there is an
existing Atom window that contains all of the given folders, the paths
will be opened in that window. Otherwise, they will be opened in a new
window.
A file may be opened at the desired line (and optionally column) by
appending the numbers right after the file name, e.g. \`atom file:5:8\`.
Paths that start with \`atom://\` will be interpreted as URLs.
Environment Variables:
@@ -44,7 +47,7 @@ module.exports = function parseCommandLine (processArgs) {
'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.'
)
options.boolean('benchmark').describe('benchmark', 'Open a new window that runs the specified benchmarks.')
options.boolean('benchmark-test').describe('benchmark--test', 'Run a faster version of the benchmarks in headless mode.')
options.boolean('benchmark-test').describe('benchmark-test', 'Run a faster version of the benchmarks in headless mode.')
options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.')
options.alias('m', 'main-process').boolean('m').describe('m', 'Run the specified specs in the main process.')
options.string('timeout').describe(
@@ -114,8 +117,6 @@ module.exports = function parseCommandLine (processArgs) {
let pathsToOpen = []
let urlsToOpen = []
let devMode = args['dev']
let devResourcePath = process.env.ATOM_DEV_RESOURCE_PATH || path.join(app.getPath('home'), 'github', 'atom')
let resourcePath = null
for (const path of args._) {
if (path.startsWith('atom://')) {
@@ -125,21 +126,8 @@ module.exports = function parseCommandLine (processArgs) {
}
}
if (args['resource-path']) {
if (args.resourcePath || test) {
devMode = true
devResourcePath = args['resource-path']
}
if (test) {
devMode = true
}
if (devMode) {
resourcePath = devResourcePath
}
if (!fs.statSyncNoException(resourcePath)) {
resourcePath = path.dirname(path.dirname(__dirname))
}
if (args['path-environment']) {
@@ -148,12 +136,7 @@ module.exports = function parseCommandLine (processArgs) {
process.env.PATH = args['path-environment']
}
resourcePath = normalizeDriveLetterName(resourcePath)
devResourcePath = normalizeDriveLetterName(devResourcePath)
return {
resourcePath,
devResourcePath,
pathsToOpen,
urlsToOpen,
executedFrom,
@@ -176,11 +159,3 @@ module.exports = function parseCommandLine (processArgs) {
env: process.env
}
}
function normalizeDriveLetterName (filePath) {
if (process.platform === 'win32') {
return filePath.replace(/^([a-z]):/, ([driveLetter]) => driveLetter.toUpperCase() + ':')
} else {
return filePath
}
}

View File

@@ -5,8 +5,11 @@ const temp = require('temp').track()
const parseCommandLine = require('./parse-command-line')
const startCrashReporter = require('../crash-reporter-start')
const atomPaths = require('../atom-paths')
const fs = require('fs')
const CSON = require('season')
const Config = require('../config')
module.exports = function start (resourcePath, startTime) {
module.exports = function start (resourcePath, devResourcePath, startTime) {
global.shellStartTime = startTime
process.on('uncaughtException', function (error = {}) {
@@ -19,16 +22,35 @@ module.exports = function start (resourcePath, startTime) {
}
})
process.on('unhandledRejection', function (error = {}) {
if (error.message != null) {
console.log(error.message)
}
if (error.stack != null) {
console.log(error.stack)
}
})
const previousConsoleLog = console.log
console.log = nslog
app.commandLine.appendSwitch('enable-experimental-web-platform-features')
const args = parseCommandLine(process.argv.slice(1))
args.resourcePath = normalizeDriveLetterName(resourcePath)
args.devResourcePath = normalizeDriveLetterName(devResourcePath)
atomPaths.setAtomHome(app.getPath('home'))
atomPaths.setUserData(app)
setupCompileCache()
const config = getConfig()
const colorProfile = config.get('core.colorProfile')
if (colorProfile && colorProfile !== 'default') {
app.commandLine.appendSwitch('force-color-profile', colorProfile)
}
if (handleStartupEventWithSquirrel()) {
return
} else if (args.test && args.mainProcess) {
@@ -87,3 +109,29 @@ function setupCompileCache () {
CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
CompileCache.install(process.resourcesPath, require)
}
function getConfig () {
const config = new Config()
let configFilePath
if (fs.existsSync(path.join(process.env.ATOM_HOME, 'config.json'))) {
configFilePath = path.join(process.env.ATOM_HOME, 'config.json')
} else if (fs.existsSync(path.join(process.env.ATOM_HOME, 'config.cson'))) {
configFilePath = path.join(process.env.ATOM_HOME, 'config.cson')
}
if (configFilePath) {
const configFileData = CSON.readFileSync(configFilePath)
config.resetUserSettings(configFileData)
}
return config
}
function normalizeDriveLetterName (filePath) {
if (process.platform === 'win32' && filePath) {
return filePath.replace(/^([a-z]):/, ([driveLetter]) => driveLetter.toUpperCase() + ':')
} else {
return filePath
}
}

View File

@@ -1,7 +1,5 @@
'use babel'
import Registry from 'winreg'
import Path from 'path'
const Registry = require('winreg')
const Path = require('path')
let exeName = Path.basename(process.execPath)
let appPath = `\"${process.execPath}\"`

View File

@@ -1,74 +0,0 @@
_ = require 'underscore-plus'
ItemSpecificities = new WeakMap
merge = (menu, item, itemSpecificity=Infinity) ->
item = cloneMenuItem(item)
ItemSpecificities.set(item, itemSpecificity) if itemSpecificity
matchingItemIndex = findMatchingItemIndex(menu, item)
matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1
if matchingItem?
if item.submenu?
merge(matchingItem.submenu, submenuItem, itemSpecificity) for submenuItem in item.submenu
else if itemSpecificity
unless itemSpecificity < ItemSpecificities.get(matchingItem)
menu[matchingItemIndex] = item
else unless item.type is 'separator' and _.last(menu)?.type is 'separator'
menu.push(item)
return
unmerge = (menu, item) ->
matchingItemIndex = findMatchingItemIndex(menu, item)
matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1
if matchingItem?
if item.submenu?
unmerge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu
unless matchingItem.submenu?.length > 0
menu.splice(matchingItemIndex, 1)
findMatchingItemIndex = (menu, {type, label, submenu}) ->
return -1 if type is 'separator'
for item, index in menu
if normalizeLabel(item.label) is normalizeLabel(label) and item.submenu? is submenu?
return index
-1
normalizeLabel = (label) ->
return undefined unless label?
if process.platform is 'darwin'
label
else
label.replace(/\&/g, '')
cloneMenuItem = (item) ->
item = _.pick(item, 'type', 'label', 'enabled', 'visible', 'command', 'submenu', 'commandDetail', 'role', 'accelerator')
if item.submenu?
item.submenu = item.submenu.map (submenuItem) -> cloneMenuItem(submenuItem)
item
# Determine the Electron accelerator for a given Atom keystroke.
#
# keystroke - The keystroke.
#
# Returns a String containing the keystroke in a format that can be interpreted
# by Electron to provide nice icons where available.
acceleratorForKeystroke = (keystroke) ->
return null unless keystroke
modifiers = keystroke.split(/-(?=.)/)
key = modifiers.pop().toUpperCase().replace('+', 'Plus')
modifiers = modifiers.map (modifier) ->
modifier.replace(/shift/ig, "Shift")
.replace(/cmd/ig, "Command")
.replace(/ctrl/ig, "Ctrl")
.replace(/alt/ig, "Alt")
keys = modifiers.concat([key])
keys.join("+")
module.exports = {merge, unmerge, normalizeLabel, cloneMenuItem, acceleratorForKeystroke}

132
src/menu-helpers.js Normal file
View File

@@ -0,0 +1,132 @@
const _ = require('underscore-plus')
const ItemSpecificities = new WeakMap()
// Add an item to a menu, ensuring separators are not duplicated.
function addItemToMenu (item, menu) {
const lastMenuItem = _.last(menu)
const lastMenuItemIsSpearator = lastMenuItem && lastMenuItem.type === 'separator'
if (!(item.type === 'separator' && lastMenuItemIsSpearator)) {
menu.push(item)
}
}
function merge (menu, item, itemSpecificity = Infinity) {
item = cloneMenuItem(item)
ItemSpecificities.set(item, itemSpecificity)
const matchingItemIndex = findMatchingItemIndex(menu, item)
if (matchingItemIndex === -1) {
addItemToMenu(item, menu)
return
}
const matchingItem = menu[matchingItemIndex]
if (item.submenu != null) {
for (let submenuItem of item.submenu) {
merge(matchingItem.submenu, submenuItem, itemSpecificity)
}
} else if (itemSpecificity && itemSpecificity >= ItemSpecificities.get(matchingItem)) {
menu[matchingItemIndex] = item
}
}
function unmerge (menu, item) {
const matchingItemIndex = findMatchingItemIndex(menu, item)
if (matchingItemIndex === -1) {
return
}
const matchingItem = menu[matchingItemIndex]
if (item.submenu != null) {
for (let submenuItem of item.submenu) {
unmerge(matchingItem.submenu, submenuItem)
}
}
if (matchingItem.submenu == null || matchingItem.submenu.length === 0) {
menu.splice(matchingItemIndex, 1)
}
}
function findMatchingItemIndex (menu, { type, label, submenu }) {
if (type === 'separator') {
return -1
}
for (let index = 0; index < menu.length; index++) {
const item = menu[index]
if (
normalizeLabel(item.label) === normalizeLabel(label) &&
(item.submenu != null) === (submenu != null)
) {
return index
}
}
return -1
}
function normalizeLabel (label) {
if (label == null) {
return
}
return process.platform === 'darwin' ? label : label.replace(/&/g, '')
}
function cloneMenuItem (item) {
item = _.pick(
item,
'type',
'label',
'enabled',
'visible',
'command',
'submenu',
'commandDetail',
'role',
'accelerator',
'before',
'after',
'beforeGroupContaining',
'afterGroupContaining'
)
if (item.submenu != null) {
item.submenu = item.submenu.map(submenuItem => cloneMenuItem(submenuItem))
}
return item
}
// Determine the Electron accelerator for a given Atom keystroke.
//
// keystroke - The keystroke.
//
// Returns a String containing the keystroke in a format that can be interpreted
// by Electron to provide nice icons where available.
function acceleratorForKeystroke (keystroke) {
if (!keystroke) {
return null
}
let modifiers = keystroke.split(/-(?=.)/)
const key = modifiers
.pop()
.toUpperCase()
.replace('+', 'Plus')
modifiers = modifiers.map(modifier =>
modifier
.replace(/shift/gi, 'Shift')
.replace(/cmd/gi, 'Command')
.replace(/ctrl/gi, 'Ctrl')
.replace(/alt/gi, 'Alt')
)
const keys = [...modifiers, key]
return keys.join('+')
}
module.exports = {
merge,
unmerge,
normalizeLabel,
cloneMenuItem,
acceleratorForKeystroke
}

View File

@@ -149,9 +149,9 @@ class MenuManager
update: ->
return unless @initialized
clearImmediate(@pendingUpdateOperation) if @pendingUpdateOperation?
clearTimeout(@pendingUpdateOperation) if @pendingUpdateOperation?
@pendingUpdateOperation = setImmediate =>
@pendingUpdateOperation = setTimeout(=>
unsetKeystrokes = new Set
for binding in @keymapManager.getKeyBindings()
if binding.command is 'unset!'
@@ -168,6 +168,7 @@ class MenuManager
keystrokesByCommand[binding.command].unshift binding.keystrokes
@sendToBrowserProcess(@template, keystrokesByCommand)
, 1)
loadPlatformItems: ->
if platformMenu?

186
src/menu-sort-helpers.js Normal file
View File

@@ -0,0 +1,186 @@
// UTILS
function splitArray (arr, predicate) {
let lastArr = []
const multiArr = [lastArr]
arr.forEach(item => {
if (predicate(item)) {
if (lastArr.length > 0) {
lastArr = []
multiArr.push(lastArr)
}
} else {
lastArr.push(item)
}
})
return multiArr
}
function joinArrays (arrays, joiner) {
const joinedArr = []
arrays.forEach((arr, i) => {
if (i > 0 && arr.length > 0) {
joinedArr.push(joiner)
}
joinedArr.push(...arr)
})
return joinedArr
}
const pushOntoMultiMap = (map, key, value) => {
if (!map.has(key)) {
map.set(key, [])
}
map.get(key).push(value)
}
function indexOfGroupContainingCommand (groups, command, ignoreGroup) {
return groups.findIndex(
candiateGroup =>
candiateGroup !== ignoreGroup &&
candiateGroup.some(
candidateItem => candidateItem.command === command
)
)
}
// Sort nodes topologically using a depth-first approach. Encountered cycles
// are broken.
function sortTopologically (originalOrder, edgesById) {
const sorted = []
const marked = new Set()
function visit (id) {
if (marked.has(id)) {
// Either this node has already been placed, or we have encountered a
// cycle and need to exit.
return
}
marked.add(id)
const edges = edgesById.get(id)
if (edges != null) {
edges.forEach(visit)
}
sorted.push(id)
}
originalOrder.forEach(visit)
return sorted
}
function attemptToMergeAGroup (groups) {
for (let i = 0; i < groups.length; i++) {
const group = groups[i]
for (const item of group) {
const toCommands = [...(item.before || []), ...(item.after || [])]
for (const command of toCommands) {
const index = indexOfGroupContainingCommand(groups, command, group)
if (index === -1) {
// No valid edge for this command
continue
}
const mergeTarget = groups[index]
// Merge with group containing `command`
mergeTarget.push(...group)
groups.splice(i, 1)
return true
}
}
}
return false
}
// Merge groups based on before/after positions
// Mutates both the array of groups, and the individual group arrays.
function mergeGroups (groups) {
let mergedAGroup = true
while (mergedAGroup) {
mergedAGroup = attemptToMergeAGroup(groups)
}
return groups
}
function sortItemsInGroup (group) {
const originalOrder = group.map((node, i) => i)
const edges = new Map()
const commandToIndex = new Map(group.map((item, i) => [item.command, i]))
group.forEach((item, i) => {
if (item.before) {
item.before.forEach(toCommand => {
const to = commandToIndex.get(toCommand)
if (to != null) {
pushOntoMultiMap(edges, to, i)
}
})
}
if (item.after) {
item.after.forEach(toCommand => {
const to = commandToIndex.get(toCommand)
if (to != null) {
pushOntoMultiMap(edges, i, to)
}
})
}
})
const sortedNodes = sortTopologically(originalOrder, edges)
return sortedNodes.map(i => group[i])
}
function findEdgesInGroup (groups, i, edges) {
const group = groups[i]
for (const item of group) {
if (item.beforeGroupContaining) {
for (const command of item.beforeGroupContaining) {
const to = indexOfGroupContainingCommand(groups, command, group)
if (to !== -1) {
pushOntoMultiMap(edges, to, i)
return
}
}
}
if (item.afterGroupContaining) {
for (const command of item.afterGroupContaining) {
const to = indexOfGroupContainingCommand(groups, command, group)
if (to !== -1) {
pushOntoMultiMap(edges, i, to)
return
}
}
}
}
}
function sortGroups (groups) {
const originalOrder = groups.map((item, i) => i)
const edges = new Map()
for (let i = 0; i < groups.length; i++) {
findEdgesInGroup(groups, i, edges)
}
const sortedGroupIndexes = sortTopologically(originalOrder, edges)
return sortedGroupIndexes.map(i => groups[i])
}
function isSeparator (item) {
return item.type === 'separator'
}
function sortMenuItems (menuItems) {
// Split the items into their implicit groups based upon separators.
const groups = splitArray(menuItems, isSeparator)
// Merge groups that contain before/after references to eachother.
const mergedGroups = mergeGroups(groups)
// Sort each individual group internally.
const mergedGroupsWithSortedItems = mergedGroups.map(sortItemsInGroup)
// Sort the groups based upon their beforeGroupContaining/afterGroupContaining
// references.
const sortedGroups = sortGroups(mergedGroupsWithSortedItems)
// Join the groups back
return joinArrays(sortedGroups, { type: 'separator' })
}
module.exports = {sortMenuItems}

View File

@@ -189,7 +189,7 @@ resolveModulePath = (relativePath, parentModule) ->
return unless candidates?
for version, resolvedPath of candidates
if Module._cache.hasOwnProperty(resolvedPath) or isCorePath(resolvedPath)
if Module._cache[resolvedPath] or isCorePath(resolvedPath)
return resolvedPath if satisfies(version, range)
return

View File

@@ -1,5 +1,3 @@
/** @babel */
const path = require('path')
// Private: re-join the segments split from an absolute path to form another absolute path.

View File

@@ -21,7 +21,7 @@ class Notification {
throw new Error(`Notification must be created with string message: ${this.message}`)
}
if (!_.isObject(this.options) || _.isArray(this.options)) {
if (!_.isObject(this.options) || Array.isArray(this.options)) {
throw new Error(`Notification must be created with an options object: ${this.options}`)
}
}

View File

@@ -1,8 +1,6 @@
/** @babel */
const {Disposable} = require('event-kit')
import {Disposable} from 'event-kit'
export default {
module.exports = {
name: 'Null Grammar',
scopeName: 'text.plain.null-grammar',
scopeForId (id) {

View File

@@ -61,6 +61,7 @@ module.exports = class PackageManager {
if (params.configDirPath != null && !params.safeMode) {
if (this.devMode) {
this.packageDirPaths.push(path.join(params.configDirPath, 'dev', 'packages'))
this.packageDirPaths.push(path.join(this.resourcePath, 'packages'))
}
this.packageDirPaths.push(path.join(params.configDirPath, 'packages'))
}

View File

@@ -43,8 +43,8 @@ class Package {
? params.bundledPackage
: this.packageManager.isBundledPackagePath(this.path)
this.name =
params.name ||
(this.metadata && this.metadata.name) ||
params.name ||
path.basename(this.path)
this.reset()
}

View File

@@ -1,28 +0,0 @@
{CompositeDisposable} = require 'event-kit'
_ = require 'underscore-plus'
module.exports =
class PaneContainerElement extends HTMLElement
createdCallback: ->
@subscriptions = new CompositeDisposable
@classList.add 'panes'
initialize: (@model, {@views}) ->
throw new Error("Must pass a views parameter when initializing PaneContainerElements") unless @views?
@subscriptions.add @model.observeRoot(@rootChanged.bind(this))
this
rootChanged: (root) ->
focusedElement = document.activeElement if @hasFocus()
@firstChild?.remove()
if root?
view = @views.getView(root)
@appendChild(view)
focusedElement?.focus()
hasFocus: ->
this is document.activeElement or @contains(document.activeElement)
module.exports = PaneContainerElement = document.registerElement 'atom-pane-container', prototype: PaneContainerElement.prototype

View File

@@ -0,0 +1,40 @@
const {CompositeDisposable} = require('event-kit')
class PaneContainerElement extends HTMLElement {
createdCallback () {
this.subscriptions = new CompositeDisposable()
this.classList.add('panes')
}
initialize (model, {views}) {
this.model = model
this.views = views
if (this.views == null) {
throw new Error('Must pass a views parameter when initializing PaneContainerElements')
}
this.subscriptions.add(this.model.observeRoot(this.rootChanged.bind(this)))
return this
}
rootChanged (root) {
const focusedElement = this.hasFocus() ? document.activeElement : null
if (this.firstChild != null) {
this.firstChild.remove()
}
if (root != null) {
const view = this.views.getView(root)
this.appendChild(view)
if (focusedElement != null) {
focusedElement.focus()
}
}
}
hasFocus () {
return this === document.activeElement || this.contains(document.activeElement)
}
}
module.exports = document.registerElement('atom-pane-container', {
prototype: PaneContainerElement.prototype
})

View File

@@ -51,6 +51,7 @@ class PaneContainer {
deserialize (state, deserializerManager) {
if (state.version !== SERIALIZATION_VERSION) return
this.itemRegistry = new ItemRegistry()
this.setRoot(deserializerManager.deserialize(state.root))
this.activePane = find(this.getRoot().getPanes(), pane => pane.id === state.activePaneId) || this.getPanes()[0]
if (this.config.get('core.destroyEmptyPanes')) this.destroyEmptyPanes()

View File

@@ -1,139 +0,0 @@
path = require 'path'
{CompositeDisposable} = require 'event-kit'
class PaneElement extends HTMLElement
attached: false
createdCallback: ->
@attached = false
@subscriptions = new CompositeDisposable
@inlineDisplayStyles = new WeakMap
@initializeContent()
@subscribeToDOMEvents()
attachedCallback: ->
@attached = true
@focus() if @model.isFocused()
detachedCallback: ->
@attached = false
initializeContent: ->
@setAttribute 'class', 'pane'
@setAttribute 'tabindex', -1
@appendChild @itemViews = document.createElement('div')
@itemViews.setAttribute 'class', 'item-views'
subscribeToDOMEvents: ->
handleFocus = (event) =>
@model.focus() unless @isActivating or @model.isDestroyed() or @contains(event.relatedTarget)
if event.target is this and view = @getActiveView()
view.focus()
event.stopPropagation()
handleBlur = (event) =>
@model.blur() unless @contains(event.relatedTarget)
handleDragOver = (event) ->
event.preventDefault()
event.stopPropagation()
handleDrop = (event) =>
event.preventDefault()
event.stopPropagation()
@getModel().activate()
pathsToOpen = Array::map.call event.dataTransfer.files, (file) -> file.path
@applicationDelegate.open({pathsToOpen}) if pathsToOpen.length > 0
@addEventListener 'focus', handleFocus, true
@addEventListener 'blur', handleBlur, true
@addEventListener 'dragover', handleDragOver
@addEventListener 'drop', handleDrop
initialize: (@model, {@views, @applicationDelegate}) ->
throw new Error("Must pass a views parameter when initializing PaneElements") unless @views?
throw new Error("Must pass an applicationDelegate parameter when initializing PaneElements") unless @applicationDelegate?
@subscriptions.add @model.onDidActivate(@activated.bind(this))
@subscriptions.add @model.observeActive(@activeStatusChanged.bind(this))
@subscriptions.add @model.observeActiveItem(@activeItemChanged.bind(this))
@subscriptions.add @model.onDidRemoveItem(@itemRemoved.bind(this))
@subscriptions.add @model.onDidDestroy(@paneDestroyed.bind(this))
@subscriptions.add @model.observeFlexScale(@flexScaleChanged.bind(this))
this
getModel: -> @model
activated: ->
@isActivating = true
@focus() unless @hasFocus() # Don't steal focus from children.
@isActivating = false
activeStatusChanged: (active) ->
if active
@classList.add('active')
else
@classList.remove('active')
activeItemChanged: (item) ->
delete @dataset.activeItemName
delete @dataset.activeItemPath
@changePathDisposable?.dispose()
return unless item?
hasFocus = @hasFocus()
itemView = @views.getView(item)
if itemPath = item.getPath?()
@dataset.activeItemName = path.basename(itemPath)
@dataset.activeItemPath = itemPath
if item.onDidChangePath?
@changePathDisposable = item.onDidChangePath =>
itemPath = item.getPath()
@dataset.activeItemName = path.basename(itemPath)
@dataset.activeItemPath = itemPath
unless @itemViews.contains(itemView)
@itemViews.appendChild(itemView)
for child in @itemViews.children
if child is itemView
@showItemView(child) if @attached
else
@hideItemView(child)
itemView.focus() if hasFocus
showItemView: (itemView) ->
inlineDisplayStyle = @inlineDisplayStyles.get(itemView)
if inlineDisplayStyle?
itemView.style.display = inlineDisplayStyle
else
itemView.style.display = ''
hideItemView: (itemView) ->
inlineDisplayStyle = itemView.style.display
unless inlineDisplayStyle is 'none'
@inlineDisplayStyles.set(itemView, inlineDisplayStyle) if inlineDisplayStyle?
itemView.style.display = 'none'
itemRemoved: ({item, index, destroyed}) ->
if viewToRemove = @views.getView(item)
viewToRemove.remove()
paneDestroyed: ->
@subscriptions.dispose()
@changePathDisposable?.dispose()
flexScaleChanged: (flexScale) ->
@style.flexGrow = flexScale
getActiveView: -> @views.getView(@model.getActiveItem())
hasFocus: ->
this is document.activeElement or @contains(document.activeElement)
module.exports = PaneElement = document.registerElement 'atom-pane', prototype: PaneElement.prototype

218
src/pane-element.js Normal file
View File

@@ -0,0 +1,218 @@
const path = require('path')
const {CompositeDisposable} = require('event-kit')
class PaneElement extends HTMLElement {
createdCallback () {
this.attached = false
this.subscriptions = new CompositeDisposable()
this.inlineDisplayStyles = new WeakMap()
this.initializeContent()
this.subscribeToDOMEvents()
}
attachedCallback () {
this.attached = true
if (this.model.isFocused()) {
this.focus()
}
}
detachedCallback () {
this.attached = false
}
initializeContent () {
this.setAttribute('class', 'pane')
this.setAttribute('tabindex', -1)
this.itemViews = document.createElement('div')
this.appendChild(this.itemViews)
this.itemViews.setAttribute('class', 'item-views')
}
subscribeToDOMEvents () {
const handleFocus = event => {
if (
!(
this.isActivating ||
this.model.isDestroyed() ||
this.contains(event.relatedTarget)
)
) {
this.model.focus()
}
if (event.target !== this) return
const view = this.getActiveView()
if (view) {
view.focus()
event.stopPropagation()
}
}
const handleBlur = event => {
if (!this.contains(event.relatedTarget)) {
this.model.blur()
}
}
const handleDragOver = event => {
event.preventDefault()
event.stopPropagation()
}
const handleDrop = event => {
event.preventDefault()
event.stopPropagation()
this.getModel().activate()
const pathsToOpen = [...event.dataTransfer.files].map(file => file.path)
if (pathsToOpen.length > 0) {
this.applicationDelegate.open({pathsToOpen})
}
}
this.addEventListener('focus', handleFocus, true)
this.addEventListener('blur', handleBlur, true)
this.addEventListener('dragover', handleDragOver)
this.addEventListener('drop', handleDrop)
}
initialize (model, {views, applicationDelegate}) {
this.model = model
this.views = views
this.applicationDelegate = applicationDelegate
if (this.views == null) {
throw new Error(
'Must pass a views parameter when initializing PaneElements'
)
}
if (this.applicationDelegate == null) {
throw new Error(
'Must pass an applicationDelegate parameter when initializing PaneElements'
)
}
this.subscriptions.add(this.model.onDidActivate(this.activated.bind(this)))
this.subscriptions.add(
this.model.observeActive(this.activeStatusChanged.bind(this))
)
this.subscriptions.add(
this.model.observeActiveItem(this.activeItemChanged.bind(this))
)
this.subscriptions.add(
this.model.onDidRemoveItem(this.itemRemoved.bind(this))
)
this.subscriptions.add(
this.model.onDidDestroy(this.paneDestroyed.bind(this))
)
this.subscriptions.add(
this.model.observeFlexScale(this.flexScaleChanged.bind(this))
)
return this
}
getModel () {
return this.model
}
activated () {
this.isActivating = true
if (!this.hasFocus()) {
// Don't steal focus from children.
this.focus()
}
this.isActivating = false
}
activeStatusChanged (active) {
if (active) {
this.classList.add('active')
} else {
this.classList.remove('active')
}
}
activeItemChanged (item) {
delete this.dataset.activeItemName
delete this.dataset.activeItemPath
if (this.changePathDisposable != null) {
this.changePathDisposable.dispose()
}
if (item == null) {
return
}
const hasFocus = this.hasFocus()
const itemView = this.views.getView(item)
const itemPath = typeof item.getPath === 'function' ? item.getPath() : null
if (itemPath) {
this.dataset.activeItemName = path.basename(itemPath)
this.dataset.activeItemPath = itemPath
if (item.onDidChangePath != null) {
this.changePathDisposable = item.onDidChangePath(() => {
const itemPath = item.getPath()
this.dataset.activeItemName = path.basename(itemPath)
this.dataset.activeItemPath = itemPath
})
}
}
if (!this.itemViews.contains(itemView)) {
this.itemViews.appendChild(itemView)
}
for (const child of this.itemViews.children) {
if (child === itemView) {
if (this.attached) {
this.showItemView(child)
}
} else {
this.hideItemView(child)
}
}
if (hasFocus) {
itemView.focus()
}
}
showItemView (itemView) {
const inlineDisplayStyle = this.inlineDisplayStyles.get(itemView)
if (inlineDisplayStyle != null) {
itemView.style.display = inlineDisplayStyle
} else {
itemView.style.display = ''
}
}
hideItemView (itemView) {
const inlineDisplayStyle = itemView.style.display
if (inlineDisplayStyle !== 'none') {
if (inlineDisplayStyle != null) {
this.inlineDisplayStyles.set(itemView, inlineDisplayStyle)
}
itemView.style.display = 'none'
}
}
itemRemoved ({item, index, destroyed}) {
const viewToRemove = this.views.getView(item)
if (viewToRemove) {
viewToRemove.remove()
}
}
paneDestroyed () {
this.subscriptions.dispose()
if (this.changePathDisposable != null) {
this.changePathDisposable.dispose()
}
}
flexScaleChanged (flexScale) {
this.style.flexGrow = flexScale
}
getActiveView () {
return this.views.getView(this.model.getActiveItem())
}
hasFocus () {
return (
this === document.activeElement || this.contains(document.activeElement)
)
}
}
module.exports = document.registerElement('atom-pane', {
prototype: PaneElement.prototype
})

View File

@@ -155,9 +155,17 @@ class Pane {
getFlexScale () { return this.flexScale }
increaseSize () { this.setFlexScale(this.getFlexScale() * 1.1) }
increaseSize () {
if (this.getContainer().getPanes().length > 1) {
this.setFlexScale(this.getFlexScale() * 1.1)
}
}
decreaseSize () { this.setFlexScale(this.getFlexScale() / 1.1) }
decreaseSize () {
if (this.getContainer().getPanes().length > 1) {
this.setFlexScale(this.getFlexScale() / 1.1)
}
}
/*
Section: Event Subscription
@@ -606,15 +614,15 @@ class Pane {
if (this.items.includes(item)) return
const itemSubscriptions = new CompositeDisposable()
this.subscriptionsPerItem.set(item, itemSubscriptions)
if (typeof item.onDidDestroy === 'function') {
const itemSubscriptions = new CompositeDisposable()
itemSubscriptions.add(item.onDidDestroy(() => this.removeItem(item, false)))
if (typeof item.onDidTerminatePendingState === 'function') {
itemSubscriptions.add(item.onDidTerminatePendingState(() => {
if (this.getPendingItem() === item) this.clearPendingItem()
}))
}
this.subscriptionsPerItem.set(item, itemSubscriptions)
}
if (typeof item.onDidTerminatePendingState === 'function') {
itemSubscriptions.add(item.onDidTerminatePendingState(() => {
if (this.getPendingItem() === item) this.clearPendingItem()
}))
}
this.items.splice(index, 0, item)

View File

@@ -1,10 +1,9 @@
/** @babel */
const fs = require('fs')
const path = require('path')
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
const nsfw = require('@atom/nsfw')
const watcher = require('@atom/watcher')
const {NativeWatcherRegistry} = require('./native-watcher-registry')
// Private: Associate native watcher action flags with descriptive String equivalents.
@@ -23,145 +22,7 @@ const WATCHER_STATE = {
STOPPING: Symbol('stopping')
}
// Private: Emulate a "filesystem watcher" by subscribing to Atom events like buffers being saved. This will miss
// any changes made to files outside of Atom, but it also has no overhead.
class AtomBackend {
async start (rootPath, eventCallback, errorCallback) {
const getRealPath = givenPath => {
return new Promise(resolve => {
fs.realpath(givenPath, (err, resolvedPath) => {
err ? resolve(null) : resolve(resolvedPath)
})
})
}
this.subs = new CompositeDisposable()
this.subs.add(atom.workspace.observeTextEditors(async editor => {
let realPath = await getRealPath(editor.getPath())
if (!realPath || !realPath.startsWith(rootPath)) {
return
}
const announce = (action, oldPath) => {
const payload = {action, path: realPath}
if (oldPath) payload.oldPath = oldPath
eventCallback([payload])
}
const buffer = editor.getBuffer()
this.subs.add(buffer.onDidConflict(() => announce('modified')))
this.subs.add(buffer.onDidReload(() => announce('modified')))
this.subs.add(buffer.onDidSave(event => {
if (event.path === realPath) {
announce('modified')
} else {
const oldPath = realPath
realPath = event.path
announce('renamed', oldPath)
}
}))
this.subs.add(buffer.onDidDelete(() => announce('deleted')))
this.subs.add(buffer.onDidChangePath(newPath => {
if (newPath !== realPath) {
const oldPath = realPath
realPath = newPath
announce('renamed', oldPath)
}
}))
}))
// Giant-ass brittle hack to hook files (and eventually directories) created from the TreeView.
const treeViewPackage = await atom.packages.getLoadedPackage('tree-view')
if (!treeViewPackage) return
await treeViewPackage.activationPromise
const treeViewModule = treeViewPackage.mainModule
if (!treeViewModule) return
const treeView = treeViewModule.getTreeViewInstance()
const isOpenInEditor = async eventPath => {
const openPaths = await Promise.all(
atom.workspace.getTextEditors().map(editor => getRealPath(editor.getPath()))
)
return openPaths.includes(eventPath)
}
this.subs.add(treeView.onFileCreated(async event => {
const realPath = await getRealPath(event.path)
if (!realPath) return
eventCallback([{action: 'added', path: realPath}])
}))
this.subs.add(treeView.onEntryDeleted(async event => {
const realPath = await getRealPath(event.path)
if (!realPath || isOpenInEditor(realPath)) return
eventCallback([{action: 'deleted', path: realPath}])
}))
this.subs.add(treeView.onEntryMoved(async event => {
const [realNewPath, realOldPath] = await Promise.all([
getRealPath(event.newPath),
getRealPath(event.initialPath)
])
if (!realNewPath || !realOldPath || isOpenInEditor(realNewPath) || isOpenInEditor(realOldPath)) return
eventCallback([{action: 'renamed', path: realNewPath, oldPath: realOldPath}])
}))
}
async stop () {
this.subs && this.subs.dispose()
}
}
// Private: Implement a native watcher by translating events from an NSFW watcher.
class NSFWBackend {
async start (rootPath, eventCallback, errorCallback) {
const handler = events => {
eventCallback(events.map(event => {
const action = ACTION_MAP.get(event.action) || `unexpected (${event.action})`
const payload = {action}
if (event.file) {
payload.path = path.join(event.directory, event.file)
} else {
payload.oldPath = path.join(event.directory, event.oldFile)
payload.path = path.join(event.directory, event.newFile)
}
return payload
}))
}
this.watcher = await nsfw(
rootPath,
handler,
{debounceMS: 100, errorCallback}
)
await this.watcher.start()
}
stop () {
return this.watcher.stop()
}
}
// Private: Map configuration settings from the feature flag to backend implementations.
const BACKENDS = {
atom: AtomBackend,
native: NSFWBackend
}
// Private: the backend implementation to fall back to if the config setting is invalid.
const DEFAULT_BACKEND = BACKENDS.nsfw
// Private: Interface with and normalize events from a native OS filesystem watcher.
// Private: Interface with and normalize events from a filesystem watcher implementation.
class NativeWatcher {
// Private: Initialize a native watcher on a path.
@@ -172,37 +33,10 @@ class NativeWatcher {
this.emitter = new Emitter()
this.subs = new CompositeDisposable()
this.backend = null
this.state = WATCHER_STATE.STOPPED
this.onEvents = this.onEvents.bind(this)
this.onError = this.onError.bind(this)
this.subs.add(atom.config.onDidChange('core.fileSystemWatcher', async () => {
if (this.state === WATCHER_STATE.STARTING) {
// Wait for this watcher to finish starting.
await new Promise(resolve => {
const sub = this.onDidStart(() => {
sub.dispose()
resolve()
})
})
}
// Re-read the config setting in case it's changed again while we were waiting for the watcher
// to start.
const Backend = this.getCurrentBackend()
if (this.state === WATCHER_STATE.RUNNING && !(this.backend instanceof Backend)) {
await this.stop()
await this.start()
}
}))
}
// Private: Read the `core.fileSystemWatcher` setting to determine the filesystem backend to use.
getCurrentBackend () {
const setting = atom.config.get('core.fileSystemWatcher')
return BACKENDS[setting] || DEFAULT_BACKEND
}
// Private: Begin watching for filesystem events.
@@ -214,15 +48,16 @@ class NativeWatcher {
}
this.state = WATCHER_STATE.STARTING
const Backend = this.getCurrentBackend()
this.backend = new Backend()
await this.backend.start(this.normalizedPath, this.onEvents, this.onError)
await this.doStart()
this.state = WATCHER_STATE.RUNNING
this.emitter.emit('did-start')
}
doStart () {
return Promise.reject('doStart() not overridden')
}
// Private: Return true if the underlying watcher is actively listening for filesystem events.
isRunning () {
return this.state === WATCHER_STATE.RUNNING
@@ -285,8 +120,8 @@ class NativeWatcher {
//
// * `replacement` the new {NativeWatcher} instance that a live {Watcher} instance should reattach to instead.
// * `watchedPath` absolute path watched by the new {NativeWatcher}.
reattachTo (replacement, watchedPath) {
this.emitter.emit('should-detach', {replacement, watchedPath})
reattachTo (replacement, watchedPath, options) {
this.emitter.emit('should-detach', {replacement, watchedPath, options})
}
// Private: Stop the native watcher and release any operating system resources associated with it.
@@ -299,12 +134,17 @@ class NativeWatcher {
this.state = WATCHER_STATE.STOPPING
this.emitter.emit('will-stop')
await this.backend.stop()
await this.doStop()
this.state = WATCHER_STATE.STOPPED
this.emitter.emit('did-stop')
}
doStop () {
return Promise.resolve()
}
// Private: Detach any event subscribers.
dispose () {
this.emitter.dispose()
@@ -326,6 +166,133 @@ class NativeWatcher {
}
}
// Private: Emulate a "filesystem watcher" by subscribing to Atom events like buffers being saved. This will miss
// any changes made to files outside of Atom, but it also has no overhead.
class AtomNativeWatcher extends NativeWatcher {
async doStart () {
const getRealPath = givenPath => {
if (!givenPath) {
return Promise.resolve(null)
}
return new Promise(resolve => {
fs.realpath(givenPath, (err, resolvedPath) => {
err ? resolve(null) : resolve(resolvedPath)
})
})
}
this.subs.add(atom.workspace.observeTextEditors(async editor => {
let realPath = await getRealPath(editor.getPath())
if (!realPath || !realPath.startsWith(this.normalizedPath)) {
return
}
const announce = (action, oldPath) => {
const payload = {action, path: realPath}
if (oldPath) payload.oldPath = oldPath
this.onEvents([payload])
}
const buffer = editor.getBuffer()
this.subs.add(buffer.onDidConflict(() => announce('modified')))
this.subs.add(buffer.onDidReload(() => announce('modified')))
this.subs.add(buffer.onDidSave(event => {
if (event.path === realPath) {
announce('modified')
} else {
const oldPath = realPath
realPath = event.path
announce('renamed', oldPath)
}
}))
this.subs.add(buffer.onDidDelete(() => announce('deleted')))
this.subs.add(buffer.onDidChangePath(newPath => {
if (newPath !== this.normalizedPath) {
const oldPath = this.normalizedPath
this.normalizedPath = newPath
announce('renamed', oldPath)
}
}))
}))
// Giant-ass brittle hack to hook files (and eventually directories) created from the TreeView.
const treeViewPackage = await atom.packages.getLoadedPackage('tree-view')
if (!treeViewPackage) return
await treeViewPackage.activationPromise
const treeViewModule = treeViewPackage.mainModule
if (!treeViewModule) return
const treeView = treeViewModule.getTreeViewInstance()
const isOpenInEditor = async eventPath => {
const openPaths = await Promise.all(
atom.workspace.getTextEditors().map(editor => getRealPath(editor.getPath()))
)
return openPaths.includes(eventPath)
}
this.subs.add(treeView.onFileCreated(async event => {
const realPath = await getRealPath(event.path)
if (!realPath) return
this.onEvents([{action: 'added', path: realPath}])
}))
this.subs.add(treeView.onEntryDeleted(async event => {
const realPath = await getRealPath(event.path)
if (!realPath || await isOpenInEditor(realPath)) return
this.onEvents([{action: 'deleted', path: realPath}])
}))
this.subs.add(treeView.onEntryMoved(async event => {
const [realNewPath, realOldPath] = await Promise.all([
getRealPath(event.newPath),
getRealPath(event.initialPath)
])
if (!realNewPath || !realOldPath || await isOpenInEditor(realNewPath) || await isOpenInEditor(realOldPath)) return
this.onEvents([{action: 'renamed', path: realNewPath, oldPath: realOldPath}])
}))
}
}
// Private: Implement a native watcher by translating events from an NSFW watcher.
class NSFWNativeWatcher extends NativeWatcher {
async doStart (rootPath, eventCallback, errorCallback) {
const handler = events => {
this.onEvents(events.map(event => {
const action = ACTION_MAP.get(event.action) || `unexpected (${event.action})`
const payload = {action}
if (event.file) {
payload.path = path.join(event.directory, event.file)
} else {
payload.oldPath = path.join(event.directory, event.oldFile)
payload.path = path.join(event.directory, event.newFile)
}
return payload
}))
}
this.watcher = await nsfw(
this.normalizedPath,
handler,
{debounceMS: 100, errorCallback: this.onError}
)
await this.watcher.start()
}
doStop () {
return this.watcher.stop()
}
}
// Extended: Manage a subscription to filesystem events that occur beneath a root directory. Construct these by
// calling `watchPath`. To watch for events within active project directories, use {Project::onDidChangeFiles}
// instead.
@@ -386,6 +353,15 @@ class PathWatcher {
this.native = null
this.changeCallbacks = new Map()
this.attachedPromise = new Promise(resolve => {
this.resolveAttachedPromise = resolve
})
this.startPromise = new Promise((resolve, reject) => {
this.resolveStartPromise = resolve
this.rejectStartPromise = reject
})
this.normalizedPathPromise = new Promise((resolve, reject) => {
fs.realpath(watchedPath, (err, real) => {
if (err) {
@@ -397,13 +373,7 @@ class PathWatcher {
resolve(real)
})
})
this.attachedPromise = new Promise(resolve => {
this.resolveAttachedPromise = resolve
})
this.startPromise = new Promise(resolve => {
this.resolveStartPromise = resolve
})
this.normalizedPathPromise.catch(err => this.rejectStartPromise(err))
this.emitter = new Emitter()
this.subs = new CompositeDisposable()
@@ -526,7 +496,29 @@ class PathWatcher {
// events may include events for paths above this watcher's root path, so filter them to only include the relevant
// ones, then re-broadcast them to our subscribers.
onNativeEvents (events, callback) {
const filtered = events.filter(event => event.path.startsWith(this.normalizedPath))
const isWatchedPath = eventPath => eventPath.startsWith(this.normalizedPath)
const filtered = []
for (let i = 0; i < events.length; i++) {
const event = events[i]
if (event.action === 'renamed') {
const srcWatched = isWatchedPath(event.oldPath)
const destWatched = isWatchedPath(event.path)
if (srcWatched && destWatched) {
filtered.push(event)
} else if (srcWatched && !destWatched) {
filtered.push({action: 'deleted', kind: event.kind, path: event.oldPath})
} else if (!srcWatched && destWatched) {
filtered.push({action: 'created', kind: event.kind, path: event.path})
}
} else {
if (isWatchedPath(event.path)) {
filtered.push(event)
}
}
}
if (filtered.length > 0) {
callback(filtered)
@@ -545,46 +537,139 @@ class PathWatcher {
}
}
// Private: Globally tracked state used to de-duplicate related [PathWatchers]{PathWatcher}.
// Private: Globally tracked state used to de-duplicate related [PathWatchers]{PathWatcher} backed by emulated Atom
// events or NSFW.
class PathWatcherManager {
// Private: Access or lazily initialize the singleton manager instance.
//
// Returns the one and only {PathWatcherManager}.
static instance () {
if (!PathWatcherManager.theManager) {
PathWatcherManager.theManager = new PathWatcherManager()
// Private: Access the currently active manager instance, creating one if necessary.
static active () {
if (!this.activeManager) {
this.activeManager = new PathWatcherManager(atom.config.get('core.fileSystemWatcher'))
this.sub = atom.config.onDidChange('core.fileSystemWatcher', ({newValue}) => { this.transitionTo(newValue) })
}
return PathWatcherManager.theManager
return this.activeManager
}
// Private: Replace the active {PathWatcherManager} with a new one that creates [NativeWatchers]{NativeWatcher}
// based on the value of `setting`.
static async transitionTo (setting) {
const current = this.active()
if (this.transitionPromise) {
await this.transitionPromise
}
if (current.setting === setting) {
return
}
current.isShuttingDown = true
let resolveTransitionPromise = () => {}
this.transitionPromise = new Promise(resolve => {
resolveTransitionPromise = resolve
})
const replacement = new PathWatcherManager(setting)
this.activeManager = replacement
await Promise.all(
Array.from(current.live, async ([root, native]) => {
const w = await replacement.createWatcher(root, {}, () => {})
native.reattachTo(w.native, root, w.native.options || {})
})
)
current.stopAllWatchers()
resolveTransitionPromise()
this.transitionPromise = null
}
// Private: Initialize global {PathWatcher} state.
constructor () {
this.live = new Set()
this.nativeRegistry = new NativeWatcherRegistry(
normalizedPath => {
const nativeWatcher = new NativeWatcher(normalizedPath)
constructor (setting) {
this.setting = setting
this.live = new Map()
this.live.add(nativeWatcher)
const sub = nativeWatcher.onWillStop(() => {
this.live.delete(nativeWatcher)
sub.dispose()
})
const initLocal = NativeConstructor => {
this.nativeRegistry = new NativeWatcherRegistry(
normalizedPath => {
const nativeWatcher = new NativeConstructor(normalizedPath)
return nativeWatcher
}
)
this.live.set(normalizedPath, nativeWatcher)
const sub = nativeWatcher.onWillStop(() => {
this.live.delete(normalizedPath)
sub.dispose()
})
return nativeWatcher
}
)
}
if (setting === 'atom') {
initLocal(AtomNativeWatcher)
} else if (setting === 'experimental') {
//
} else if (setting === 'poll') {
//
} else {
initLocal(NSFWNativeWatcher)
}
this.isShuttingDown = false
}
useExperimentalWatcher () {
return this.setting === 'experimental' || this.setting === 'poll'
}
// Private: Create a {PathWatcher} tied to this global state. See {watchPath} for detailed arguments.
createWatcher (rootPath, options, eventCallback) {
const watcher = new PathWatcher(this.nativeRegistry, rootPath, options)
watcher.onDidChange(eventCallback)
return watcher
async createWatcher (rootPath, options, eventCallback) {
if (this.isShuttingDown) {
await this.constructor.transitionPromise
return PathWatcherManager.active().createWatcher(rootPath, options, eventCallback)
}
if (this.useExperimentalWatcher()) {
if (this.setting === 'poll') {
options.poll = true
}
const w = await watcher.watchPath(rootPath, options, eventCallback)
this.live.set(rootPath, w.native)
return w
}
const w = new PathWatcher(this.nativeRegistry, rootPath, options)
w.onDidChange(eventCallback)
await w.getStartPromise()
return w
}
// Private: Directly access the {NativeWatcherRegistry}.
getRegistry () {
if (this.useExperimentalWatcher()) {
return watcher.getRegistry()
}
return this.nativeRegistry
}
// Private: Sample watcher usage statistics. Only available for experimental watchers.
status () {
if (this.useExperimentalWatcher()) {
return watcher.status()
}
return {}
}
// Private: Return a {String} depicting the currently active native watchers.
print () {
if (this.useExperimentalWatcher()) {
return watcher.printWatchers()
}
return this.nativeRegistry.print()
}
@@ -592,8 +677,12 @@ class PathWatcherManager {
//
// Returns a {Promise} that resolves when all native watcher resources are disposed.
stopAllWatchers () {
if (this.useExperimentalWatcher()) {
return watcher.stopAllWatchers()
}
return Promise.all(
Array.from(this.live, watcher => watcher.stop())
Array.from(this.live, ([, w]) => w.stop())
)
}
}
@@ -638,19 +727,33 @@ class PathWatcherManager {
// ```
//
function watchPath (rootPath, options, eventCallback) {
const watcher = PathWatcherManager.instance().createWatcher(rootPath, options, eventCallback)
return watcher.getStartPromise().then(() => watcher)
return PathWatcherManager.active().createWatcher(rootPath, options, eventCallback)
}
// Private: Return a Promise that resolves when all {NativeWatcher} instances associated with a FileSystemManager
// have stopped listening. This is useful for `afterEach()` blocks in unit tests.
function stopAllWatchers () {
return PathWatcherManager.instance().stopAllWatchers()
return PathWatcherManager.active().stopAllWatchers()
}
// Private: Show the currently active native watchers.
function printWatchers () {
return PathWatcherManager.instance().print()
// Private: Show the currently active native watchers in a formatted {String}.
watchPath.printWatchers = function () {
return PathWatcherManager.active().print()
}
module.exports = {watchPath, stopAllWatchers, printWatchers}
// Private: Access the active {NativeWatcherRegistry}.
watchPath.getRegistry = function () {
return PathWatcherManager.active().getRegistry()
}
// Private: Sample usage statistics for the active watcher.
watchPath.status = function () {
return PathWatcherManager.active().status()
}
// Private: Configure @atom/watcher ("experimental") directly.
watchPath.configure = function (...args) {
return watcher.configure(...args)
}
module.exports = {watchPath, stopAllWatchers}

View File

@@ -77,6 +77,31 @@ class Project extends Model {
}
}
// Layers the contents of a project's file's config
// on top of the current global config.
replace (projectSpecification) {
if (projectSpecification == null) {
atom.config.clearProjectSettings()
this.setPaths([])
} else {
if (projectSpecification.originPath == null) {
return
}
// If no path is specified, set to directory of originPath.
if (!Array.isArray(projectSpecification.paths)) {
projectSpecification.paths = [path.dirname(projectSpecification.originPath)]
}
atom.config.resetProjectSettings(projectSpecification.config, projectSpecification.originPath)
this.setPaths(projectSpecification.paths)
}
this.emitter.emit('did-replace', projectSpecification)
}
onDidReplace (callback) {
return this.emitter.on('did-replace', callback)
}
/*
Section: Serialization
*/
@@ -174,7 +199,7 @@ class Project extends Model {
// const disposable = atom.project.onDidChangeFiles(events => {
// for (const event of events) {
// // "created", "modified", "deleted", or "renamed"
// console.log(`Event action: ${event.type}`)
// console.log(`Event action: ${event.action}`)
//
// // absolute path to the filesystem entry that was touched
// console.log(`Event path: ${event.path}`)
@@ -191,7 +216,7 @@ class Project extends Model {
// To watch paths outside of open projects, use the `watchPaths` function instead; see {PathWatcher}.
//
// When writing tests against functionality that uses this method, be sure to wait for the
// {Promise} returned by {getWatcherPromise()} before manipulating the filesystem to ensure that
// {Promise} returned by {::getWatcherPromise} before manipulating the filesystem to ensure that
// the watcher is receiving events.
//
// * `callback` {Function} to be called with batches of filesystem events reported by
@@ -209,6 +234,38 @@ class Project extends Model {
return this.emitter.on('did-change-files', callback)
}
// Public: Invoke the given callback with all current and future
// repositories in the project.
//
// * `callback` {Function} to be called with current and future
// repositories.
// * `repository` A {GitRepository} that is present at the time of
// subscription or that is added at some later time.
//
// Returns a {Disposable} on which `.dispose()` can be called to
// unsubscribe.
observeRepositories (callback) {
for (const repo of this.repositories) {
if (repo != null) {
callback(repo)
}
}
return this.onDidAddRepository(callback)
}
// Public: Invoke the given callback when a repository is added to the
// project.
//
// * `callback` {Function} to be called when a repository is added.
// * `repository` A {GitRepository}.
//
// Returns a {Disposable} on which `.dispose()` can be called to
// unsubscribe.
onDidAddRepository (callback) {
return this.emitter.on('did-add-repository', callback)
}
/*
Section: Accessing the git repository
*/
@@ -218,7 +275,7 @@ class Project extends Model {
//
// This method will be removed in 2.0 because it does synchronous I/O.
// Prefer the following, which evaluates to a {Promise} that resolves to an
// {Array} of {Repository} objects:
// {Array} of {GitRepository} objects:
// ```
// Promise.all(atom.project.getDirectories().map(
// atom.project.repositoryForDirectory.bind(atom.project)))
@@ -229,10 +286,10 @@ class Project extends Model {
// Public: Get the repository for a given directory asynchronously.
//
// * `directory` {Directory} for which to get a {Repository}.
// * `directory` {Directory} for which to get a {GitRepository}.
//
// Returns a {Promise} that resolves with either:
// * {Repository} if a repository can be created for the given directory
// * {GitRepository} if a repository can be created for the given directory
// * `null` if no repository can be created for the given directory.
repositoryForDirectory (directory) {
const pathForDirectory = directory.getRealPathSync()
@@ -323,7 +380,6 @@ class Project extends Model {
// a file or does not exist, its parent directory will be added instead.
addPath (projectPath, options = {}) {
const directory = this.getDirectoryForProjectPath(projectPath)
let ok = true
if (options.exact === true) {
ok = (directory.getPath() === projectPath)
@@ -353,6 +409,7 @@ class Project extends Model {
this.emitter.emit('did-change-files', events)
}
}
// We'll use the directory's custom onDidChangeFiles callback, if available.
// CustomDirectory::onDidChangeFiles should match the signature of
// Project::onDidChangeFiles below (although it may resolve asynchronously)
@@ -375,6 +432,9 @@ class Project extends Model {
if (repo) { break }
}
this.repositories.push(repo != null ? repo : null)
if (repo != null) {
this.emitter.emit('did-add-repository', repo)
}
if (options.emitEvent !== false) {
this.emitter.emit('did-change-paths', this.getPaths())
@@ -637,27 +697,32 @@ class Project extends Model {
// * `text` The {String} text to use as a buffer.
//
// Returns a {Promise} that resolves to the {TextBuffer}.
buildBuffer (absoluteFilePath) {
async buildBuffer (absoluteFilePath) {
const params = {shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete}
let promise
let buffer
if (absoluteFilePath != null) {
if (this.loadPromisesByPath[absoluteFilePath] == null) {
this.loadPromisesByPath[absoluteFilePath] =
TextBuffer.load(absoluteFilePath, params).catch(error => {
delete this.loadPromisesByPath[absoluteFilePath]
throw error
})
TextBuffer.load(absoluteFilePath, params)
.then(result => {
delete this.loadPromisesByPath[absoluteFilePath]
return result
})
.catch(error => {
delete this.loadPromisesByPath[absoluteFilePath]
throw error
})
}
promise = this.loadPromisesByPath[absoluteFilePath]
buffer = await this.loadPromisesByPath[absoluteFilePath]
} else {
promise = Promise.resolve(new TextBuffer(params))
buffer = new TextBuffer(params)
}
return promise.then(buffer => {
delete this.loadPromisesByPath[absoluteFilePath]
this.addBuffer(buffer)
return buffer
})
this.grammarRegistry.autoAssignLanguageMode(buffer)
this.addBuffer(buffer)
return buffer
}
addBuffer (buffer, options = {}) {
@@ -695,7 +760,7 @@ class Project extends Model {
}
subscribeToBuffer (buffer) {
buffer.onWillSave(({path}) => this.applicationDelegate.emitWillSavePath(path))
buffer.onWillSave(async ({path}) => this.applicationDelegate.emitWillSavePath(path))
buffer.onDidSave(({path}) => this.applicationDelegate.emitDidSavePath(path))
buffer.onDidDestroy(() => this.removeBuffer(buffer))
buffer.onDidChangePath(() => {

View File

@@ -122,8 +122,6 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
commandRegistry.add(
'atom-text-editor',
stopEventPropagation({
'core:undo': -> @undo()
'core:redo': -> @redo()
'core:move-left': -> @moveLeft()
'core:move-right': -> @moveRight()
'core:select-left': -> @selectLeft()
@@ -166,15 +164,35 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
false
)
commandRegistry.add(
'atom-text-editor:not([readonly])',
stopEventPropagation({
'core:undo': -> @undo()
'core:redo': -> @redo()
}),
false
)
commandRegistry.add(
'atom-text-editor',
stopEventPropagationAndGroupUndo(
config,
{
'core:copy': -> @copySelectedText()
'editor:copy-selection': -> @copyOnlySelectedText()
}
),
false
)
commandRegistry.add(
'atom-text-editor:not([readonly])',
stopEventPropagationAndGroupUndo(
config,
{
'core:backspace': -> @backspace()
'core:delete': -> @delete()
'core:cut': -> @cutSelectedText()
'core:copy': -> @copySelectedText()
'core:paste': -> @pasteText()
'editor:paste-without-reformatting': -> @pasteText({
normalizeLineEndings: false,
@@ -195,7 +213,6 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
'editor:transpose': -> @transpose()
'editor:upper-case': -> @upperCase()
'editor:lower-case': -> @lowerCase()
'editor:copy-selection': -> @copyOnlySelectedText()
}
),
false
@@ -266,7 +283,7 @@ module.exports = ({commandRegistry, commandInstaller, config, notificationManage
)
commandRegistry.add(
'atom-text-editor:not([mini])',
'atom-text-editor:not([mini]):not([readonly])',
stopEventPropagationAndGroupUndo(
config,
{

View File

@@ -1,8 +1,7 @@
/** @babel */
const SelectListView = require('atom-select-list')
import SelectListView from 'atom-select-list'
export default class ReopenProjectListView {
module.exports =
class ReopenProjectListView {
constructor (callback) {
this.callback = callback
this.selectListView = new SelectListView({

View File

@@ -1,9 +1,8 @@
/** @babel */
const {CompositeDisposable} = require('event-kit')
const path = require('path')
import {CompositeDisposable} from 'event-kit'
import path from 'path'
export default class ReopenProjectMenuManager {
module.exports =
class ReopenProjectMenuManager {
constructor ({menu, commands, history, config, open}) {
this.menuManager = menu
this.historyManager = history

View File

@@ -1,63 +0,0 @@
# Extended: Wraps an {Array} of `String`s. The Array describes a path from the
# root of the syntax tree to a token including _all_ scope names for the entire
# path.
#
# Methods that take a `ScopeDescriptor` will also accept an {Array} of {Strings}
# scope names e.g. `['.source.js']`.
#
# You can use `ScopeDescriptor`s to get language-specific config settings via
# {Config::get}.
#
# You should not need to create a `ScopeDescriptor` directly.
#
# * {TextEditor::getRootScopeDescriptor} to get the language's descriptor.
# * {TextEditor::scopeDescriptorForBufferPosition} to get the descriptor at a
# specific position in the buffer.
# * {Cursor::getScopeDescriptor} to get a cursor's descriptor based on position.
#
# See the [scopes and scope descriptor guide](http://flight-manual.atom.io/behind-atom/sections/scoped-settings-scopes-and-scope-descriptors/)
# for more information.
module.exports =
class ScopeDescriptor
@fromObject: (scopes) ->
if scopes instanceof ScopeDescriptor
scopes
else
new ScopeDescriptor({scopes})
###
Section: Construction and Destruction
###
# Public: Create a {ScopeDescriptor} object.
#
# * `object` {Object}
# * `scopes` {Array} of {String}s
constructor: ({@scopes}) ->
# Public: Returns an {Array} of {String}s
getScopesArray: -> @scopes
getScopeChain: ->
# For backward compatibility, prefix TextMate-style scope names with
# leading dots (e.g. 'source.js' -> '.source.js').
if @scopes[0].includes('.')
result = ''
for scope, i in @scopes
result += ' ' if i > 0
result += '.' if scope[0] isnt '.'
result += scope
result
else
@scopes.join(' ')
toString: ->
@getScopeChain()
isEqual: (other) ->
if @scopes.length isnt other.scopes.length
return false
for scope, i in @scopes
if scope isnt other.scopes[i]
return false
true

80
src/scope-descriptor.js Normal file
View File

@@ -0,0 +1,80 @@
// Extended: Wraps an {Array} of `String`s. The Array describes a path from the
// root of the syntax tree to a token including _all_ scope names for the entire
// path.
//
// Methods that take a `ScopeDescriptor` will also accept an {Array} of {String}
// scope names e.g. `['.source.js']`.
//
// You can use `ScopeDescriptor`s to get language-specific config settings via
// {Config::get}.
//
// You should not need to create a `ScopeDescriptor` directly.
//
// * {TextEditor::getRootScopeDescriptor} to get the language's descriptor.
// * {TextEditor::scopeDescriptorForBufferPosition} to get the descriptor at a
// specific position in the buffer.
// * {Cursor::getScopeDescriptor} to get a cursor's descriptor based on position.
//
// See the [scopes and scope descriptor guide](http://flight-manual.atom.io/behind-atom/sections/scoped-settings-scopes-and-scope-descriptors/)
// for more information.
module.exports =
class ScopeDescriptor {
static fromObject (scopes) {
if (scopes instanceof ScopeDescriptor) {
return scopes
} else {
return new ScopeDescriptor({scopes})
}
}
/*
Section: Construction and Destruction
*/
// Public: Create a {ScopeDescriptor} object.
//
// * `object` {Object}
// * `scopes` {Array} of {String}s
constructor ({scopes}) {
this.scopes = scopes
}
// Public: Returns an {Array} of {String}s
getScopesArray () {
return this.scopes
}
getScopeChain () {
// For backward compatibility, prefix TextMate-style scope names with
// leading dots (e.g. 'source.js' -> '.source.js').
if (this.scopes[0] != null && this.scopes[0].includes('.')) {
let result = ''
for (let i = 0; i < this.scopes.length; i++) {
const scope = this.scopes[i]
if (i > 0) { result += ' ' }
if (scope[0] !== '.') { result += '.' }
result += scope
}
return result
} else {
return this.scopes.join(' ')
}
}
toString () {
return this.getScopeChain()
}
isEqual (other) {
if (this.scopes.length !== other.scopes.length) {
return false
}
for (let i = 0; i < this.scopes.length; i++) {
const scope = this.scopes[i]
if (scope !== other.scopes[i]) {
return false
}
}
return true
}
}

View File

@@ -407,6 +407,25 @@ class Selection {
if (autoscroll) this.cursor.autoscroll()
}
// Private: Ensure that the {TextEditor} is not marked read-only before allowing a buffer modification to occur. if
// the editor is read-only, require an explicit opt-in option to proceed (`bypassReadOnly`) or throw an Error.
ensureWritable (methodName, opts) {
if (!opts.bypassReadOnly && this.editor.isReadOnly()) {
if (atom.inDevMode() || atom.inSpecMode()) {
const e = new Error('Attempt to mutate a read-only TextEditor through a Selection')
e.detail =
`Your package is attempting to call ${methodName} on a selection within an editor that has been marked ` +
' read-only. Pass {bypassReadOnly: true} to modify it anyway, or test editors with .isReadOnly() before ' +
' attempting modifications.'
throw e
}
return false
}
return true
}
/*
Section: Modifying the selected text
*/
@@ -427,8 +446,11 @@ class Selection {
// behavior is suppressed.
// level between the first lines and the trailing lines.
// * `normalizeLineEndings` (optional) {Boolean} (default: true)
// * `undo` If `skip`, skips the undo stack for this operation.
// * `undo` *Deprecated* If `skip`, skips the undo stack for this operation. This property is deprecated. Call groupLastChanges() on the {TextBuffer} afterward instead.
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
insertText (text, options = {}) {
if (!this.ensureWritable('insertText', options)) return
let desiredIndentLevel, indentAdjustment
const oldBufferRange = this.getBufferRange()
const wasReversed = this.isReversed()
@@ -492,90 +514,134 @@ class Selection {
// Public: Removes the first character before the selection if the selection
// is empty otherwise it deletes the selection.
backspace () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
backspace (options = {}) {
if (!this.ensureWritable('backspace', options)) return
if (this.isEmpty()) this.selectLeft()
this.deleteSelectedText()
this.deleteSelectedText(options)
}
// Public: Removes the selection or, if nothing is selected, then all
// characters from the start of the selection back to the previous word
// boundary.
deleteToPreviousWordBoundary () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToPreviousWordBoundary (options = {}) {
if (!this.ensureWritable('deleteToPreviousWordBoundary', options)) return
if (this.isEmpty()) this.selectToPreviousWordBoundary()
this.deleteSelectedText()
this.deleteSelectedText(options)
}
// Public: Removes the selection or, if nothing is selected, then all
// characters from the start of the selection up to the next word
// boundary.
deleteToNextWordBoundary () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToNextWordBoundary (options = {}) {
if (!this.ensureWritable('deleteToNextWordBoundary', options)) return
if (this.isEmpty()) this.selectToNextWordBoundary()
this.deleteSelectedText()
this.deleteSelectedText(options)
}
// Public: Removes from the start of the selection to the beginning of the
// current word if the selection is empty otherwise it deletes the selection.
deleteToBeginningOfWord () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToBeginningOfWord (options = {}) {
if (!this.ensureWritable('deleteToBeginningOfWord', options)) return
if (this.isEmpty()) this.selectToBeginningOfWord()
this.deleteSelectedText()
this.deleteSelectedText(options)
}
// Public: Removes from the beginning of the line which the selection begins on
// all the way through to the end of the selection.
deleteToBeginningOfLine () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToBeginningOfLine (options = {}) {
if (!this.ensureWritable('deleteToBeginningOfLine', options)) return
if (this.isEmpty() && this.cursor.isAtBeginningOfLine()) {
this.selectLeft()
} else {
this.selectToBeginningOfLine()
}
this.deleteSelectedText()
this.deleteSelectedText(options)
}
// Public: Removes the selection or the next character after the start of the
// selection if the selection is empty.
delete () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
delete (options = {}) {
if (!this.ensureWritable('delete', options)) return
if (this.isEmpty()) this.selectRight()
this.deleteSelectedText()
this.deleteSelectedText(options)
}
// Public: If the selection is empty, removes all text from the cursor to the
// end of the line. If the cursor is already at the end of the line, it
// removes the following newline. If the selection isn't empty, only deletes
// the contents of the selection.
deleteToEndOfLine () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToEndOfLine (options = {}) {
if (!this.ensureWritable('deleteToEndOfLine', options)) return
if (this.isEmpty()) {
if (this.cursor.isAtEndOfLine()) {
this.delete()
this.delete(options)
return
}
this.selectToEndOfLine()
}
this.deleteSelectedText()
this.deleteSelectedText(options)
}
// Public: Removes the selection or all characters from the start of the
// selection to the end of the current word if nothing is selected.
deleteToEndOfWord () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToEndOfWord (options = {}) {
if (!this.ensureWritable('deleteToEndOfWord', options)) return
if (this.isEmpty()) this.selectToEndOfWord()
this.deleteSelectedText()
this.deleteSelectedText(options)
}
// Public: Removes the selection or all characters from the start of the
// selection to the end of the current word if nothing is selected.
deleteToBeginningOfSubword () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToBeginningOfSubword (options = {}) {
if (!this.ensureWritable('deleteToBeginningOfSubword', options)) return
if (this.isEmpty()) this.selectToPreviousSubwordBoundary()
this.deleteSelectedText()
this.deleteSelectedText(options)
}
// Public: Removes the selection or all characters from the start of the
// selection to the end of the current word if nothing is selected.
deleteToEndOfSubword () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteToEndOfSubword (options = {}) {
if (!this.ensureWritable('deleteToEndOfSubword', options)) return
if (this.isEmpty()) this.selectToNextSubwordBoundary()
this.deleteSelectedText()
this.deleteSelectedText(options)
}
// Public: Removes only the selected text.
deleteSelectedText () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteSelectedText (options = {}) {
if (!this.ensureWritable('deleteSelectedText', options)) return
const bufferRange = this.getBufferRange()
if (!bufferRange.isEmpty()) this.editor.buffer.delete(bufferRange)
if (this.cursor) this.cursor.setBufferPosition(bufferRange.start)
@@ -584,7 +650,11 @@ class Selection {
// Public: Removes the line at the beginning of the selection if the selection
// is empty unless the selection spans multiple lines in which case all lines
// are removed.
deleteLine () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
deleteLine (options = {}) {
if (!this.ensureWritable('deleteLine', options)) return
const range = this.getBufferRange()
if (range.isEmpty()) {
const start = this.cursor.getScreenRow()
@@ -607,7 +677,11 @@ class Selection {
// be separated by a single space.
//
// If there selection spans more than one line, all the lines are joined together.
joinLines () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
joinLines (options = {}) {
if (!this.ensureWritable('joinLines', options)) return
let joinMarker
const selectedRange = this.getBufferRange()
if (selectedRange.isEmpty()) {
@@ -629,7 +703,7 @@ class Selection {
})
if (trailingWhitespaceRange) {
this.setBufferRange(trailingWhitespaceRange)
this.deleteSelectedText()
this.deleteSelectedText(options)
}
const currentRow = selectedRange.start.row
@@ -638,7 +712,7 @@ class Selection {
(nextRow <= this.editor.buffer.getLastRow()) &&
(this.editor.buffer.lineLengthForRow(nextRow) > 0) &&
(this.editor.buffer.lineLengthForRow(currentRow) > 0)
if (insertSpace) this.insertText(' ')
if (insertSpace) this.insertText(' ', options)
this.cursor.moveToEndOfLine()
@@ -647,7 +721,7 @@ class Selection {
this.cursor.moveRight()
this.cursor.moveToFirstCharacterOfLine()
})
this.deleteSelectedText()
this.deleteSelectedText(options)
if (insertSpace) this.cursor.moveLeft()
}
@@ -660,7 +734,11 @@ class Selection {
}
// Public: Removes one level of indent from the currently selected rows.
outdentSelectedRows () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
outdentSelectedRows (options = {}) {
if (!this.ensureWritable('outdentSelectedRows', options)) return
const [start, end] = this.getBufferRowRange()
const {buffer} = this.editor
const leadingTabRegex = new RegExp(`^( {1,${this.editor.getTabLength()}}|\t)`)
@@ -674,7 +752,11 @@ class Selection {
// Public: Sets the indentation level of all selected rows to values suggested
// by the relevant grammars.
autoIndentSelectedRows () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
autoIndentSelectedRows (options = {}) {
if (!this.ensureWritable('autoIndentSelectedRows', options)) return
const [start, end] = this.getBufferRowRange()
return this.editor.autoIndentBufferRows(start, end)
}
@@ -683,29 +765,45 @@ class Selection {
// of a comment.
//
// Removes the comment if they are currently wrapped in a comment.
toggleLineComments () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
toggleLineComments (options = {}) {
if (!this.ensureWritable('toggleLineComments', options)) return
this.editor.toggleLineCommentsForBufferRows(...(this.getBufferRowRange() || []))
}
// Public: Cuts the selection until the end of the screen line.
cutToEndOfLine (maintainClipboard) {
//
// * `maintainClipboard` {Boolean}
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
cutToEndOfLine (maintainClipboard, options = {}) {
if (!this.ensureWritable('cutToEndOfLine', options)) return
if (this.isEmpty()) this.selectToEndOfLine()
return this.cut(maintainClipboard)
return this.cut(maintainClipboard, false, options.bypassReadOnly)
}
// Public: Cuts the selection until the end of the buffer line.
cutToEndOfBufferLine (maintainClipboard) {
//
// * `maintainClipboard` {Boolean}
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
cutToEndOfBufferLine (maintainClipboard, options = {}) {
if (!this.ensureWritable('cutToEndOfBufferLine', options)) return
if (this.isEmpty()) this.selectToEndOfBufferLine()
this.cut(maintainClipboard)
this.cut(maintainClipboard, false, options.bypassReadOnly)
}
// Public: Copies the selection to the clipboard and then deletes it.
//
// * `maintainClipboard` {Boolean} (default: false) See {::copy}
// * `fullLine` {Boolean} (default: false) See {::copy}
cut (maintainClipboard = false, fullLine = false) {
// * `bypassReadOnly` {Boolean} (default: false) Must be `true` to modify text within a read-only editor.
cut (maintainClipboard = false, fullLine = false, bypassReadOnly = false) {
if (!this.ensureWritable('cut', {bypassReadOnly})) return
this.copy(maintainClipboard, fullLine)
this.delete()
this.delete({bypassReadOnly})
}
// Public: Copies the current selection to the clipboard.
@@ -783,7 +881,9 @@ class Selection {
// * `options` (optional) {Object} with the keys:
// * `autoIndent` If `true`, the line is indented to an automatically-inferred
// level. Otherwise, {TextEditor::getTabText} is inserted.
indent ({autoIndent} = {}) {
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
indent ({autoIndent, bypassReadOnly} = {}) {
if (!this.ensureWritable('indent', {bypassReadOnly})) return
const {row} = this.cursor.getBufferPosition()
if (this.isEmpty()) {
@@ -793,17 +893,21 @@ class Selection {
if (autoIndent && delta > 0) {
if (!this.editor.getSoftTabs()) delta = Math.max(delta, 1)
this.insertText(this.editor.buildIndentString(delta))
this.insertText(this.editor.buildIndentString(delta), {bypassReadOnly})
} else {
this.insertText(this.editor.buildIndentString(1, this.cursor.getBufferColumn()))
this.insertText(this.editor.buildIndentString(1, this.cursor.getBufferColumn()), {bypassReadOnly})
}
} else {
this.indentSelectedRows()
this.indentSelectedRows({bypassReadOnly})
}
}
// Public: If the selection spans multiple rows, indent all of them.
indentSelectedRows () {
//
// * `options` (optional) {Object} with the keys:
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify text within a read-only editor. (default: false)
indentSelectedRows (options = {}) {
if (!this.ensureWritable('indentSelectedRows', options)) return
const [start, end] = this.getBufferRowRange()
for (let row = start; row <= end; row++) {
if (this.editor.buffer.lineLengthForRow(row) !== 0) {

38
src/selectors.js Normal file
View File

@@ -0,0 +1,38 @@
module.exports = {selectorMatchesAnyScope, matcherForSelector}
const {isSubset} = require('underscore-plus')
// Private: Parse a selector into parts.
// If already parsed, returns the selector unmodified.
//
// * `selector` a {String|Array<String>} specifying what to match
// Returns selector parts, an {Array<String>}.
function parse (selector) {
return typeof selector === 'string'
? selector.replace(/^\./, '').split('.')
: selector
}
const always = scope => true
// Essential: Return a matcher function for a selector.
//
// * selector, a {String} selector
// Returns {(scope: String) -> Boolean}, a matcher function returning
// true iff the scope matches the selector.
function matcherForSelector (selector) {
const parts = parse(selector)
if (typeof parts === 'function') return parts
return selector
? scope => isSubset(parts, parse(scope))
: always
}
// Essential: Return true iff the selector matches any provided scope.
//
// * {String} selector
// * {Array<String>} scopes
// Returns {Boolean} true if any scope matches the selector.
function selectorMatchesAnyScope (selector, scopes) {
return !selector || scopes.some(matcherForSelector(selector))
}

View File

@@ -1,39 +0,0 @@
path = require "path"
fs = require "fs-plus"
module.exports =
class StorageFolder
constructor: (containingPath) ->
@path = path.join(containingPath, "storage") if containingPath?
clear: ->
return unless @path?
try
fs.removeSync(@path)
catch error
console.warn "Error deleting #{@path}", error.stack, error
storeSync: (name, object) ->
return unless @path?
fs.writeFileSync(@pathForKey(name), JSON.stringify(object), 'utf8')
load: (name) ->
return unless @path?
statePath = @pathForKey(name)
try
stateString = fs.readFileSync(statePath, 'utf8')
catch error
unless error.code is 'ENOENT'
console.warn "Error reading state file: #{statePath}", error.stack, error
return undefined
try
JSON.parse(stateString)
catch error
console.warn "Error parsing state file: #{statePath}", error.stack, error
pathForKey: (name) -> path.join(@getPath(), name)
getPath: -> @path

49
src/storage-folder.js Normal file
View File

@@ -0,0 +1,49 @@
const path = require('path')
const fs = require('fs-plus')
module.exports =
class StorageFolder {
constructor (containingPath) {
if (containingPath) {
this.path = path.join(containingPath, 'storage')
}
}
store (name, object) {
return new Promise((resolve, reject) => {
if (!this.path) return resolve()
fs.writeFile(this.pathForKey(name), JSON.stringify(object), 'utf8', error =>
error ? reject(error) : resolve()
)
})
}
load (name) {
return new Promise(resolve => {
if (!this.path) return resolve(null)
const statePath = this.pathForKey(name)
fs.readFile(statePath, 'utf8', (error, stateString) => {
if (error && error.code !== 'ENOENT') {
console.warn(`Error reading state file: ${statePath}`, error.stack, error)
}
if (!stateString) return resolve(null)
try {
resolve(JSON.parse(stateString))
} catch (error) {
console.warn(`Error parsing state file: ${statePath}`, error.stack, error)
resolve(null)
}
})
})
}
pathForKey (name) {
return path.join(this.getPath(), name)
}
getPath () {
return this.path
}
}

9
src/test.ejs Normal file
View File

@@ -0,0 +1,9 @@
<html>
<% if something() { %>
<div class=foo>
<%= html `ok how about <span>this</span>` %>
</div>
<% } %>
</html>

View File

@@ -148,12 +148,13 @@ class TextEditorComponent {
this.lineNumbersToRender = {
maxDigits: 2,
bufferRows: [],
screenRows: [],
keys: [],
softWrappedFlags: [],
foldableFlags: []
}
this.decorationsToRender = {
lineNumbers: null,
lineNumbers: new Map(),
lines: null,
highlights: [],
cursors: [],
@@ -266,14 +267,22 @@ class TextEditorComponent {
if (useScheduler === true) {
const scheduler = etch.getScheduler()
scheduler.readDocument(() => {
this.measureContentDuringUpdateSync()
const restartFrame = this.measureContentDuringUpdateSync()
scheduler.updateDocument(() => {
this.updateSyncAfterMeasuringContent()
if (restartFrame) {
this.updateSync(true)
} else {
this.updateSyncAfterMeasuringContent()
}
})
})
} else {
this.measureContentDuringUpdateSync()
this.updateSyncAfterMeasuringContent()
const restartFrame = this.measureContentDuringUpdateSync()
if (restartFrame) {
this.updateSync(false)
} else {
this.updateSyncAfterMeasuringContent()
}
}
this.updateScheduled = false
@@ -391,15 +400,16 @@ class TextEditorComponent {
this.measureHorizontalPositions()
this.updateAbsolutePositionedDecorations()
const isHorizontalScrollbarVisible = (
this.canScrollHorizontally() &&
this.getHorizontalScrollbarHeight() > 0
)
if (this.pendingAutoscroll) {
this.derivedDimensionsCache = {}
const {screenRange, options} = this.pendingAutoscroll
this.autoscrollHorizontally(screenRange, options)
const isHorizontalScrollbarVisible = (
this.canScrollHorizontally() &&
this.getHorizontalScrollbarHeight() > 0
)
if (!wasHorizontalScrollbarVisible && isHorizontalScrollbarVisible) {
this.autoscrollVertically(screenRange, options)
}
@@ -408,6 +418,8 @@ class TextEditorComponent {
this.linesToMeasure.clear()
this.measuredContent = true
return wasHorizontalScrollbarVisible !== isHorizontalScrollbarVisible
}
updateSyncAfterMeasuringContent () {
@@ -447,15 +459,18 @@ class TextEditorComponent {
let clientContainerWidth = '100%'
if (this.hasInitialMeasurements) {
if (model.getAutoHeight()) {
clientContainerHeight = this.getContentHeight()
if (this.canScrollHorizontally()) clientContainerHeight += this.getHorizontalScrollbarHeight()
clientContainerHeight += 'px'
clientContainerHeight =
this.getContentHeight() +
this.getHorizontalScrollbarHeight() +
'px'
}
if (model.getAutoWidth()) {
style.width = 'min-content'
clientContainerWidth = this.getGutterContainerWidth() + this.getContentWidth()
if (this.canScrollVertically()) clientContainerWidth += this.getVerticalScrollbarWidth()
clientContainerWidth += 'px'
clientContainerWidth =
this.getGutterContainerWidth() +
this.getContentWidth() +
this.getVerticalScrollbarWidth() +
'px'
} else {
style.width = this.element.style.width
}
@@ -466,7 +481,7 @@ class TextEditorComponent {
attributes.mini = ''
}
if (!this.isInputEnabled()) {
if (model.isReadOnly()) {
attributes.readonly = ''
}
@@ -740,20 +755,14 @@ class TextEditorComponent {
scrollLeft = this.getScrollLeft()
canScrollHorizontally = this.canScrollHorizontally()
canScrollVertically = this.canScrollVertically()
horizontalScrollbarHeight =
canScrollHorizontally
? this.getHorizontalScrollbarHeight()
: 0
verticalScrollbarWidth =
canScrollVertically
? this.getVerticalScrollbarWidth()
: 0
horizontalScrollbarHeight = this.getHorizontalScrollbarHeight()
verticalScrollbarWidth = this.getVerticalScrollbarWidth()
forceScrollbarVisible = this.remeasureScrollbars
} else {
forceScrollbarVisible = true
}
const dummyScrollbarVnodes = [
return [
$(DummyScrollbarComponent, {
ref: 'verticalScrollbar',
orientation: 'vertical',
@@ -775,13 +784,10 @@ class TextEditorComponent {
scrollLeft,
verticalScrollbarWidth,
forceScrollbarVisible
})
]
}),
// If both scrollbars are visible, push a dummy element to force a "corner"
// to render where the two scrollbars meet at the lower right
if (verticalScrollbarWidth > 0 && horizontalScrollbarHeight > 0) {
dummyScrollbarVnodes.push($.div(
// Force a "corner" to render where the two scrollbars meet at the lower right
$.div(
{
ref: 'scrollbarCorner',
className: 'scrollbar-corner',
@@ -794,10 +800,8 @@ class TextEditorComponent {
overflow: 'scroll'
}
}
))
}
return dummyScrollbarVnodes
)
]
} else {
return null
}
@@ -845,10 +849,7 @@ class TextEditorComponent {
}
for (let i = 0; i < newClassList.length; i++) {
const className = newClassList[i]
if (!oldClassList || !oldClassList.includes(className)) {
this.element.classList.add(className)
}
this.element.classList.add(newClassList[i])
}
this.classList = newClassList
@@ -886,7 +887,7 @@ class TextEditorComponent {
queryLineNumbersToRender () {
const {model} = this.props
if (!model.isLineNumberGutterVisible()) return
if (!model.anyLineNumberGutterVisible()) return
if (this.showLineNumbers !== model.doesShowLineNumbers()) {
this.remeasureGutterDimensions = true
this.showLineNumbers = model.doesShowLineNumbers()
@@ -942,7 +943,7 @@ class TextEditorComponent {
queryMaxLineNumberDigits () {
const {model} = this.props
if (model.isLineNumberGutterVisible()) {
if (model.anyLineNumberGutterVisible()) {
const maxDigits = Math.max(2, model.getLineCount().toString().length)
if (maxDigits !== this.lineNumbersToRender.maxDigits) {
this.remeasureGutterDimensions = true
@@ -977,7 +978,7 @@ class TextEditorComponent {
}
queryDecorationsToRender () {
this.decorationsToRender.lineNumbers = []
this.decorationsToRender.lineNumbers.clear()
this.decorationsToRender.lines = []
this.decorationsToRender.overlays.length = 0
this.decorationsToRender.customGutter.clear()
@@ -1040,7 +1041,17 @@ class TextEditorComponent {
}
addLineDecorationToRender (type, decoration, screenRange, reversed) {
const decorationsToRender = (type === 'line') ? this.decorationsToRender.lines : this.decorationsToRender.lineNumbers
let decorationsToRender
if (type === 'line') {
decorationsToRender = this.decorationsToRender.lines
} else {
const gutterName = decoration.gutterName || 'line-number'
decorationsToRender = this.decorationsToRender.lineNumbers.get(gutterName)
if (!decorationsToRender) {
decorationsToRender = []
this.decorationsToRender.lineNumbers.set(gutterName, decorationsToRender)
}
}
let omitLastRow = false
if (screenRange.isEmpty()) {
@@ -1520,15 +1531,11 @@ class TextEditorComponent {
let {wheelDeltaX, wheelDeltaY} = event
if (Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)) {
wheelDeltaX = (Math.sign(wheelDeltaX) === 1)
? Math.max(1, wheelDeltaX * scrollSensitivity)
: Math.min(-1, wheelDeltaX * scrollSensitivity)
wheelDeltaX = wheelDeltaX * scrollSensitivity
wheelDeltaY = 0
} else {
wheelDeltaX = 0
wheelDeltaY = (Math.sign(wheelDeltaY) === 1)
? Math.max(1, wheelDeltaY * scrollSensitivity)
: Math.min(-1, wheelDeltaY * scrollSensitivity)
wheelDeltaY = wheelDeltaY * scrollSensitivity
}
if (this.getPlatform() !== 'darwin' && event.shiftKey) {
@@ -1752,28 +1759,28 @@ class TextEditorComponent {
const screenPosition = this.screenPositionForMouseEvent(event)
if (button !== 0 || (platform === 'darwin' && ctrlKey)) {
// Always set cursor position on middle-click
// Only set cursor position on right-click if there is one cursor with no selection
const ranges = model.getSelectedBufferRanges()
if (button === 1 || (ranges.length === 1 && ranges[0].isEmpty())) {
model.setCursorScreenPosition(screenPosition, {autoscroll: false})
}
if (button === 1) {
model.setCursorScreenPosition(screenPosition, {autoscroll: false})
// On Linux, pasting happens on middle click. A textInput event with the
// contents of the selection clipboard will be dispatched by the browser
// automatically on mouseup.
if (platform === 'linux' && button === 1) model.insertText(clipboard.readText('selection'))
if (platform === 'linux' && this.isInputEnabled()) model.insertText(clipboard.readText('selection'))
return
}
if (button !== 0) return
// Ctrl-click brings up the context menu on macOS
if (platform === 'darwin' && ctrlKey) return
if (target && target.matches('.fold-marker')) {
const bufferPosition = model.bufferPositionForScreenPosition(screenPosition)
model.destroyFoldsContainingBufferPositions([bufferPosition], false)
return
}
const addOrRemoveSelection = metaKey || ctrlKey
const addOrRemoveSelection = metaKey || (ctrlKey && platform !== 'darwin')
switch (detail) {
case 1:
@@ -2619,37 +2626,25 @@ class TextEditorComponent {
getScrollContainerHeight () {
if (this.props.model.getAutoHeight()) {
return this.getScrollHeight()
return this.getScrollHeight() + this.getHorizontalScrollbarHeight()
} else {
return this.getClientContainerHeight()
}
}
getScrollContainerClientWidth () {
if (this.canScrollVertically()) {
return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth()
} else {
return this.getScrollContainerWidth()
}
return this.getScrollContainerWidth() - this.getVerticalScrollbarWidth()
}
getScrollContainerClientHeight () {
if (this.canScrollHorizontally()) {
return this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight()
} else {
return this.getScrollContainerHeight()
}
return this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight()
}
canScrollVertically () {
const {model} = this.props
if (model.isMini()) return false
if (model.getAutoHeight()) return false
if (this.getContentHeight() > this.getScrollContainerHeight()) return true
return (
this.getContentWidth() > this.getScrollContainerWidth() &&
this.getContentHeight() > (this.getScrollContainerHeight() - this.getHorizontalScrollbarHeight())
)
return this.getContentHeight() > this.getScrollContainerClientHeight()
}
canScrollHorizontally () {
@@ -2657,11 +2652,7 @@ class TextEditorComponent {
if (model.isMini()) return false
if (model.getAutoWidth()) return false
if (model.isSoftWrapped()) return false
if (this.getContentWidth() > this.getScrollContainerWidth()) return true
return (
this.getContentHeight() > this.getScrollContainerHeight() &&
this.getContentWidth() > (this.getScrollContainerWidth() - this.getVerticalScrollbarWidth())
)
return this.getContentWidth() > this.getScrollContainerClientWidth()
}
getScrollHeight () {
@@ -2694,7 +2685,7 @@ class TextEditorComponent {
}
getContentWidth () {
return Math.round(this.getLongestLineWidth() + this.getBaseCharacterWidth())
return Math.ceil(this.getLongestLineWidth() + this.getBaseCharacterWidth())
}
getScrollContainerClientWidthInBaseCharacters () {
@@ -2804,7 +2795,7 @@ class TextEditorComponent {
setScrollTop (scrollTop) {
if (Number.isNaN(scrollTop) || scrollTop == null) return false
scrollTop = Math.round(Math.max(0, Math.min(this.getMaxScrollTop(), scrollTop)))
scrollTop = roundToPhysicalPixelBoundary(Math.max(0, Math.min(this.getMaxScrollTop(), scrollTop)))
if (scrollTop !== this.scrollTop) {
this.derivedDimensionsCache = {}
this.scrollTopPending = true
@@ -2835,7 +2826,7 @@ class TextEditorComponent {
setScrollLeft (scrollLeft) {
if (Number.isNaN(scrollLeft) || scrollLeft == null) return false
scrollLeft = Math.round(Math.max(0, Math.min(this.getMaxScrollLeft(), scrollLeft)))
scrollLeft = roundToPhysicalPixelBoundary(Math.max(0, Math.min(this.getMaxScrollLeft(), scrollLeft)))
if (scrollLeft !== this.scrollLeft) {
this.scrollLeftPending = true
this.scrollLeft = scrollLeft
@@ -2958,11 +2949,11 @@ class TextEditorComponent {
}
setInputEnabled (inputEnabled) {
this.props.model.update({readOnly: !inputEnabled})
this.props.model.update({keyboardInputEnabled: inputEnabled})
}
isInputEnabled (inputEnabled) {
return !this.props.model.isReadOnly()
isInputEnabled () {
return !this.props.model.isReadOnly() && this.props.model.isKeyboardInputEnabled()
}
getHiddenInput () {
@@ -3119,7 +3110,7 @@ class GutterContainerComponent {
},
$.div({style: innerStyle},
guttersToRender.map((gutter) => {
if (gutter.name === 'line-number') {
if (gutter.type === 'line-number') {
return this.renderLineNumberGutter(gutter)
} else {
return $(CustomGutterComponent, {
@@ -3138,18 +3129,29 @@ class GutterContainerComponent {
renderLineNumberGutter (gutter) {
const {
rootComponent, isLineNumberGutterVisible, showLineNumbers, hasInitialMeasurements, lineNumbersToRender,
rootComponent, showLineNumbers, hasInitialMeasurements, lineNumbersToRender,
renderedStartRow, renderedEndRow, rowsPerTile, decorationsToRender, didMeasureVisibleBlockDecoration,
scrollHeight, lineNumberGutterWidth, lineHeight
} = this.props
if (!isLineNumberGutterVisible) return null
if (!gutter.isVisible()) {
return null
}
const oneTrueLineNumberGutter = gutter.name === 'line-number'
const ref = oneTrueLineNumberGutter ? 'lineNumberGutter' : undefined
const width = oneTrueLineNumberGutter ? lineNumberGutterWidth : undefined
if (hasInitialMeasurements) {
const {maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags} = lineNumbersToRender
return $(LineNumberGutterComponent, {
ref: 'lineNumberGutter',
ref,
element: gutter.getElement(),
name: gutter.name,
className: gutter.className,
labelFn: gutter.labelFn,
onMouseDown: gutter.onMouseDown,
onMouseMove: gutter.onMouseMove,
rootComponent: rootComponent,
startRow: renderedStartRow,
endRow: renderedEndRow,
@@ -3160,18 +3162,22 @@ class GutterContainerComponent {
screenRows: screenRows,
softWrappedFlags: softWrappedFlags,
foldableFlags: foldableFlags,
decorations: decorationsToRender.lineNumbers,
decorations: decorationsToRender.lineNumbers.get(gutter.name) || [],
blockDecorations: decorationsToRender.blocks,
didMeasureVisibleBlockDecoration: didMeasureVisibleBlockDecoration,
height: scrollHeight,
width: lineNumberGutterWidth,
width,
lineHeight: lineHeight,
showLineNumbers
})
} else {
return $(LineNumberGutterComponent, {
ref: 'lineNumberGutter',
ref,
element: gutter.getElement(),
name: gutter.name,
className: gutter.className,
onMouseDown: gutter.onMouseDown,
onMouseMove: gutter.onMouseMove,
maxDigits: lineNumbersToRender.maxDigits,
showLineNumbers
})
@@ -3199,7 +3205,8 @@ class LineNumberGutterComponent {
render () {
const {
rootComponent, showLineNumbers, height, width, startRow, endRow, rowsPerTile,
maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags, decorations
maxDigits, keys, bufferRows, screenRows, softWrappedFlags, foldableFlags, decorations,
className
} = this.props
let children = null
@@ -3227,8 +3234,12 @@ class LineNumberGutterComponent {
let number = null
if (showLineNumbers) {
number = softWrapped ? '•' : bufferRow + 1
number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number
if (this.props.labelFn == null) {
number = softWrapped ? '•' : bufferRow + 1
number = NBSP_CHARACTER.repeat(maxDigits - number.length) + number
} else {
number = this.props.labelFn({bufferRow, screenRow, foldable, softWrapped, maxDigits})
}
}
// We need to adjust the line number position to account for block
@@ -3255,6 +3266,7 @@ class LineNumberGutterComponent {
const tileTop = rootComponent.pixelPositionBeforeBlocksForRow(tileStartRow)
const tileBottom = rootComponent.pixelPositionBeforeBlocksForRow(tileEndRow)
const tileHeight = tileBottom - tileTop
const tileWidth = width != null && width > 0 ? width + 'px' : ''
children[i] = $.div({
key: rootComponent.idsByTileStartRow.get(tileStartRow),
@@ -3263,20 +3275,26 @@ class LineNumberGutterComponent {
position: 'absolute',
top: 0,
height: tileHeight + 'px',
width: width + 'px',
width: tileWidth,
transform: `translateY(${tileTop}px)`
}
}, ...tileChildren)
}
}
let rootClassName = 'gutter line-numbers'
if (className) {
rootClassName += ' ' + className
}
return $.div(
{
className: 'gutter line-numbers',
attributes: {'gutter-name': 'line-number'},
className: rootClassName,
attributes: {'gutter-name': this.props.name},
style: {position: 'relative', height: ceilToPhysicalPixelBoundary(height) + 'px'},
on: {
mousedown: this.didMouseDown
mousedown: this.didMouseDown,
mousemove: this.didMouseMove
}
},
$.div({key: 'placeholder', className: 'line-number dummy', style: {visibility: 'hidden'}},
@@ -3298,6 +3316,8 @@ class LineNumberGutterComponent {
if (oldProps.endRow !== newProps.endRow) return true
if (oldProps.rowsPerTile !== newProps.rowsPerTile) return true
if (oldProps.maxDigits !== newProps.maxDigits) return true
if (oldProps.labelFn !== newProps.labelFn) return true
if (oldProps.className !== newProps.className) return true
if (newProps.didMeasureVisibleBlockDecoration) return true
if (!arraysEqual(oldProps.keys, newProps.keys)) return true
if (!arraysEqual(oldProps.bufferRows, newProps.bufferRows)) return true
@@ -3344,7 +3364,27 @@ class LineNumberGutterComponent {
}
didMouseDown (event) {
this.props.rootComponent.didMouseDownOnLineNumberGutter(event)
if (this.props.onMouseDown == null) {
this.props.rootComponent.didMouseDownOnLineNumberGutter(event)
} else {
const {bufferRow, screenRow} = event.target.dataset
this.props.onMouseDown({
bufferRow: parseInt(bufferRow, 10),
screenRow: parseInt(screenRow, 10),
domEvent: event
})
}
}
didMouseMove (event) {
if (this.props.onMouseMove != null) {
const {bufferRow, screenRow} = event.target.dataset
this.props.onMouseMove({
bufferRow: parseInt(bufferRow, 10),
screenRow: parseInt(screenRow, 10),
domEvent: event
})
}
}
}
@@ -3352,7 +3392,8 @@ class LineNumberComponent {
constructor (props) {
const {className, width, marginTop, bufferRow, screenRow, number, nodePool} = props
this.props = props
const style = {width: width + 'px'}
const style = {}
if (width != null && width > 0) style.width = width + 'px'
if (marginTop != null && marginTop > 0) style.marginTop = marginTop + 'px'
this.element = nodePool.getElement('DIV', className, style)
this.element.dataset.bufferRow = bufferRow
@@ -3372,22 +3413,31 @@ class LineNumberComponent {
if (this.props.bufferRow !== bufferRow) this.element.dataset.bufferRow = bufferRow
if (this.props.screenRow !== screenRow) this.element.dataset.screenRow = screenRow
if (this.props.className !== className) this.element.className = className
if (this.props.width !== width) this.element.style.width = width + 'px'
if (this.props.width !== width) {
if (width != null && width > 0) {
this.element.style.width = width + 'px'
} else {
this.element.style.width = ''
}
}
if (this.props.marginTop !== marginTop) {
if (marginTop != null) {
if (marginTop != null && marginTop > 0) {
this.element.style.marginTop = marginTop + 'px'
} else {
this.element.style.marginTop = ''
}
}
if (this.props.number !== number) {
if (number) {
this.element.insertBefore(nodePool.getTextNode(number), this.element.firstChild)
} else {
if (this.props.number != null) {
const numberNode = this.element.firstChild
numberNode.remove()
nodePool.release(numberNode)
}
if (number != null) {
this.element.insertBefore(nodePool.getTextNode(number), this.element.firstChild)
}
}
this.props = props
@@ -3413,9 +3463,13 @@ class CustomGutterComponent {
}
render () {
let className = 'gutter'
if (this.props.className) {
className += ' ' + this.props.className
}
return $.div(
{
className: 'gutter',
className,
attributes: {'gutter-name': this.props.name},
style: {
display: this.props.visible ? '' : 'none'
@@ -3515,7 +3569,7 @@ class CursorsAndInputComponent {
const cursorStyle = {
height: cursorHeight,
width: pixelWidth + 'px',
width: Math.min(pixelWidth, scrollWidth - pixelLeft) + 'px',
transform: `translate(${pixelLeft}px, ${pixelTop}px)`
}
if (extraCursorStyle) Object.assign(cursorStyle, extraCursorStyle)

View File

@@ -1,3 +1,4 @@
const _ = require('underscore-plus')
const {Emitter, Disposable, CompositeDisposable} = require('event-kit')
const TextEditor = require('./text-editor')
const ScopeDescriptor = require('./scope-descriptor')
@@ -147,11 +148,11 @@ class TextEditorRegistry {
}
this.editorsWithMaintainedConfig.add(editor)
this.subscribeToSettingsForEditorScope(editor)
const grammarChangeSubscription = editor.onDidChangeGrammar(() => {
this.subscribeToSettingsForEditorScope(editor)
this.updateAndMonitorEditorSettings(editor)
const languageChangeSubscription = editor.buffer.onDidChangeLanguageMode((newLanguageMode, oldLanguageMode) => {
this.updateAndMonitorEditorSettings(editor, oldLanguageMode)
})
this.subscriptions.add(grammarChangeSubscription)
this.subscriptions.add(languageChangeSubscription)
const updateTabTypes = () => {
const configOptions = {scope: editor.getRootScopeDescriptor()}
@@ -169,8 +170,8 @@ class TextEditorRegistry {
return new Disposable(() => {
this.editorsWithMaintainedConfig.delete(editor)
tokenizeSubscription.dispose()
grammarChangeSubscription.dispose()
this.subscriptions.remove(grammarChangeSubscription)
languageChangeSubscription.dispose()
this.subscriptions.remove(languageChangeSubscription)
this.subscriptions.remove(tokenizeSubscription)
})
}
@@ -204,7 +205,7 @@ class TextEditorRegistry {
// Returns a {String} scope name, or `null` if no override has been set
// for the given editor.
getGrammarOverride (editor) {
return editor.getBuffer().getLanguageMode().grammar.scopeName
return atom.grammars.getAssignedLanguageId(editor.getBuffer())
}
// Deprecated: Remove any grammar override that has been set for the given {TextEditor}.
@@ -214,14 +215,43 @@ class TextEditorRegistry {
atom.grammars.autoAssignLanguageMode(editor.getBuffer())
}
async subscribeToSettingsForEditorScope (editor) {
async updateAndMonitorEditorSettings (editor, oldLanguageMode) {
await this.initialPackageActivationPromise
this.updateEditorSettingsForLanguageMode(editor, oldLanguageMode)
this.subscribeToSettingsForEditorScope(editor)
}
updateEditorSettingsForLanguageMode (editor, oldLanguageMode) {
const newLanguageMode = editor.buffer.getLanguageMode()
if (oldLanguageMode) {
const newSettings = this.textEditorParamsForScope(newLanguageMode.rootScopeDescriptor)
const oldSettings = this.textEditorParamsForScope(oldLanguageMode.rootScopeDescriptor)
const updatedSettings = {}
for (const [, paramName] of EDITOR_PARAMS_BY_SETTING_KEY) {
// Update the setting only if it has changed between the two language
// modes. This prevents user-modified settings in an editor (like
// 'softWrapped') from being reset when the language mode changes.
if (!_.isEqual(newSettings[paramName], oldSettings[paramName])) {
updatedSettings[paramName] = newSettings[paramName]
}
}
if (_.size(updatedSettings) > 0) {
editor.update(updatedSettings)
}
} else {
editor.update(this.textEditorParamsForScope(newLanguageMode.rootScopeDescriptor))
}
}
subscribeToSettingsForEditorScope (editor) {
if (!this.editorsWithMaintainedConfig) return
const scopeDescriptor = editor.getRootScopeDescriptor()
const scopeChain = scopeDescriptor.getScopeChain()
editor.update(this.textEditorParamsForScope(scopeDescriptor))
if (!this.scopesWithConfigSubscriptions.has(scopeChain)) {
this.scopesWithConfigSubscriptions.add(scopeChain)
const configOptions = {scope: scopeDescriptor}

View File

@@ -11,6 +11,7 @@ const Cursor = require('./cursor')
const Selection = require('./selection')
const NullGrammar = require('./null-grammar')
const TextMateLanguageMode = require('./text-mate-language-mode')
const ScopeDescriptor = require('./scope-descriptor')
const TextMateScopeSelector = require('first-mate').ScopeSelector
const GutterContainer = require('./gutter-container')
@@ -41,9 +42,10 @@ const DEFAULT_NON_WORD_CHARACTERS = "/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-…"
// then be called with all current editor instances and also when any editor is
// created in the future.
//
// ```coffee
// atom.workspace.observeTextEditors (editor) ->
// ```js
// atom.workspace.observeTextEditors(editor => {
// editor.insertText('Hello World')
// })
// ```
//
// ## Buffer vs. Screen Coordinates
@@ -105,6 +107,13 @@ class TextEditor {
}
state.assert = atomEnvironment.assert.bind(atomEnvironment)
// Semantics of the readOnly flag have changed since its introduction.
// Only respect readOnly2, which has been set with the current readOnly semantics.
delete state.readOnly
state.readOnly = state.readOnly2
delete state.readOnly2
const editor = new TextEditor(state)
if (state.registered) {
const disposable = atomEnvironment.textEditors.add(editor)
@@ -128,6 +137,7 @@ class TextEditor {
this.decorationManager = params.decorationManager
this.selectionsMarkerLayer = params.selectionsMarkerLayer
this.mini = (params.mini != null) ? params.mini : false
this.keyboardInputEnabled = (params.keyboardInputEnabled != null) ? params.keyboardInputEnabled : true
this.readOnly = (params.readOnly != null) ? params.readOnly : false
this.placeholderText = params.placeholderText
this.showLineNumbers = params.showLineNumbers
@@ -223,7 +233,7 @@ class TextEditor {
this.defaultMarkerLayer = this.displayLayer.addMarkerLayer()
if (!this.selectionsMarkerLayer) {
this.selectionsMarkerLayer = this.addMarkerLayer({maintainHistory: true, persistent: true})
this.selectionsMarkerLayer = this.addMarkerLayer({maintainHistory: true, persistent: true, role: 'selections'})
}
this.decorationManager = new DecorationManager(this)
@@ -248,6 +258,7 @@ class TextEditor {
this.gutterContainer = new GutterContainer(this)
this.lineNumberGutter = this.gutterContainer.addGutter({
name: 'line-number',
type: 'line-number',
priority: 0,
visible: params.lineNumberGutterVisible
})
@@ -414,6 +425,15 @@ class TextEditor {
}
break
case 'keyboardInputEnabled':
if (value !== this.keyboardInputEnabled) {
this.keyboardInputEnabled = value
if (this.component != null) {
this.component.scheduleUpdate()
}
}
break
case 'placeholderText':
if (value !== this.placeholderText) {
this.placeholderText = value
@@ -544,7 +564,8 @@ class TextEditor {
softWrapAtPreferredLineLength: this.softWrapAtPreferredLineLength,
preferredLineLength: this.preferredLineLength,
mini: this.mini,
readOnly: this.readOnly,
readOnly2: this.readOnly, // readOnly encompassed both readOnly and keyboardInputEnabled
keyboardInputEnabled: this.keyboardInputEnabled,
editorWidthInChars: this.editorWidthInChars,
width: this.width,
maxScreenLineLength: this.maxScreenLineLength,
@@ -986,6 +1007,12 @@ class TextEditor {
isReadOnly () { return this.readOnly }
enableKeyboardInput (enabled) {
this.update({keyboardInputEnabled: enabled})
}
isKeyboardInputEnabled () { return this.keyboardInputEnabled }
onDidChangeMini (callback) {
return this.emitter.on('did-change-mini', callback)
}
@@ -994,6 +1021,10 @@ class TextEditor {
isLineNumberGutterVisible () { return this.lineNumberGutter.isVisible() }
anyLineNumberGutterVisible () {
return this.getGutters().some(gutter => gutter.type === 'line-number' && gutter.visible)
}
onDidChangeLineNumberGutterVisible (callback) {
return this.emitter.on('did-change-line-number-gutter-visible', callback)
}
@@ -1305,7 +1336,12 @@ class TextEditor {
// Essential: Replaces the entire contents of the buffer with the given {String}.
//
// * `text` A {String} to replace with
setText (text) { return this.buffer.setText(text) }
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
setText (text, options = {}) {
if (!this.ensureWritable('setText', options)) return
return this.buffer.setText(text)
}
// Essential: Set the text in the given {Range} in buffer coordinates.
//
@@ -1313,10 +1349,12 @@ class TextEditor {
// * `text` A {String}
// * `options` (optional) {Object}
// * `normalizeLineEndings` (optional) {Boolean} (default: true)
// * `undo` (optional) {String} 'skip' will skip the undo system
// * `undo` (optional) *Deprecated* {String} 'skip' will skip the undo system. This property is deprecated. Call groupLastChanges() on the {TextBuffer} afterward instead.
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
//
// Returns the {Range} of the newly-inserted text.
setTextInBufferRange (range, text, options) {
setTextInBufferRange (range, text, options = {}) {
if (!this.ensureWritable('setTextInBufferRange', options)) return
return this.getBuffer().setTextInRange(range, text, options)
}
@@ -1325,9 +1363,9 @@ class TextEditor {
// * `text` A {String} representing the text to insert.
// * `options` (optional) See {Selection::insertText}.
//
// Returns a {Range} when the text has been inserted
// Returns a {Boolean} false when the text has not been inserted
// Returns a {Range} when the text has been inserted. Returns a {Boolean} `false` when the text has not been inserted.
insertText (text, options = {}) {
if (!this.ensureWritable('insertText', options)) return
if (!this.emitWillInsertTextEvent(text)) return false
let groupLastChanges = false
@@ -1351,20 +1389,31 @@ class TextEditor {
}
// Essential: For each selection, replace the selected text with a newline.
insertNewline (options) {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
insertNewline (options = {}) {
return this.insertText('\n', options)
}
// Essential: For each selection, if the selection is empty, delete the character
// following the cursor. Otherwise delete the selected text.
delete () {
return this.mutateSelectedText(selection => selection.delete())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
delete (options = {}) {
if (!this.ensureWritable('delete', options)) return
return this.mutateSelectedText(selection => selection.delete(options))
}
// Essential: For each selection, if the selection is empty, delete the character
// preceding the cursor. Otherwise delete the selected text.
backspace () {
return this.mutateSelectedText(selection => selection.backspace())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
backspace (options = {}) {
if (!this.ensureWritable('backspace', options)) return
return this.mutateSelectedText(selection => selection.backspace(options))
}
// Extended: Mutate the text of all the selections in a single transaction.
@@ -1385,7 +1434,12 @@ class TextEditor {
// Move lines intersecting the most recent selection or multiple selections
// up by one row in screen coordinates.
moveLineUp () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
moveLineUp (options = {}) {
if (!this.ensureWritable('moveLineUp', options)) return
const selections = this.getSelectedBufferRanges().sort((a, b) => a.compare(b))
if (selections[0].start.row === 0) return
@@ -1453,7 +1507,12 @@ class TextEditor {
// Move lines intersecting the most recent selection or multiple selections
// down by one row in screen coordinates.
moveLineDown () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
moveLineDown (options = {}) {
if (!this.ensureWritable('moveLineDown', options)) return
const selections = this.getSelectedBufferRanges()
selections.sort((a, b) => b.compare(a))
@@ -1525,7 +1584,11 @@ class TextEditor {
}
// Move any active selections one column to the left.
moveSelectionLeft () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
moveSelectionLeft (options = {}) {
if (!this.ensureWritable('moveSelectionLeft', options)) return
const selections = this.getSelectedBufferRanges()
const noSelectionAtStartOfLine = selections.every(selection => selection.start.column !== 0)
@@ -1549,7 +1612,11 @@ class TextEditor {
}
// Move any active selections one column to the right.
moveSelectionRight () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
moveSelectionRight (options = {}) {
if (!this.ensureWritable('moveSelectionRight', options)) return
const selections = this.getSelectedBufferRanges()
const noSelectionAtEndOfLine = selections.every(selection => {
return selection.end.column !== this.buffer.lineLengthForRow(selection.end.row)
@@ -1574,7 +1641,12 @@ class TextEditor {
}
}
duplicateLines () {
// Duplicate all lines containing active selections.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
duplicateLines (options = {}) {
if (!this.ensureWritable('duplicateLines', options)) return
this.transact(() => {
const selections = this.getSelectionsOrderedByBufferPosition()
const previousSelectionRanges = []
@@ -1661,7 +1733,11 @@ class TextEditor {
//
// If the selection is empty, the characters preceding and following the cursor
// are swapped. Otherwise, the selected characters are reversed.
transpose () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
transpose (options = {}) {
if (!this.ensureWritable('transpose', options)) return
this.mutateSelectedText(selection => {
if (selection.isEmpty()) {
selection.selectRight()
@@ -1679,23 +1755,35 @@ class TextEditor {
//
// For each selection, if the selection is empty, converts the containing word
// to upper case. Otherwise convert the selected text to upper case.
upperCase () {
this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toUpperCase())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
upperCase (options = {}) {
if (!this.ensureWritable('upperCase', options)) return
this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toUpperCase(options))
}
// 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 () {
this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toLowerCase())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
lowerCase (options = {}) {
if (!this.ensureWritable('lowerCase', options)) return
this.replaceSelectedText({selectWordIfEmpty: true}, text => text.toLowerCase(options))
}
// Extended: Toggle line comments for rows intersecting selections.
//
// If the current grammar doesn't support comments, does nothing.
toggleLineCommentsInSelection () {
this.mutateSelectedText(selection => selection.toggleLineComments())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
toggleLineCommentsInSelection (options = {}) {
if (!this.ensureWritable('toggleLineCommentsInSelection', options)) return
this.mutateSelectedText(selection => selection.toggleLineComments(options))
}
// Convert multiple lines to a single line.
@@ -1706,20 +1794,32 @@ class TextEditor {
//
// Joining a line means that multiple lines are converted to a single line with
// the contents of each of the original non-empty lines separated by a space.
joinLines () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
joinLines (options = {}) {
if (!this.ensureWritable('joinLines', options)) return
this.mutateSelectedText(selection => selection.joinLines())
}
// Extended: For each cursor, insert a newline at beginning the following line.
insertNewlineBelow () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
insertNewlineBelow (options = {}) {
if (!this.ensureWritable('insertNewlineBelow', options)) return
this.transact(() => {
this.moveToEndOfLine()
this.insertNewline()
this.insertNewline(options)
})
}
// Extended: For each cursor, insert a newline at the end of the preceding line.
insertNewlineAbove () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
insertNewlineAbove (options = {}) {
if (!this.ensureWritable('insertNewlineAbove', options)) return
this.transact(() => {
const bufferRow = this.getCursorBufferPosition().row
const indentLevel = this.indentationForBufferRow(bufferRow)
@@ -1727,7 +1827,7 @@ class TextEditor {
this.moveToBeginningOfLine()
this.moveLeft()
this.insertNewline()
this.insertNewline(options)
if (this.shouldAutoIndent() && (this.indentationForBufferRow(bufferRow) < indentLevel)) {
this.setIndentationForBufferRow(bufferRow, indentLevel)
@@ -1743,62 +1843,117 @@ class TextEditor {
// 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 () {
this.mutateSelectedText(selection => selection.deleteToBeginningOfWord())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToBeginningOfWord (options = {}) {
if (!this.ensureWritable('deleteToBeginningOfWord', options)) return
this.mutateSelectedText(selection => selection.deleteToBeginningOfWord(options))
}
// Extended: Similar to {::deleteToBeginningOfWord}, but deletes only back to the
// previous word boundary.
deleteToPreviousWordBoundary () {
this.mutateSelectedText(selection => selection.deleteToPreviousWordBoundary())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToPreviousWordBoundary (options = {}) {
if (!this.ensureWritable('deleteToPreviousWordBoundary', options)) return
this.mutateSelectedText(selection => selection.deleteToPreviousWordBoundary(options))
}
// Extended: Similar to {::deleteToEndOfWord}, but deletes only up to the
// next word boundary.
deleteToNextWordBoundary () {
this.mutateSelectedText(selection => selection.deleteToNextWordBoundary())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToNextWordBoundary (options = {}) {
if (!this.ensureWritable('deleteToNextWordBoundary', options)) return
this.mutateSelectedText(selection => selection.deleteToNextWordBoundary(options))
}
// Extended: For each selection, if the selection is empty, delete all characters
// of the containing subword following the cursor. Otherwise delete the selected
// text.
deleteToBeginningOfSubword () {
this.mutateSelectedText(selection => selection.deleteToBeginningOfSubword())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToBeginningOfSubword (options = {}) {
if (!this.ensureWritable('deleteToBeginningOfSubword', options)) return
this.mutateSelectedText(selection => selection.deleteToBeginningOfSubword(options))
}
// Extended: For each selection, if the selection is empty, delete all characters
// of the containing subword following the cursor. Otherwise delete the selected
// text.
deleteToEndOfSubword () {
this.mutateSelectedText(selection => selection.deleteToEndOfSubword())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToEndOfSubword (options = {}) {
if (!this.ensureWritable('deleteToEndOfSubword', options)) return
this.mutateSelectedText(selection => selection.deleteToEndOfSubword(options))
}
// 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 () {
this.mutateSelectedText(selection => selection.deleteToBeginningOfLine())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToBeginningOfLine (options = {}) {
if (!this.ensureWritable('deleteToBeginningOfLine', options)) return
this.mutateSelectedText(selection => selection.deleteToBeginningOfLine(options))
}
// 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 () {
this.mutateSelectedText(selection => selection.deleteToEndOfLine())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToEndOfLine (options = {}) {
if (!this.ensureWritable('deleteToEndOfLine', options)) return
this.mutateSelectedText(selection => selection.deleteToEndOfLine(options))
}
// 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 () {
this.mutateSelectedText(selection => selection.deleteToEndOfWord())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteToEndOfWord (options = {}) {
if (!this.ensureWritable('deleteToEndOfWord', options)) return
this.mutateSelectedText(selection => selection.deleteToEndOfWord(options))
}
// Extended: Delete all lines intersecting selections.
deleteLine () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
deleteLine (options = {}) {
if (!this.ensureWritable('deleteLine', options)) return
this.mergeSelectionsOnSameRows()
this.mutateSelectedText(selection => selection.deleteLine())
this.mutateSelectedText(selection => selection.deleteLine(options))
}
// Private: Ensure that this editor is not marked read-only before allowing a buffer modification to occur. If
// the editor is read-only, require an explicit opt-in option to proceed (`bypassReadOnly`) or throw an Error.
ensureWritable (methodName, opts) {
if (!opts.bypassReadOnly && this.isReadOnly()) {
if (atom.inDevMode() || atom.inSpecMode()) {
const e = new Error('Attempt to mutate a read-only TextEditor')
e.detail =
`Your package is attempting to call ${methodName} on an editor that has been marked read-only. ` +
'Pass {bypassReadOnly: true} to modify it anyway, or test editors with .isReadOnly() before attempting ' +
'modifications.'
throw e
}
return false
}
return true
}
/*
@@ -1806,14 +1961,22 @@ class TextEditor {
*/
// Essential: Undo the last change.
undo () {
this.avoidMergingSelections(() => this.buffer.undo())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
undo (options = {}) {
if (!this.ensureWritable('undo', options)) return
this.avoidMergingSelections(() => this.buffer.undo({selectionsMarkerLayer: this.selectionsMarkerLayer}))
this.getLastSelection().autoscroll()
}
// Essential: Redo the last change.
redo () {
this.avoidMergingSelections(() => this.buffer.redo())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor. (default: false)
redo (options = {}) {
if (!this.ensureWritable('redo', options)) return
this.avoidMergingSelections(() => this.buffer.redo({selectionsMarkerLayer: this.selectionsMarkerLayer}))
this.getLastSelection().autoscroll()
}
@@ -1830,7 +1993,13 @@ class TextEditor {
// still 'groupable', the two transactions are merged with respect to undo and redo.
// * `fn` A {Function} to call inside the transaction.
transact (groupingInterval, fn) {
return this.buffer.transact(groupingInterval, fn)
const options = {selectionsMarkerLayer: this.selectionsMarkerLayer}
if (typeof groupingInterval === 'function') {
fn = groupingInterval
} else {
options.groupingInterval = groupingInterval
}
return this.buffer.transact(options, fn)
}
// Extended: Abort an open transaction, undoing any operations performed so far
@@ -1841,7 +2010,9 @@ class TextEditor {
// with {::revertToCheckpoint} and {::groupChangesSinceCheckpoint}.
//
// Returns a checkpoint value.
createCheckpoint () { return this.buffer.createCheckpoint() }
createCheckpoint () {
return this.buffer.createCheckpoint({selectionsMarkerLayer: this.selectionsMarkerLayer})
}
// Extended: Revert the buffer to the state it was in when the given
// checkpoint was created.
@@ -1865,7 +2036,9 @@ class TextEditor {
// * `checkpoint` The checkpoint from which to group changes.
//
// Returns a {Boolean} indicating whether the operation succeeded.
groupChangesSinceCheckpoint (checkpoint) { return this.buffer.groupChangesSinceCheckpoint(checkpoint) }
groupChangesSinceCheckpoint (checkpoint) {
return this.buffer.groupChangesSinceCheckpoint(checkpoint, {selectionsMarkerLayer: this.selectionsMarkerLayer})
}
/*
Section: TextEditor Coordinates
@@ -1956,11 +2129,11 @@ class TextEditor {
//
// ## Examples
//
// ```coffee
// editor.clipBufferPosition([-1, -1]) # -> `[0, 0]`
// ```js
// editor.clipBufferPosition([-1, -1]) // -> `[0, 0]`
//
// # When the line at buffer row 2 is 10 characters long
// editor.clipBufferPosition([2, Infinity]) # -> `[2, 10]`
// // When the line at buffer row 2 is 10 characters long
// editor.clipBufferPosition([2, Infinity]) // -> `[2, 10]`
// ```
//
// * `bufferPosition` The {Point} representing the position to clip.
@@ -1985,11 +2158,11 @@ class TextEditor {
//
// ## Examples
//
// ```coffee
// editor.clipScreenPosition([-1, -1]) # -> `[0, 0]`
// ```js
// editor.clipScreenPosition([-1, -1]) // -> `[0, 0]`
//
// # When the line at screen row 2 is 10 characters long
// editor.clipScreenPosition([2, Infinity]) # -> `[2, 10]`
// // When the line at screen row 2 is 10 characters long
// editor.clipScreenPosition([2, Infinity]) // -> `[2, 10]`
// ```
//
// * `screenPosition` The {Point} representing the position to clip.
@@ -2673,7 +2846,7 @@ class TextEditor {
return this.cursors.slice()
}
// Extended: Get all {Cursors}s, ordered by their position in the buffer
// Extended: Get all {Cursor}s, ordered by their position in the buffer
// instead of the order in which they were added.
//
// Returns an {Array} of {Selection}s.
@@ -3547,13 +3720,21 @@ class TextEditor {
}
// Extended: Indent rows intersecting selections by one level.
indentSelectedRows () {
return this.mutateSelectedText(selection => selection.indentSelectedRows())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
indentSelectedRows (options = {}) {
if (!this.ensureWritable('indentSelectedRows', options)) return
return this.mutateSelectedText(selection => selection.indentSelectedRows(options))
}
// Extended: Outdent rows intersecting selections by one level.
outdentSelectedRows () {
return this.mutateSelectedText(selection => selection.outdentSelectedRows())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
outdentSelectedRows (options = {}) {
if (!this.ensureWritable('outdentSelectedRows', options)) return
return this.mutateSelectedText(selection => selection.outdentSelectedRows(options))
}
// Extended: Get the indentation level of the given line of text.
@@ -3584,13 +3765,21 @@ class TextEditor {
// Extended: Indent rows intersecting selections based on the grammar's suggested
// indent level.
autoIndentSelectedRows () {
return this.mutateSelectedText(selection => selection.autoIndentSelectedRows())
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
autoIndentSelectedRows (options = {}) {
if (!this.ensureWritable('autoIndentSelectedRows', options)) return
return this.mutateSelectedText(selection => selection.autoIndentSelectedRows(options))
}
// Indent all lines intersecting selections. See {Selection::indent} for more
// information.
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
indent (options = {}) {
if (!this.ensureWritable('indent', options)) return
if (options.autoIndent == null) options.autoIndent = this.shouldAutoIndent()
this.mutateSelectedText(selection => selection.indent(options))
}
@@ -3655,7 +3844,10 @@ class TextEditor {
//
// Returns a {ScopeDescriptor}.
scopeDescriptorForBufferPosition (bufferPosition) {
return this.buffer.getLanguageMode().scopeDescriptorForPosition(bufferPosition)
const languageMode = this.buffer.getLanguageMode()
return languageMode.scopeDescriptorForPosition
? languageMode.scopeDescriptorForPosition(bufferPosition)
: new ScopeDescriptor({scopes: ['text']})
}
// Extended: Get the range in buffer coordinates of all tokens surrounding the
@@ -3725,14 +3917,18 @@ class TextEditor {
}
// Essential: For each selection, cut the selected text.
cutSelectedText () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
cutSelectedText (options = {}) {
if (!this.ensureWritable('cutSelectedText', options)) return
let maintainClipboard = false
this.mutateSelectedText(selection => {
if (selection.isEmpty()) {
selection.selectLine()
selection.cut(maintainClipboard, true)
selection.cut(maintainClipboard, true, options.bypassReadOnly)
} else {
selection.cut(maintainClipboard, false)
selection.cut(maintainClipboard, false, options.bypassReadOnly)
}
maintainClipboard = true
})
@@ -3746,7 +3942,8 @@ class TextEditor {
// corresponding clipboard selection text.
//
// * `options` (optional) See {Selection::insertText}.
pasteText (options) {
pasteText (options = {}) {
if (!this.ensureWritable('parseText', options)) return
options = Object.assign({}, options)
let {text: clipboardText, metadata} = this.constructor.clipboard.readWithMetadata()
if (!this.emitWillInsertTextEvent(clipboardText)) return false
@@ -3787,10 +3984,14 @@ class TextEditor {
// Essential: For each selection, if the selection is empty, cut all characters
// of the containing screen line following the cursor. Otherwise cut the selected
// text.
cutToEndOfLine () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
cutToEndOfLine (options = {}) {
if (!this.ensureWritable('cutToEndOfLine', options)) return
let maintainClipboard = false
this.mutateSelectedText(selection => {
selection.cutToEndOfLine(maintainClipboard)
selection.cutToEndOfLine(maintainClipboard, options)
maintainClipboard = true
})
}
@@ -3798,10 +3999,14 @@ class TextEditor {
// Essential: For each selection, if the selection is empty, cut all characters
// of the containing buffer line following the cursor. Otherwise cut the
// selected text.
cutToEndOfBufferLine () {
//
// * `options` (optional) {Object}
// * `bypassReadOnly` (optional) {Boolean} Must be `true` to modify a read-only editor.
cutToEndOfBufferLine (options = {}) {
if (!this.ensureWritable('cutToEndOfBufferLine', options)) return
let maintainClipboard = false
this.mutateSelectedText(selection => {
selection.cutToEndOfBufferLine(maintainClipboard)
selection.cutToEndOfBufferLine(maintainClipboard, options)
maintainClipboard = true
})
}
@@ -3893,7 +4098,7 @@ class TextEditor {
// Extended: Unfold all existing folds.
unfoldAll () {
const result = this.displayLayer.destroyAllFolds()
this.scrollToCursorPosition()
if (result.length > 0) this.scrollToCursorPosition()
return result
}
@@ -4011,6 +4216,29 @@ class TextEditor {
// window. (default: -100)
// * `visible` (optional) {Boolean} specifying whether the gutter is visible
// initially after being created. (default: true)
// * `type` (optional) {String} specifying the type of gutter to create. `'decorated'`
// gutters are useful as a destination for decorations created with {Gutter::decorateMarker}.
// `'line-number'` gutters.
// * `class` (optional) {String} added to the CSS classnames of the gutter's root DOM element.
// * `labelFn` (optional) {Function} called by a `'line-number'` gutter to generate the label for each line number
// element. Should return a {String} that will be used to label the corresponding line.
// * `lineData` an {Object} containing information about each line to label.
// * `bufferRow` {Number} indicating the zero-indexed buffer index of this line.
// * `screenRow` {Number} indicating the zero-indexed screen index.
// * `foldable` {Boolean} that is `true` if a fold may be created here.
// * `softWrapped` {Boolean} if this screen row is the soft-wrapped continuation of the same buffer row.
// * `maxDigits` {Number} the maximum number of digits necessary to represent any known screen row.
// * `onMouseDown` (optional) {Function} to be called when a mousedown event is received by a line-number
// element within this `type: 'line-number'` {Gutter}. If unspecified, the default behavior is to select the
// clicked buffer row.
// * `lineData` an {Object} containing information about the line that's being clicked.
// * `bufferRow` {Number} of the originating line element
// * `screenRow` {Number}
// * `onMouseMove` (optional) {Function} to be called when a mousemove event occurs on a line-number element within
// within this `type: 'line-number'` {Gutter}.
// * `lineData` an {Object} containing information about the line that's being clicked.
// * `bufferRow` {Number} of the originating line element
// * `screenRow` {Number}
//
// Returns the newly-created {Gutter}.
addGutter (options) {
@@ -4615,7 +4843,7 @@ class TextEditor {
let endRow = bufferRow
const rowCount = this.getLineCount()
while (endRow < rowCount) {
while (endRow + 1 < rowCount) {
if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(endRow + 1))) break
if (languageMode.isRowCommented(endRow + 1) !== isCommented) break
endRow++

View File

@@ -7,6 +7,7 @@ const ScopeDescriptor = require('./scope-descriptor')
const NullGrammar = require('./null-grammar')
const {OnigRegExp} = require('oniguruma')
const {toFirstMateScopeId, fromFirstMateScopeId} = require('./first-mate-helpers')
const {selectorMatchesAnyScope} = require('./selectors')
const NON_WHITESPACE_REGEX = /\S/
@@ -235,15 +236,18 @@ class TextMateLanguageMode {
return this.buffer.getTextInRange([[0, 0], [10, 0]])
}
hasTokenForSelector (selector) {
updateForInjection (grammar) {
if (!grammar.injectionSelector) return
for (const tokenizedLine of this.tokenizedLines) {
if (tokenizedLine) {
for (let token of tokenizedLine.tokens) {
if (selector.matches(token.scopes)) return true
if (grammar.injectionSelector.matches(token.scopes)) {
this.retokenizeLines()
return
}
}
}
}
return false
}
retokenizeLines () {
@@ -605,7 +609,7 @@ class TextMateLanguageMode {
for (let row = point.row - 1; row >= 0; row--) {
const endRow = this.endRowForFoldAtRow(row, tabLength)
if (endRow != null && endRow > point.row) {
if (endRow != null && endRow >= point.row) {
return Range(Point(row, Infinity), Point(endRow, Infinity))
}
}
@@ -723,14 +727,6 @@ class TextMateLanguageMode {
TextMateLanguageMode.prototype.chunkSize = 50
function selectorMatchesAnyScope (selector, scopes) {
const targetClasses = selector.replace(/^\./, '').split('.')
return scopes.some((scope) => {
const scopeClasses = scope.split('.')
return _.isSubset(targetClasses, scopeClasses)
})
}
class TextMateHighlightIterator {
constructor (languageMode) {
this.languageMode = languageMode

View File

@@ -103,7 +103,7 @@ class ThemeManager {
warnForNonExistentThemes () {
let themeNames = this.config.get('core.themes') || []
if (!_.isArray(themeNames)) { themeNames = [themeNames] }
if (!Array.isArray(themeNames)) { themeNames = [themeNames] }
for (let themeName of themeNames) {
if (!themeName || (typeof themeName !== 'string') || !this.packageManager.resolvePackagePath(themeName)) {
console.warn(`Enabled theme '${themeName}' is not installed.`)
@@ -116,7 +116,7 @@ class ThemeManager {
// Returns an array of theme names in the order that they should be activated.
getEnabledThemeNames () {
let themeNames = this.config.get('core.themes') || []
if (!_.isArray(themeNames)) { themeNames = [themeNames] }
if (!Array.isArray(themeNames)) { themeNames = [themeNames] }
themeNames = themeNames.filter((themeName) =>
(typeof themeName === 'string') && this.packageManager.resolvePackagePath(themeName)
)
@@ -138,7 +138,7 @@ class ThemeManager {
if (themeNames.length === 0) {
themeNames = ['one-dark-syntax', 'one-dark-ui']
} else if (themeNames.length === 1) {
if (_.endsWith(themeNames[0], '-ui')) {
if (themeNames[0].endsWith('-ui')) {
themeNames.unshift('one-dark-syntax')
} else {
themeNames.push('one-dark-ui')

View File

@@ -153,9 +153,11 @@ class TooltipManager {
}
window.addEventListener('resize', hideTooltip)
window.addEventListener('keydown', hideTooltip)
const disposable = new Disposable(() => {
window.removeEventListener('resize', hideTooltip)
window.removeEventListener('keydown', hideTooltip)
hideTooltip()
tooltip.destroy()

View File

@@ -6,12 +6,17 @@ module.exports =
class TreeSitterGrammar {
constructor (registry, filePath, params) {
this.registry = registry
this.id = params.id
this.name = params.name
this.legacyScopeName = params.legacyScopeName
if (params.contentRegExp) this.contentRegExp = new RegExp(params.contentRegExp)
this.scopeName = params.scopeName
// TODO - Remove the `RegExp` spelling and only support `Regex`, once all of the existing
// Tree-sitter grammars are updated to spell it `Regex`.
this.contentRegex = buildRegex(params.contentRegex || params.contentRegExp)
this.injectionRegex = buildRegex(params.injectionRegex || params.injectionRegExp)
this.firstLineRegex = buildRegex(params.firstLineRegex)
this.folds = params.folds || []
this.folds.forEach(normalizeFoldSpecification)
this.commentStrings = {
commentStartString: params.comments && params.comments.start,
@@ -20,14 +25,22 @@ class TreeSitterGrammar {
const scopeSelectors = {}
for (const key in params.scopes || {}) {
scopeSelectors[key] = params.scopes[key]
.split('.')
.map(s => `syntax--${s}`)
.join(' ')
const classes = toSyntaxClasses(params.scopes[key])
const selectors = key.split(/,\s+/)
for (let selector of selectors) {
selector = selector.trim()
if (!selector) continue
if (scopeSelectors[selector]) {
scopeSelectors[selector] = [].concat(scopeSelectors[selector], classes)
} else {
scopeSelectors[selector] = classes
}
}
}
this.scopeMap = new SyntaxScopeMap(scopeSelectors)
this.fileTypes = params.fileTypes
this.fileTypes = params.fileTypes || []
this.injectionPoints = params.injectionPoints || []
// TODO - When we upgrade to a new enough version of node, use `require.resolve`
// with the new `paths` option instead of this private API.
@@ -39,11 +52,16 @@ class TreeSitterGrammar {
this.languageModule = require(languageModulePath)
this.scopesById = new Map()
this.conciseScopesById = new Map()
this.idsByScope = {}
this.nextScopeId = 256 + 1
this.registration = null
}
inspect () {
return `TreeSitterGrammar {scopeName: ${this.scopeName}}`
}
idForScope (scope) {
let id = this.idsByScope[scope]
if (!id) {
@@ -58,8 +76,15 @@ class TreeSitterGrammar {
return this.scopesById.get(id)
}
get scopeName () {
return this.id
scopeNameForScopeId (id) {
let result = this.conciseScopesById.get(id)
if (!result) {
result = this.scopesById.get(id)
.slice('syntax--'.length)
.replace(/ syntax--/g, '.')
this.conciseScopesById.set(id, result)
}
return result
}
activate () {
@@ -70,3 +95,56 @@ class TreeSitterGrammar {
if (this.registration) this.registration.dispose()
}
}
const toSyntaxClasses = scopes =>
typeof scopes === 'string'
? scopes
.split('.')
.map(s => `syntax--${s}`)
.join(' ')
: Array.isArray(scopes)
? scopes.map(toSyntaxClasses)
: scopes.match
? {match: new RegExp(scopes.match), scopes: toSyntaxClasses(scopes.scopes)}
: Object.assign({}, scopes, {scopes: toSyntaxClasses(scopes.scopes)})
const NODE_NAME_REGEX = /[\w_]+/
function matcherForSpec (spec) {
if (typeof spec === 'string') {
if (spec[0] === '"' && spec[spec.length - 1] === '"') {
return {
type: spec.substr(1, spec.length - 2),
named: false
}
}
if (!NODE_NAME_REGEX.test(spec)) {
return {type: spec, named: false}
}
return {type: spec, named: true}
}
return spec
}
function normalizeFoldSpecification (spec) {
if (spec.type) {
if (Array.isArray(spec.type)) {
spec.matchers = spec.type.map(matcherForSpec)
} else {
spec.matchers = [matcherForSpec(spec.type)]
}
}
if (spec.start) normalizeFoldSpecification(spec.start)
if (spec.end) normalizeFoldSpecification(spec.end)
}
function buildRegex (value) {
// Allow multiple alternatives to be specified via an array, for
// readability of the grammar file
if (Array.isArray(value)) value = value.map(_ => `(${_})`).join('|')
if (typeof value === 'string') return new RegExp(value)
return null
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,5 @@
/** @babel */
import fs from 'fs'
import childProcess from 'child_process'
const fs = require('fs')
const childProcess = require('child_process')
const ENVIRONMENT_VARIABLES_TO_PRESERVE = new Set([
'NODE_ENV',
@@ -20,7 +18,7 @@ async function updateProcessEnv (launchEnv) {
if (launchEnv) {
if (shouldGetEnvFromShell(launchEnv)) {
envToAssign = await getEnvFromShell(launchEnv)
} else if (launchEnv.PWD) {
} else if (launchEnv.PWD || launchEnv.PROMPT || launchEnv.PSModulePath) {
envToAssign = launchEnv
}
}
@@ -120,4 +118,4 @@ async function getEnvFromShell (env) {
return result
}
export default { updateProcessEnv, shouldGetEnvFromShell }
module.exports = {updateProcessEnv, shouldGetEnvFromShell}

View File

@@ -56,10 +56,10 @@ class WorkspaceElement extends HTMLElement {
}
updateGlobalTextEditorStyleSheet () {
const styleSheetSource = `atom-text-editor {
font-size: ${this.config.get('editor.fontSize')}px;
font-family: ${this.config.get('editor.fontFamily')};
line-height: ${this.config.get('editor.lineHeight')};
const styleSheetSource = `atom-workspace {
--editor-font-size: ${this.config.get('editor.fontSize')}px;
--editor-font-family: ${this.config.get('editor.fontFamily')};
--editor-line-height: ${this.config.get('editor.lineHeight')};
}`
this.styleManager.addStyleSheet(styleSheetSource, {sourcePath: 'global-text-editor-styles', priority: -1})
}
@@ -92,7 +92,13 @@ class WorkspaceElement extends HTMLElement {
window.removeEventListener('dragstart', this.handleDragStart)
window.removeEventListener('dragend', this.handleDragEnd, true)
window.removeEventListener('drop', this.handleDrop, true)
})
}),
...[this.model.getLeftDock(), this.model.getRightDock(), this.model.getBottomDock()]
.map(dock => dock.onDidChangeHovered(hovered => {
if (hovered) this.hoveredDock = dock
else if (dock === this.hoveredDock) this.hoveredDock = null
this.checkCleanupDockHoverEvents()
}))
)
this.initializeContent()
this.observeScrollbarStyle()
@@ -104,6 +110,7 @@ class WorkspaceElement extends HTMLElement {
this.addEventListener('mousewheel', this.handleMousewheel.bind(this), true)
window.addEventListener('dragstart', this.handleDragStart)
window.addEventListener('mousemove', this.handleEdgesMouseMove)
this.panelContainers = {
top: this.model.panelContainers.top.getElement(),
@@ -132,6 +139,10 @@ class WorkspaceElement extends HTMLElement {
return this
}
destroy () {
this.subscriptions.dispose()
}
getModel () { return this.model }
handleDragStart (event) {
@@ -169,7 +180,6 @@ class WorkspaceElement extends HTMLElement {
// being hovered.
this.cursorInCenter = false
this.updateHoveredDock({x: event.pageX, y: event.pageY})
window.addEventListener('mousemove', this.handleEdgesMouseMove)
window.addEventListener('dragend', this.handleDockDragEnd)
}
@@ -182,24 +192,17 @@ class WorkspaceElement extends HTMLElement {
}
updateHoveredDock (mousePosition) {
this.hoveredDock = null
for (let location in this.model.paneContainers) {
if (location !== 'center') {
const dock = this.model.paneContainers[location]
if (!this.hoveredDock && dock.pointWithinHoverArea(mousePosition)) {
this.hoveredDock = dock
dock.setHovered(true)
} else {
dock.setHovered(false)
}
}
}
this.checkCleanupDockHoverEvents()
// If we haven't left the currently hovered dock, don't change anything.
if (this.hoveredDock && this.hoveredDock.pointWithinHoverArea(mousePosition, true)) return
const docks = [this.model.getLeftDock(), this.model.getRightDock(), this.model.getBottomDock()]
const nextHoveredDock =
docks.find(dock => dock !== this.hoveredDock && dock.pointWithinHoverArea(mousePosition))
docks.forEach(dock => { dock.setHovered(dock === nextHoveredDock) })
}
checkCleanupDockHoverEvents () {
if (this.cursorInCenter && !this.hoveredDock) {
window.removeEventListener('mousemove', this.handleEdgesMouseMove)
window.removeEventListener('dragend', this.handleDockDragEnd)
}
}
@@ -307,7 +310,7 @@ class WorkspaceElement extends HTMLElement {
}
}
runPackageSpecs () {
runPackageSpecs (options = {}) {
const activePaneItem = this.model.getActivePaneItem()
const activePath = activePaneItem && typeof activePaneItem.getPath === 'function' ? activePaneItem.getPath() : null
let projectPath
@@ -323,7 +326,7 @@ class WorkspaceElement extends HTMLElement {
specPath = testPath
}
ipcRenderer.send('run-package-specs', specPath)
ipcRenderer.send('run-package-specs', specPath, options)
}
}

View File

@@ -1,5 +1,3 @@
'use babel'
const _ = require('underscore-plus')
const url = require('url')
const path = require('path')
@@ -227,6 +225,8 @@ module.exports = class Workspace extends Model {
modal: new PanelContainer({viewRegistry: this.viewRegistry, location: 'modal'})
}
this.incoming = new Map()
this.subscribeToEvents()
}
@@ -310,7 +310,10 @@ module.exports = class Workspace extends Model {
this.originalFontSize = null
this.openers = []
this.destroyedItemURIs = []
this.element = null
if (this.element) {
this.element.destroy()
this.element = null
}
this.consumeServices(this.packageManager)
}
@@ -494,14 +497,22 @@ module.exports = class Workspace extends Model {
if (item instanceof TextEditor) {
const subscriptions = new CompositeDisposable(
this.textEditorRegistry.add(item),
this.textEditorRegistry.maintainConfig(item),
item.observeGrammar(this.handleGrammarUsed.bind(this))
this.textEditorRegistry.maintainConfig(item)
)
if (!this.project.findBufferForId(item.buffer.id)) {
this.project.addBuffer(item.buffer)
}
item.onDidDestroy(() => { subscriptions.dispose() })
this.emitter.emit('did-add-text-editor', {textEditor: item, pane, index})
// It's important to call handleGrammarUsed after emitting the did-add event:
// if we activate a package between adding the editor to the registry and emitting
// the package may receive the editor twice from `observeTextEditors`.
// (Note that the item can be destroyed by an `observeTextEditors` handler.)
if (!item.isDestroyed()) {
subscriptions.add(
item.observeGrammar(this.handleGrammarUsed.bind(this))
)
}
}
})
}
@@ -920,133 +931,150 @@ module.exports = class Workspace extends Model {
if (typeof item.getURI === 'function') uri = item.getURI()
}
if (!atom.config.get('core.allowPendingPaneItems')) {
options.pending = false
}
// Avoid adding URLs as recent documents to work-around this Spotlight crash:
// https://github.com/atom/atom/issues/10071
if (uri && (!url.parse(uri).protocol || process.platform === 'win32')) {
this.applicationDelegate.addRecentDocument(uri)
}
let pane, itemExistsInWorkspace
// Try to find an existing item in the workspace.
if (item || uri) {
if (options.pane) {
pane = options.pane
} else if (options.searchAllPanes) {
pane = item ? this.paneForItem(item) : this.paneForURI(uri)
let resolveItem = () => {}
if (uri) {
const incomingItem = this.incoming.get(uri)
if (!incomingItem) {
this.incoming.set(uri, new Promise(resolve => { resolveItem = resolve }))
} else {
// If an item with the given URI is already in the workspace, assume
// that item's pane container is the preferred location for that URI.
let container
if (uri) container = this.paneContainerForURI(uri)
if (!container) container = this.getActivePaneContainer()
await incomingItem
}
}
// The `split` option affects where we search for the item.
pane = container.getActivePane()
switch (options.split) {
case 'left':
pane = pane.findLeftmostSibling()
break
case 'right':
pane = pane.findRightmostSibling()
break
case 'up':
pane = pane.findTopmostSibling()
break
case 'down':
pane = pane.findBottommostSibling()
break
}
try {
if (!atom.config.get('core.allowPendingPaneItems')) {
options.pending = false
}
if (pane) {
if (item) {
itemExistsInWorkspace = pane.getItems().includes(item)
// Avoid adding URLs as recent documents to work-around this Spotlight crash:
// https://github.com/atom/atom/issues/10071
if (uri && (!url.parse(uri).protocol || process.platform === 'win32')) {
this.applicationDelegate.addRecentDocument(uri)
}
let pane, itemExistsInWorkspace
// Try to find an existing item in the workspace.
if (item || uri) {
if (options.pane) {
pane = options.pane
} else if (options.searchAllPanes) {
pane = item ? this.paneForItem(item) : this.paneForURI(uri)
} else {
item = pane.itemForURI(uri)
itemExistsInWorkspace = item != null
// If an item with the given URI is already in the workspace, assume
// that item's pane container is the preferred location for that URI.
let container
if (uri) container = this.paneContainerForURI(uri)
if (!container) container = this.getActivePaneContainer()
// The `split` option affects where we search for the item.
pane = container.getActivePane()
switch (options.split) {
case 'left':
pane = pane.findLeftmostSibling()
break
case 'right':
pane = pane.findRightmostSibling()
break
case 'up':
pane = pane.findTopmostSibling()
break
case 'down':
pane = pane.findBottommostSibling()
break
}
}
if (pane) {
if (item) {
itemExistsInWorkspace = pane.getItems().includes(item)
} else {
item = pane.itemForURI(uri)
itemExistsInWorkspace = item != null
}
}
}
}
// If we already have an item at this stage, we won't need to do an async
// lookup of the URI, so we yield the event loop to ensure this method
// is consistently asynchronous.
if (item) await Promise.resolve()
// If we already have an item at this stage, we won't need to do an async
// lookup of the URI, so we yield the event loop to ensure this method
// is consistently asynchronous.
if (item) await Promise.resolve()
if (!itemExistsInWorkspace) {
item = item || await this.createItemForURI(uri, options)
if (!item) return
if (!itemExistsInWorkspace) {
item = item || await this.createItemForURI(uri, options)
if (!item) return
if (options.pane) {
pane = options.pane
if (options.pane) {
pane = options.pane
} else {
let location = options.location
if (!location && !options.split && uri && this.enablePersistence) {
location = await this.itemLocationStore.load(uri)
}
if (!location && typeof item.getDefaultLocation === 'function') {
location = item.getDefaultLocation()
}
const allowedLocations = typeof item.getAllowedLocations === 'function' ? item.getAllowedLocations() : ALL_LOCATIONS
location = allowedLocations.includes(location) ? location : allowedLocations[0]
const container = this.paneContainers[location] || this.getCenter()
pane = container.getActivePane()
switch (options.split) {
case 'left':
pane = pane.findLeftmostSibling()
break
case 'right':
pane = pane.findOrCreateRightmostSibling()
break
case 'up':
pane = pane.findTopmostSibling()
break
case 'down':
pane = pane.findOrCreateBottommostSibling()
break
}
}
}
if (!options.pending && (pane.getPendingItem() === item)) {
pane.clearPendingItem()
}
this.itemOpened(item)
if (options.activateItem === false) {
pane.addItem(item, {pending: options.pending})
} else {
let location = options.location
if (!location && !options.split && uri && this.enablePersistence) {
location = await this.itemLocationStore.load(uri)
}
if (!location && typeof item.getDefaultLocation === 'function') {
location = item.getDefaultLocation()
}
pane.activateItem(item, {pending: options.pending})
}
const allowedLocations = typeof item.getAllowedLocations === 'function' ? item.getAllowedLocations() : ALL_LOCATIONS
location = allowedLocations.includes(location) ? location : allowedLocations[0]
if (options.activatePane !== false) {
pane.activate()
}
const container = this.paneContainers[location] || this.getCenter()
pane = container.getActivePane()
switch (options.split) {
case 'left':
pane = pane.findLeftmostSibling()
break
case 'right':
pane = pane.findOrCreateRightmostSibling()
break
case 'up':
pane = pane.findTopmostSibling()
break
case 'down':
pane = pane.findOrCreateBottommostSibling()
break
let initialColumn = 0
let initialLine = 0
if (!Number.isNaN(options.initialLine)) {
initialLine = options.initialLine
}
if (!Number.isNaN(options.initialColumn)) {
initialColumn = options.initialColumn
}
if (initialLine >= 0 || initialColumn >= 0) {
if (typeof item.setCursorBufferPosition === 'function') {
item.setCursorBufferPosition([initialLine, initialColumn])
}
}
}
if (!options.pending && (pane.getPendingItem() === item)) {
pane.clearPendingItem()
}
this.itemOpened(item)
if (options.activateItem === false) {
pane.addItem(item, {pending: options.pending})
} else {
pane.activateItem(item, {pending: options.pending})
}
if (options.activatePane !== false) {
pane.activate()
}
let initialColumn = 0
let initialLine = 0
if (!Number.isNaN(options.initialLine)) {
initialLine = options.initialLine
}
if (!Number.isNaN(options.initialColumn)) {
initialColumn = options.initialColumn
}
if (initialLine >= 0 || initialColumn >= 0) {
if (typeof item.setCursorBufferPosition === 'function') {
item.setCursorBufferPosition([initialLine, initialColumn])
const index = pane.getActiveItemIndex()
this.emitter.emit('did-open', {uri, pane, item, index})
if (uri) {
this.incoming.delete(uri)
}
} finally {
resolveItem()
}
const index = pane.getActiveItemIndex()
this.emitter.emit('did-open', {uri, pane, item, index})
return item
}
@@ -1216,42 +1244,32 @@ module.exports = class Workspace extends Model {
const fileSize = fs.getSizeSync(filePath)
let [resolveConfirmFileOpenPromise, rejectConfirmFileOpenPromise] = []
const confirmFileOpenPromise = new Promise((resolve, reject) => {
resolveConfirmFileOpenPromise = resolve
rejectConfirmFileOpenPromise = reject
})
if (fileSize >= (this.config.get('core.warnOnLargeFileLimit') * 1048576)) { // 40MB by default
this.applicationDelegate.confirm({
message: 'Atom will be unresponsive during the loading of very large files.',
detail: 'Do you still want to load this file?',
buttons: ['Proceed', 'Cancel']
}, response => {
if (response === 1) {
rejectConfirmFileOpenPromise()
} else {
resolveConfirmFileOpenPromise()
}
await new Promise((resolve, reject) => {
this.applicationDelegate.confirm({
message: 'Atom will be unresponsive during the loading of very large files.',
detail: 'Do you still want to load this file?',
buttons: ['Proceed', 'Cancel']
}, response => {
if (response === 1) {
const error = new Error()
error.code = 'CANCELLED'
reject(error)
} else {
resolve()
}
})
})
} else {
resolveConfirmFileOpenPromise()
}
try {
await confirmFileOpenPromise
const buffer = await this.project.bufferForPath(filePath, options)
return this.textEditorRegistry.build(Object.assign({buffer, autoHeight: false}, options))
} catch (e) {
const error = new Error()
error.code = 'CANCELLED'
throw error
}
const buffer = await this.project.bufferForPath(filePath, options)
return this.textEditorRegistry.build(Object.assign({buffer, autoHeight: false}, options))
}
handleGrammarUsed (grammar) {
if (grammar == null) { return }
return this.packageManager.triggerActivationHook(`${grammar.packageName}:grammar-used`)
this.packageManager.triggerActivationHook(`${grammar.scopeName}:root-scope-used`)
this.packageManager.triggerActivationHook(`${grammar.packageName}:grammar-used`)
}
// Public: Returns a {Boolean} that is `true` if `object` is a `TextEditor`.
@@ -1570,6 +1588,7 @@ module.exports = class Workspace extends Model {
if (this.activeItemSubscriptions != null) {
this.activeItemSubscriptions.dispose()
}
if (this.element) this.element.destroy()
}
/*