mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
Merge branch 'master' into wl-rm-safe-clipboard
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/** @babel */
|
||||
|
||||
const fs = require('fs-plus')
|
||||
const path = require('path')
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
145
src/config-file.js
Normal 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()
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
1353
src/config.coffee
1353
src/config.coffee
File diff suppressed because it is too large
Load Diff
1484
src/config.js
Normal file
1484
src/config.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'}`
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
314
src/dock.js
314
src/dock.js
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
//
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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
21
src/item-registry.js
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}\"`
|
||||
|
||||
@@ -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
132
src/menu-helpers.js
Normal 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
|
||||
}
|
||||
@@ -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
186
src/menu-sort-helpers.js
Normal 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}
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/** @babel */
|
||||
|
||||
const path = require('path')
|
||||
|
||||
// Private: re-join the segments split from an absolute path to form another absolute path.
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
40
src/pane-container-element.js
Normal file
40
src/pane-container-element.js
Normal 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
|
||||
})
|
||||
@@ -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()
|
||||
|
||||
@@ -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
218
src/pane-element.js
Normal 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
|
||||
})
|
||||
26
src/pane.js
26
src/pane.js
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
105
src/project.js
105
src/project.js
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
80
src/scope-descriptor.js
Normal 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
|
||||
}
|
||||
}
|
||||
188
src/selection.js
188
src/selection.js
@@ -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
38
src/selectors.js
Normal 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))
|
||||
}
|
||||
@@ -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
49
src/storage-folder.js
Normal 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
9
src/test.ejs
Normal file
@@ -0,0 +1,9 @@
|
||||
<html>
|
||||
|
||||
<% if something() { %>
|
||||
<div class=foo>
|
||||
<%= html `ok how about <span>this</span>` %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
</html>
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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++
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
303
src/workspace.js
303
src/workspace.js
@@ -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()
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user