Atom is a text editor hackable to its core. It allows you to modify and add functionality to better fit your needs.
Yeah, OK, but what does it mean to be a hackable editor?
Everything in Atom is a package and every feature comes in the form of a package. This makes it a highly modular text editor. It is so modular that anyone can write packages for it.
Atom has a bunch of people contributing to it on github, so don’t hesitate to lend a hand!
How to Install a Package?
There are two ways to install packages for Atom,
- Enter
apm install package-name
on your terminal. Obviously, the Atom package manager,apm
, must be installed (you can enterapm
to verify installation). - Open Atom, go to
edit > preferences > install
and search for the package you wish to install.
Both of these methods will download your packages to the default directory (e.g., ~/.atom/packages
on linux).
Package States
A package can be active
, loaded
, unloaded
, or inactive
. Internally, the PackageManager
class (in package-manager.js) manages these states.
Loaded Packages
When a package is loaded
it means that Atom knows it is installed and that it will be either activated
or deactivated
. First, Atom will get every available package by saving the required paths (i.e., folders containing the packages) in an array and use that to create another array containing the packages found on those directories. Loading a package causes Atom to read and parse the package's metadata and resources such as keymaps, menus, stylesheets, etc.
loadPackages () {
// Ensure atom exports is already in the require cache so the load time
// of the first package isn't skewed by being the first to require atom
require('../exports/atom')
const disabledPackageNames = new Set(this.config.get('core.disabledPackages'))
this.config.transact(() => {
for (const pack of this.getAvailablePackages()) {
this.loadAvailablePackage(pack, disabledPackageNames)
}
})
this.initialPackagesLoaded = true
this.emitter.emit('did-load-initial-packages')
}
getAvailablePackages () {
const packages = []
const packagesByName = new Set()
for (const packageDirPath of this.packageDirPaths) {
if (fs.isDirectorySync(packageDirPath)) {
for (let packagePath of fs.readdirSync(packageDirPath)) {
packagePath = path.join(packageDirPath, packagePath)
const packageName = path.basename(packagePath)
if (!packageName.startsWith('.') && !packagesByName.has(packageName) && fs.isDirectorySync(packagePath)) {
packages.push({
name: packageName,
path: packagePath,
isBundled: false
})
packagesByName.add(packageName)
}
}
}
}
for (const packageName in this.packageDependencies) {
if (!packagesByName.has(packageName)) {
packages.push({
name: packageName,
path: path.join(this.resourcePath, 'node_modules', packageName),
isBundled: true
})
}
}
return packages.sort((a, b) => a.name.localeCompare(b.name))
}
Unloaded Packages
Unloading a package removes it completely from the PackageManager. Here Atom will look for that package in the loadedPackages
list and remove it.
unloadPackages () {
_.keys(this.loadedPackages).forEach(name => this.unloadPackage(name))
}
unloadPackage (name) {
if (this.isPackageActive(name)) {
throw new Error(`Tried to unload active package '${name}'`)
}
const pack = this.getLoadedPackage(name)
if (pack) {
delete this.loadedPackages[pack.name]
this.emitter.emit('did-unload-package', pack)
} else {
throw new Error(`No loaded package for name '${name}'`)
}
}
Active Packages
When a package is activated the activate()
method on the PackageManager is called. It gets every loaded package and tries to call activate()
on the package's main module.
This function skips every package that was disabled by the user.
activatePackages (packages) {
const promises = []
this.config.transactAsync(() => {
for (const pack of packages) {
const promise = this.activatePackage(pack.name)
if (!pack.activationShouldBeDeferred()) {
promises.push(promise)
}
}
return Promise.all(promises)
})
this.observeDisabledPackages()
this.observePackagesWithKeymapsDisabled()
return promises
}
Inactive Packages
Deactivating a package unregisters the package's resources and calls deactivate()
on the package's main module.
// Deactivate all packages
async deactivatePackages () {
await this.config.transactAsync(() =>
Promise.all(this.getLoadedPackages().map(pack => this.deactivatePackage(pack.name, true)))
)
this.unobserveDisabledPackages()
this.unobservePackagesWithKeymapsDisabled()
}
// Deactivate the package with the given name
async deactivatePackage (name, suppressSerialization) {
const pack = this.getLoadedPackage(name)
if (pack == null) {
return
}
if (!suppressSerialization && this.isPackageActive(pack.name)) {
this.serializePackage(pack)
}
const deactivationResult = pack.deactivate()
if (deactivationResult && typeof deactivationResult.then === 'function') {
await deactivationResult
}
delete this.activePackages[pack.name]
delete this.activatingPackages[pack.name]
this.emitter.emit('did-deactivate-package', pack)
}
Turns out Atom is constantly keeping an eye on its packages folder, and whenever it sees a change a callback is executed.
The PathWatcher
class is defined in path-watcher.js
and it is used to manage a subscription to file system events that occur beneath a root directory.
The flow works this way,
- first you install a package by adding it to the packages folder,
- the PathWatcher detects the change and returns the according callback,
- then the PackageManager decides what to do with the new package.
Wish to Contribute?
As I mentioned before, everyone is welcomed to contribute! You can also create your own packages. I will not go into details about it here since they have done an amazing job at that already. Just head to their site and follow their tutorial on How to Hack Atom.