mirror of
https://github.com/atom/atom.git
synced 2026-01-12 16:38:20 -05:00
A `TypeError` raised when a certain file, or lack thereof, causes Atom to open improperly, without the side pane appearing. To fix this, users must clear their Atom window state, then reopen the program. Add a notification when this error occurs with information on how to resolve it. Co-authored-by: Sadick <sadickjunior@gmail.com> Co-authored-by: Amin Yahyaabadi <aminyahyaabadi74@gmail.com>
877 lines
27 KiB
JavaScript
877 lines
27 KiB
JavaScript
const path = require('path');
|
|
|
|
const _ = require('underscore-plus');
|
|
const fs = require('fs-plus');
|
|
const { Emitter, Disposable, CompositeDisposable } = require('event-kit');
|
|
const TextBuffer = require('text-buffer');
|
|
const { watchPath } = require('./path-watcher');
|
|
|
|
const DefaultDirectoryProvider = require('./default-directory-provider');
|
|
const Model = require('./model');
|
|
const GitRepositoryProvider = require('./git-repository-provider');
|
|
|
|
// Extended: Represents a project that's opened in Atom.
|
|
//
|
|
// An instance of this class is always available as the `atom.project` global.
|
|
module.exports = class Project extends Model {
|
|
/*
|
|
Section: Construction and Destruction
|
|
*/
|
|
|
|
constructor({
|
|
notificationManager,
|
|
packageManager,
|
|
config,
|
|
applicationDelegate,
|
|
grammarRegistry
|
|
}) {
|
|
super();
|
|
this.notificationManager = notificationManager;
|
|
this.applicationDelegate = applicationDelegate;
|
|
this.grammarRegistry = grammarRegistry;
|
|
|
|
this.emitter = new Emitter();
|
|
this.buffers = [];
|
|
this.rootDirectories = [];
|
|
this.repositories = [];
|
|
this.directoryProviders = [];
|
|
this.defaultDirectoryProvider = new DefaultDirectoryProvider();
|
|
this.repositoryPromisesByPath = new Map();
|
|
this.repositoryProviders = [new GitRepositoryProvider(this, config)];
|
|
this.loadPromisesByPath = {};
|
|
this.watcherPromisesByPath = {};
|
|
this.retiredBufferIDs = new Set();
|
|
this.retiredBufferPaths = new Set();
|
|
this.subscriptions = new CompositeDisposable();
|
|
this.consumeServices(packageManager);
|
|
}
|
|
|
|
destroyed() {
|
|
for (let buffer of this.buffers.slice()) {
|
|
buffer.destroy();
|
|
}
|
|
for (let repository of this.repositories.slice()) {
|
|
if (repository != null) repository.destroy();
|
|
}
|
|
for (let path in this.watcherPromisesByPath) {
|
|
this.watcherPromisesByPath[path].then(watcher => {
|
|
watcher.dispose();
|
|
});
|
|
}
|
|
this.rootDirectories = [];
|
|
this.repositories = [];
|
|
}
|
|
|
|
reset(packageManager) {
|
|
this.emitter.dispose();
|
|
this.emitter = new Emitter();
|
|
|
|
this.subscriptions.dispose();
|
|
this.subscriptions = new CompositeDisposable();
|
|
|
|
for (let buffer of this.buffers) {
|
|
if (buffer != null) buffer.destroy();
|
|
}
|
|
this.buffers = [];
|
|
this.setPaths([]);
|
|
this.loadPromisesByPath = {};
|
|
this.retiredBufferIDs = new Set();
|
|
this.retiredBufferPaths = new Set();
|
|
this.consumeServices(packageManager);
|
|
}
|
|
|
|
destroyUnretainedBuffers() {
|
|
for (let buffer of this.getBuffers()) {
|
|
if (!buffer.isRetained()) buffer.destroy();
|
|
}
|
|
}
|
|
|
|
// 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
|
|
*/
|
|
|
|
deserialize(state) {
|
|
this.retiredBufferIDs = new Set();
|
|
this.retiredBufferPaths = new Set();
|
|
|
|
const handleBufferState = bufferState => {
|
|
if (bufferState.shouldDestroyOnFileDelete == null) {
|
|
bufferState.shouldDestroyOnFileDelete = () =>
|
|
atom.config.get('core.closeDeletedFileTabs');
|
|
}
|
|
|
|
// Use a little guilty knowledge of the way TextBuffers are serialized.
|
|
// This allows TextBuffers that have never been saved (but have filePaths) to be deserialized, but prevents
|
|
// TextBuffers backed by files that have been deleted from being saved.
|
|
bufferState.mustExist = bufferState.digestWhenLastPersisted !== false;
|
|
|
|
return TextBuffer.deserialize(bufferState).catch(_ => {
|
|
this.retiredBufferIDs.add(bufferState.id);
|
|
this.retiredBufferPaths.add(bufferState.filePath);
|
|
return null;
|
|
});
|
|
};
|
|
|
|
const bufferPromises = [];
|
|
for (let bufferState of state.buffers) {
|
|
bufferPromises.push(handleBufferState(bufferState));
|
|
}
|
|
|
|
return Promise.all(bufferPromises).then(buffers => {
|
|
this.buffers = buffers.filter(Boolean);
|
|
for (let buffer of this.buffers) {
|
|
this.grammarRegistry.maintainLanguageMode(buffer);
|
|
this.subscribeToBuffer(buffer);
|
|
}
|
|
this.setPaths(state.paths || [], { mustExist: true, exact: true });
|
|
});
|
|
}
|
|
|
|
serialize(options = {}) {
|
|
return {
|
|
deserializer: 'Project',
|
|
paths: this.getPaths(),
|
|
buffers: _.compact(
|
|
this.buffers.map(function(buffer) {
|
|
if (buffer.isRetained()) {
|
|
const isUnloading = options.isUnloading === true;
|
|
return buffer.serialize({
|
|
markerLayers: isUnloading,
|
|
history: isUnloading
|
|
});
|
|
}
|
|
})
|
|
)
|
|
};
|
|
}
|
|
|
|
/*
|
|
Section: Event Subscription
|
|
*/
|
|
|
|
// Public: Invoke the given callback when the project paths change.
|
|
//
|
|
// * `callback` {Function} to be called after the project paths change.
|
|
// * `projectPaths` An {Array} of {String} project paths.
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangePaths(callback) {
|
|
return this.emitter.on('did-change-paths', callback);
|
|
}
|
|
|
|
// Public: Invoke the given callback when a text buffer is added to the
|
|
// project.
|
|
//
|
|
// * `callback` {Function} to be called when a text buffer is added.
|
|
// * `buffer` A {TextBuffer} item.
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidAddBuffer(callback) {
|
|
return this.emitter.on('did-add-buffer', callback);
|
|
}
|
|
|
|
// Public: Invoke the given callback with all current and future text
|
|
// buffers in the project.
|
|
//
|
|
// * `callback` {Function} to be called with current and future text buffers.
|
|
// * `buffer` A {TextBuffer} item.
|
|
//
|
|
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
observeBuffers(callback) {
|
|
for (let buffer of this.getBuffers()) {
|
|
callback(buffer);
|
|
}
|
|
return this.onDidAddBuffer(callback);
|
|
}
|
|
|
|
// Extended: Invoke a callback when a filesystem change occurs within any open
|
|
// project path.
|
|
//
|
|
// ```js
|
|
// const disposable = atom.project.onDidChangeFiles(events => {
|
|
// for (const event of events) {
|
|
// // "created", "modified", "deleted", or "renamed"
|
|
// console.log(`Event action: ${event.action}`)
|
|
//
|
|
// // absolute path to the filesystem entry that was touched
|
|
// console.log(`Event path: ${event.path}`)
|
|
//
|
|
// if (event.action === 'renamed') {
|
|
// console.log(`.. renamed from: ${event.oldPath}`)
|
|
// }
|
|
// }
|
|
// })
|
|
//
|
|
// disposable.dispose()
|
|
// ```
|
|
//
|
|
// 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
|
|
// the watcher is receiving events.
|
|
//
|
|
// * `callback` {Function} to be called with batches of filesystem events reported by
|
|
// the operating system.
|
|
// * `events` An {Array} of objects that describe a batch of filesystem events.
|
|
// * `action` {String} describing the filesystem action that occurred. One of `"created"`,
|
|
// `"modified"`, `"deleted"`, or `"renamed"`.
|
|
// * `path` {String} containing the absolute path to the filesystem entry
|
|
// that was acted upon.
|
|
// * `oldPath` For rename events, {String} containing the filesystem entry's
|
|
// former absolute path.
|
|
//
|
|
// Returns a {Disposable} to manage this event subscription.
|
|
onDidChangeFiles(callback) {
|
|
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
|
|
*/
|
|
|
|
// Public: Get an {Array} of {GitRepository}s associated with the project's
|
|
// directories.
|
|
//
|
|
// 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 {GitRepository} objects:
|
|
// ```
|
|
// Promise.all(atom.project.getDirectories().map(
|
|
// atom.project.repositoryForDirectory.bind(atom.project)))
|
|
// ```
|
|
getRepositories() {
|
|
return this.repositories;
|
|
}
|
|
|
|
// Public: Get the repository for a given directory asynchronously.
|
|
//
|
|
// * `directory` {Directory} for which to get a {GitRepository}.
|
|
//
|
|
// Returns a {Promise} that resolves with either:
|
|
// * {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();
|
|
let promise = this.repositoryPromisesByPath.get(pathForDirectory);
|
|
if (!promise) {
|
|
const promises = this.repositoryProviders.map(provider =>
|
|
provider.repositoryForDirectory(directory)
|
|
);
|
|
promise = Promise.all(promises).then(repositories => {
|
|
const repo = repositories.find(repo => repo != null) || null;
|
|
|
|
// If no repository is found, remove the entry for the directory in
|
|
// @repositoryPromisesByPath in case some other RepositoryProvider is
|
|
// registered in the future that could supply a Repository for the
|
|
// directory.
|
|
if (repo == null)
|
|
this.repositoryPromisesByPath.delete(pathForDirectory);
|
|
|
|
if (repo && repo.onDidDestroy) {
|
|
repo.onDidDestroy(() =>
|
|
this.repositoryPromisesByPath.delete(pathForDirectory)
|
|
);
|
|
}
|
|
|
|
return repo;
|
|
});
|
|
this.repositoryPromisesByPath.set(pathForDirectory, promise);
|
|
}
|
|
return promise;
|
|
}
|
|
|
|
/*
|
|
Section: Managing Paths
|
|
*/
|
|
|
|
// Public: Get an {Array} of {String}s containing the paths of the project's
|
|
// directories.
|
|
getPaths() {
|
|
try {
|
|
return this.rootDirectories.map(rootDirectory => rootDirectory.getPath());
|
|
} catch (e) {
|
|
atom.notifications.addError(
|
|
"Please clear Atom's window state with: atom --clear-window-state"
|
|
);
|
|
}
|
|
}
|
|
|
|
// Public: Set the paths of the project's directories.
|
|
//
|
|
// * `projectPaths` {Array} of {String} paths.
|
|
// * `options` An optional {Object} that may contain the following keys:
|
|
// * `mustExist` If `true`, throw an Error if any `projectPaths` do not exist. Any remaining `projectPaths` that
|
|
// do exist will still be added to the project. Default: `false`.
|
|
// * `exact` If `true`, only add a `projectPath` if it names an existing directory. If `false` and any `projectPath`
|
|
// is a file or does not exist, its parent directory will be added instead. Default: `false`.
|
|
setPaths(projectPaths, options = {}) {
|
|
for (let repository of this.repositories) {
|
|
if (repository != null) repository.destroy();
|
|
}
|
|
this.rootDirectories = [];
|
|
this.repositories = [];
|
|
|
|
for (let path in this.watcherPromisesByPath) {
|
|
this.watcherPromisesByPath[path].then(watcher => {
|
|
watcher.dispose();
|
|
});
|
|
}
|
|
this.watcherPromisesByPath = {};
|
|
|
|
const missingProjectPaths = [];
|
|
for (let projectPath of projectPaths) {
|
|
try {
|
|
this.addPath(projectPath, {
|
|
emitEvent: false,
|
|
mustExist: true,
|
|
exact: options.exact === true
|
|
});
|
|
} catch (e) {
|
|
if (e.missingProjectPaths != null) {
|
|
missingProjectPaths.push(...e.missingProjectPaths);
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.emitter.emit('did-change-paths', projectPaths);
|
|
|
|
if (options.mustExist === true && missingProjectPaths.length > 0) {
|
|
const err = new Error('One or more project directories do not exist');
|
|
err.missingProjectPaths = missingProjectPaths;
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// Public: Add a path to the project's list of root paths
|
|
//
|
|
// * `projectPath` {String} The path to the directory to add.
|
|
// * `options` An optional {Object} that may contain the following keys:
|
|
// * `mustExist` If `true`, throw an Error if the `projectPath` does not exist. If `false`, a `projectPath` that does
|
|
// not exist is ignored. Default: `false`.
|
|
// * `exact` If `true`, only add `projectPath` if it names an existing directory. If `false`, if `projectPath` is a
|
|
// 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;
|
|
}
|
|
ok = ok && directory.existsSync();
|
|
|
|
if (!ok) {
|
|
if (options.mustExist === true) {
|
|
const err = new Error(`Project directory ${directory} does not exist`);
|
|
err.missingProjectPaths = [projectPath];
|
|
throw err;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (let existingDirectory of this.getDirectories()) {
|
|
if (existingDirectory.getPath() === directory.getPath()) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.rootDirectories.push(directory);
|
|
|
|
const didChangeCallback = events => {
|
|
// Stop event delivery immediately on removal of a rootDirectory, even if its watcher
|
|
// promise has yet to resolve at the time of removal
|
|
if (this.rootDirectories.includes(directory)) {
|
|
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)
|
|
this.watcherPromisesByPath[directory.getPath()] =
|
|
directory.onDidChangeFiles != null
|
|
? Promise.resolve(directory.onDidChangeFiles(didChangeCallback))
|
|
: watchPath(directory.getPath(), {}, didChangeCallback);
|
|
|
|
for (let watchedPath in this.watcherPromisesByPath) {
|
|
if (!this.rootDirectories.find(dir => dir.getPath() === watchedPath)) {
|
|
this.watcherPromisesByPath[watchedPath].then(watcher => {
|
|
watcher.dispose();
|
|
});
|
|
}
|
|
}
|
|
|
|
let repo = null;
|
|
for (let provider of this.repositoryProviders) {
|
|
if (provider.repositoryForDirectorySync) {
|
|
repo = provider.repositoryForDirectorySync(directory);
|
|
}
|
|
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());
|
|
}
|
|
}
|
|
|
|
getProvidedDirectoryForProjectPath(projectPath) {
|
|
for (let provider of this.directoryProviders) {
|
|
if (typeof provider.directoryForURISync === 'function') {
|
|
const directory = provider.directoryForURISync(projectPath);
|
|
if (directory) {
|
|
return directory;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getDirectoryForProjectPath(projectPath) {
|
|
let directory = this.getProvidedDirectoryForProjectPath(projectPath);
|
|
if (directory == null) {
|
|
directory = this.defaultDirectoryProvider.directoryForURISync(
|
|
projectPath
|
|
);
|
|
}
|
|
return directory;
|
|
}
|
|
|
|
// Extended: Access a {Promise} that resolves when the filesystem watcher associated with a project
|
|
// root directory is ready to begin receiving events.
|
|
//
|
|
// This is especially useful in test cases, where it's important to know that the watcher is
|
|
// ready before manipulating the filesystem to produce events.
|
|
//
|
|
// * `projectPath` {String} One of the project's root directories.
|
|
//
|
|
// Returns a {Promise} that resolves with the {PathWatcher} associated with this project root
|
|
// once it has initialized and is ready to start sending events. The Promise will reject with
|
|
// an error instead if `projectPath` is not currently a root directory.
|
|
getWatcherPromise(projectPath) {
|
|
return (
|
|
this.watcherPromisesByPath[projectPath] ||
|
|
Promise.reject(new Error(`${projectPath} is not a project root`))
|
|
);
|
|
}
|
|
|
|
// Public: remove a path from the project's list of root paths.
|
|
//
|
|
// * `projectPath` {String} The path to remove.
|
|
removePath(projectPath) {
|
|
// The projectPath may be a URI, in which case it should not be normalized.
|
|
if (!this.getPaths().includes(projectPath)) {
|
|
projectPath = this.defaultDirectoryProvider.normalizePath(projectPath);
|
|
}
|
|
|
|
let indexToRemove = null;
|
|
for (let i = 0; i < this.rootDirectories.length; i++) {
|
|
const directory = this.rootDirectories[i];
|
|
if (directory.getPath() === projectPath) {
|
|
indexToRemove = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (indexToRemove != null) {
|
|
this.rootDirectories.splice(indexToRemove, 1);
|
|
const [removedRepository] = this.repositories.splice(indexToRemove, 1);
|
|
if (!this.repositories.includes(removedRepository)) {
|
|
if (removedRepository) removedRepository.destroy();
|
|
}
|
|
if (this.watcherPromisesByPath[projectPath] != null) {
|
|
this.watcherPromisesByPath[projectPath].then(w => w.dispose());
|
|
}
|
|
delete this.watcherPromisesByPath[projectPath];
|
|
this.emitter.emit('did-change-paths', this.getPaths());
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Public: Get an {Array} of {Directory}s associated with this project.
|
|
getDirectories() {
|
|
return this.rootDirectories;
|
|
}
|
|
|
|
resolvePath(uri) {
|
|
if (!uri) {
|
|
return;
|
|
}
|
|
|
|
if (uri.match(/[A-Za-z0-9+-.]+:\/\//)) {
|
|
// leave path alone if it has a scheme
|
|
return uri;
|
|
} else {
|
|
let projectPath;
|
|
if (fs.isAbsolute(uri)) {
|
|
return this.defaultDirectoryProvider.normalizePath(fs.resolveHome(uri));
|
|
// TODO: what should we do here when there are multiple directories?
|
|
} else if ((projectPath = this.getPaths()[0])) {
|
|
return this.defaultDirectoryProvider.normalizePath(
|
|
fs.resolveHome(path.join(projectPath, uri))
|
|
);
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
relativize(fullPath) {
|
|
return this.relativizePath(fullPath)[1];
|
|
}
|
|
|
|
// Public: Get the path to the project directory that contains the given path,
|
|
// and the relative path from that project directory to the given path.
|
|
//
|
|
// * `fullPath` {String} An absolute path.
|
|
//
|
|
// Returns an {Array} with two elements:
|
|
// * `projectPath` The {String} path to the project directory that contains the
|
|
// given path, or `null` if none is found.
|
|
// * `relativePath` {String} The relative path from the project directory to
|
|
// the given path.
|
|
relativizePath(fullPath) {
|
|
let result = [null, fullPath];
|
|
if (fullPath != null) {
|
|
for (let rootDirectory of this.rootDirectories) {
|
|
const relativePath = rootDirectory.relativize(fullPath);
|
|
if (relativePath != null && relativePath.length < result[1].length) {
|
|
result = [rootDirectory.getPath(), relativePath];
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Public: Determines whether the given path (real or symbolic) is inside the
|
|
// project's directory.
|
|
//
|
|
// This method does not actually check if the path exists, it just checks their
|
|
// locations relative to each other.
|
|
//
|
|
// ## Examples
|
|
//
|
|
// Basic operation
|
|
//
|
|
// ```coffee
|
|
// # Project's root directory is /foo/bar
|
|
// project.contains('/foo/bar/baz') # => true
|
|
// project.contains('/usr/lib/baz') # => false
|
|
// ```
|
|
//
|
|
// Existence of the path is not required
|
|
//
|
|
// ```coffee
|
|
// # Project's root directory is /foo/bar
|
|
// fs.existsSync('/foo/bar/baz') # => false
|
|
// project.contains('/foo/bar/baz') # => true
|
|
// ```
|
|
//
|
|
// * `pathToCheck` {String} path
|
|
//
|
|
// Returns whether the path is inside the project's root directory.
|
|
contains(pathToCheck) {
|
|
return this.rootDirectories.some(dir => dir.contains(pathToCheck));
|
|
}
|
|
|
|
/*
|
|
Section: Private
|
|
*/
|
|
|
|
consumeServices({ serviceHub }) {
|
|
serviceHub.consume('atom.directory-provider', '^0.1.0', provider => {
|
|
this.directoryProviders.unshift(provider);
|
|
return new Disposable(() => {
|
|
return this.directoryProviders.splice(
|
|
this.directoryProviders.indexOf(provider),
|
|
1
|
|
);
|
|
});
|
|
});
|
|
|
|
return serviceHub.consume(
|
|
'atom.repository-provider',
|
|
'^0.1.0',
|
|
provider => {
|
|
this.repositoryProviders.unshift(provider);
|
|
if (this.repositories.includes(null)) {
|
|
this.setPaths(this.getPaths());
|
|
}
|
|
return new Disposable(() => {
|
|
return this.repositoryProviders.splice(
|
|
this.repositoryProviders.indexOf(provider),
|
|
1
|
|
);
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
// Retrieves all the {TextBuffer}s in the project; that is, the
|
|
// buffers for all open files.
|
|
//
|
|
// Returns an {Array} of {TextBuffer}s.
|
|
getBuffers() {
|
|
return this.buffers.slice();
|
|
}
|
|
|
|
// Is the buffer for the given path modified?
|
|
isPathModified(filePath) {
|
|
const bufferForPath = this.findBufferForPath(this.resolvePath(filePath));
|
|
return bufferForPath && bufferForPath.isModified();
|
|
}
|
|
|
|
findBufferForPath(filePath) {
|
|
return _.find(this.buffers, buffer => buffer.getPath() === filePath);
|
|
}
|
|
|
|
findBufferForId(id) {
|
|
return _.find(this.buffers, buffer => buffer.getId() === id);
|
|
}
|
|
|
|
// Only to be used in specs
|
|
bufferForPathSync(filePath) {
|
|
const absoluteFilePath = this.resolvePath(filePath);
|
|
if (this.retiredBufferPaths.has(absoluteFilePath)) {
|
|
return null;
|
|
}
|
|
|
|
let existingBuffer;
|
|
if (filePath) {
|
|
existingBuffer = this.findBufferForPath(absoluteFilePath);
|
|
}
|
|
return existingBuffer != null
|
|
? existingBuffer
|
|
: this.buildBufferSync(absoluteFilePath);
|
|
}
|
|
|
|
// Only to be used when deserializing
|
|
bufferForIdSync(id) {
|
|
if (this.retiredBufferIDs.has(id)) {
|
|
return null;
|
|
}
|
|
|
|
let existingBuffer;
|
|
if (id) {
|
|
existingBuffer = this.findBufferForId(id);
|
|
}
|
|
return existingBuffer != null ? existingBuffer : this.buildBufferSync();
|
|
}
|
|
|
|
// Given a file path, this retrieves or creates a new {TextBuffer}.
|
|
//
|
|
// If the `filePath` already has a `buffer`, that value is used instead. Otherwise,
|
|
// `text` is used as the contents of the new buffer.
|
|
//
|
|
// * `filePath` A {String} representing a path. If `null`, an "Untitled" buffer is created.
|
|
//
|
|
// Returns a {Promise} that resolves to the {TextBuffer}.
|
|
bufferForPath(absoluteFilePath) {
|
|
let existingBuffer;
|
|
if (absoluteFilePath != null) {
|
|
existingBuffer = this.findBufferForPath(absoluteFilePath);
|
|
}
|
|
if (existingBuffer) {
|
|
return Promise.resolve(existingBuffer);
|
|
} else {
|
|
return this.buildBuffer(absoluteFilePath);
|
|
}
|
|
}
|
|
|
|
shouldDestroyBufferOnFileDelete() {
|
|
return atom.config.get('core.closeDeletedFileTabs');
|
|
}
|
|
|
|
// Still needed when deserializing a tokenized buffer
|
|
buildBufferSync(absoluteFilePath) {
|
|
const params = {
|
|
shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete
|
|
};
|
|
|
|
let buffer;
|
|
if (absoluteFilePath != null) {
|
|
buffer = TextBuffer.loadSync(absoluteFilePath, params);
|
|
} else {
|
|
buffer = new TextBuffer(params);
|
|
}
|
|
this.addBuffer(buffer);
|
|
return buffer;
|
|
}
|
|
|
|
// Given a file path, this sets its {TextBuffer}.
|
|
//
|
|
// * `absoluteFilePath` A {String} representing a path.
|
|
// * `text` The {String} text to use as a buffer.
|
|
//
|
|
// Returns a {Promise} that resolves to the {TextBuffer}.
|
|
async buildBuffer(absoluteFilePath) {
|
|
const params = {
|
|
shouldDestroyOnFileDelete: this.shouldDestroyBufferOnFileDelete
|
|
};
|
|
|
|
let buffer;
|
|
if (absoluteFilePath != null) {
|
|
if (this.loadPromisesByPath[absoluteFilePath] == null) {
|
|
this.loadPromisesByPath[absoluteFilePath] = TextBuffer.load(
|
|
absoluteFilePath,
|
|
params
|
|
)
|
|
.then(result => {
|
|
delete this.loadPromisesByPath[absoluteFilePath];
|
|
return result;
|
|
})
|
|
.catch(error => {
|
|
delete this.loadPromisesByPath[absoluteFilePath];
|
|
throw error;
|
|
});
|
|
}
|
|
buffer = await this.loadPromisesByPath[absoluteFilePath];
|
|
} else {
|
|
buffer = new TextBuffer(params);
|
|
}
|
|
|
|
this.grammarRegistry.autoAssignLanguageMode(buffer);
|
|
|
|
this.addBuffer(buffer);
|
|
return buffer;
|
|
}
|
|
|
|
addBuffer(buffer, options = {}) {
|
|
this.buffers.push(buffer);
|
|
this.subscriptions.add(this.grammarRegistry.maintainLanguageMode(buffer));
|
|
this.subscribeToBuffer(buffer);
|
|
this.emitter.emit('did-add-buffer', buffer);
|
|
return buffer;
|
|
}
|
|
|
|
// Removes a {TextBuffer} association from the project.
|
|
//
|
|
// Returns the removed {TextBuffer}.
|
|
removeBuffer(buffer) {
|
|
const index = this.buffers.indexOf(buffer);
|
|
if (index !== -1) {
|
|
return this.removeBufferAtIndex(index);
|
|
}
|
|
}
|
|
|
|
removeBufferAtIndex(index, options = {}) {
|
|
const [buffer] = this.buffers.splice(index, 1);
|
|
return buffer != null ? buffer.destroy() : undefined;
|
|
}
|
|
|
|
eachBuffer(...args) {
|
|
let subscriber;
|
|
if (args.length > 1) {
|
|
subscriber = args.shift();
|
|
}
|
|
const callback = args.shift();
|
|
|
|
for (let buffer of this.getBuffers()) {
|
|
callback(buffer);
|
|
}
|
|
if (subscriber) {
|
|
return subscriber.subscribe(this, 'buffer-created', buffer =>
|
|
callback(buffer)
|
|
);
|
|
} else {
|
|
return this.on('buffer-created', buffer => callback(buffer));
|
|
}
|
|
}
|
|
|
|
subscribeToBuffer(buffer) {
|
|
buffer.onWillSave(async ({ path }) =>
|
|
this.applicationDelegate.emitWillSavePath(path)
|
|
);
|
|
buffer.onDidSave(({ path }) =>
|
|
this.applicationDelegate.emitDidSavePath(path)
|
|
);
|
|
buffer.onDidDestroy(() => this.removeBuffer(buffer));
|
|
buffer.onDidChangePath(() => {
|
|
if (!(this.getPaths().length > 0)) {
|
|
this.setPaths([path.dirname(buffer.getPath())]);
|
|
}
|
|
});
|
|
buffer.onWillThrowWatchError(({ error, handle }) => {
|
|
handle();
|
|
const message =
|
|
`Unable to read file after file \`${error.eventType}\` event.` +
|
|
`Make sure you have permission to access \`${buffer.getPath()}\`.`;
|
|
this.notificationManager.addWarning(message, {
|
|
detail: error.message,
|
|
dismissable: true
|
|
});
|
|
});
|
|
}
|
|
};
|