Files
atom/src/theme-manager.js
2019-05-31 18:33:56 +02:00

465 lines
14 KiB
JavaScript

/* global snapshotAuxiliaryData */
const path = require('path');
const _ = require('underscore-plus');
const { Emitter, CompositeDisposable } = require('event-kit');
const { File } = require('pathwatcher');
const fs = require('fs-plus');
const LessCompileCache = require('./less-compile-cache');
// Extended: Handles loading and activating available themes.
//
// An instance of this class is always available as the `atom.themes` global.
module.exports = class ThemeManager {
constructor({
packageManager,
config,
styleManager,
notificationManager,
viewRegistry
}) {
this.packageManager = packageManager;
this.config = config;
this.styleManager = styleManager;
this.notificationManager = notificationManager;
this.viewRegistry = viewRegistry;
this.emitter = new Emitter();
this.styleSheetDisposablesBySourcePath = {};
this.lessCache = null;
this.initialLoadComplete = false;
this.packageManager.registerPackageActivator(this, ['theme']);
this.packageManager.onDidActivateInitialPackages(() => {
this.onDidChangeActiveThemes(() =>
this.packageManager.reloadActivePackageStyleSheets()
);
});
}
initialize({ resourcePath, configDirPath, safeMode, devMode }) {
this.resourcePath = resourcePath;
this.configDirPath = configDirPath;
this.safeMode = safeMode;
this.lessSourcesByRelativeFilePath = null;
if (devMode || typeof snapshotAuxiliaryData === 'undefined') {
this.lessSourcesByRelativeFilePath = {};
this.importedFilePathsByRelativeImportPath = {};
} else {
this.lessSourcesByRelativeFilePath =
snapshotAuxiliaryData.lessSourcesByRelativeFilePath;
this.importedFilePathsByRelativeImportPath =
snapshotAuxiliaryData.importedFilePathsByRelativeImportPath;
}
}
/*
Section: Event Subscription
*/
// Essential: Invoke `callback` when style sheet changes associated with
// updating the list of active themes have completed.
//
// * `callback` {Function}
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeActiveThemes(callback) {
return this.emitter.on('did-change-active-themes', callback);
}
/*
Section: Accessing Available Themes
*/
getAvailableNames() {
// TODO: Maybe should change to list all the available themes out there?
return this.getLoadedNames();
}
/*
Section: Accessing Loaded Themes
*/
// Public: Returns an {Array} of {String}s of all the loaded theme names.
getLoadedThemeNames() {
return this.getLoadedThemes().map(theme => theme.name);
}
// Public: Returns an {Array} of all the loaded themes.
getLoadedThemes() {
return this.packageManager
.getLoadedPackages()
.filter(pack => pack.isTheme());
}
/*
Section: Accessing Active Themes
*/
// Public: Returns an {Array} of {String}s of all the active theme names.
getActiveThemeNames() {
return this.getActiveThemes().map(theme => theme.name);
}
// Public: Returns an {Array} of all the active themes.
getActiveThemes() {
return this.packageManager
.getActivePackages()
.filter(pack => pack.isTheme());
}
activatePackages() {
return this.activateThemes();
}
/*
Section: Managing Enabled Themes
*/
warnForNonExistentThemes() {
let themeNames = this.config.get('core.themes') || [];
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.`);
}
}
}
// Public: Get the enabled theme names from the config.
//
// Returns an array of theme names in the order that they should be activated.
getEnabledThemeNames() {
let themeNames = this.config.get('core.themes') || [];
if (!Array.isArray(themeNames)) {
themeNames = [themeNames];
}
themeNames = themeNames.filter(
themeName =>
typeof themeName === 'string' &&
this.packageManager.resolvePackagePath(themeName)
);
// Use a built-in syntax and UI theme any time the configured themes are not
// available.
if (themeNames.length < 2) {
const builtInThemeNames = [
'atom-dark-syntax',
'atom-dark-ui',
'atom-light-syntax',
'atom-light-ui',
'base16-tomorrow-dark-theme',
'base16-tomorrow-light-theme',
'solarized-dark-syntax',
'solarized-light-syntax'
];
themeNames = _.intersection(themeNames, builtInThemeNames);
if (themeNames.length === 0) {
themeNames = ['one-dark-syntax', 'one-dark-ui'];
} else if (themeNames.length === 1) {
if (themeNames[0].endsWith('-ui')) {
themeNames.unshift('one-dark-syntax');
} else {
themeNames.push('one-dark-ui');
}
}
}
// Reverse so the first (top) theme is loaded after the others. We want
// the first/top theme to override later themes in the stack.
return themeNames.reverse();
}
/*
Section: Private
*/
// Resolve and apply the stylesheet specified by the path.
//
// This supports both CSS and Less stylesheets.
//
// * `stylesheetPath` A {String} path to the stylesheet that can be an absolute
// path or a relative path that will be resolved against the load path.
//
// Returns a {Disposable} on which `.dispose()` can be called to remove the
// required stylesheet.
requireStylesheet(
stylesheetPath,
priority,
skipDeprecatedSelectorsTransformation
) {
let fullPath = this.resolveStylesheet(stylesheetPath);
if (fullPath) {
const content = this.loadStylesheet(fullPath);
return this.applyStylesheet(
fullPath,
content,
priority,
skipDeprecatedSelectorsTransformation
);
} else {
throw new Error(`Could not find a file at path '${stylesheetPath}'`);
}
}
unwatchUserStylesheet() {
if (this.userStylesheetSubscriptions != null)
this.userStylesheetSubscriptions.dispose();
this.userStylesheetSubscriptions = null;
this.userStylesheetFile = null;
if (this.userStyleSheetDisposable != null)
this.userStyleSheetDisposable.dispose();
this.userStyleSheetDisposable = null;
}
loadUserStylesheet() {
this.unwatchUserStylesheet();
const userStylesheetPath = this.styleManager.getUserStyleSheetPath();
if (!fs.isFileSync(userStylesheetPath)) {
return;
}
try {
this.userStylesheetFile = new File(userStylesheetPath);
this.userStylesheetSubscriptions = new CompositeDisposable();
const reloadStylesheet = () => this.loadUserStylesheet();
this.userStylesheetSubscriptions.add(
this.userStylesheetFile.onDidChange(reloadStylesheet)
);
this.userStylesheetSubscriptions.add(
this.userStylesheetFile.onDidRename(reloadStylesheet)
);
this.userStylesheetSubscriptions.add(
this.userStylesheetFile.onDidDelete(reloadStylesheet)
);
} catch (error) {
const message = `\
Unable to watch path: \`${path.basename(userStylesheetPath)}\`. Make sure
you have permissions to \`${userStylesheetPath}\`.
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\
`;
this.notificationManager.addError(message, { dismissable: true });
}
let userStylesheetContents;
try {
userStylesheetContents = this.loadStylesheet(userStylesheetPath, true);
} catch (error) {
return;
}
this.userStyleSheetDisposable = this.styleManager.addStyleSheet(
userStylesheetContents,
{ sourcePath: userStylesheetPath, priority: 2 }
);
}
loadBaseStylesheets() {
this.reloadBaseStylesheets();
}
reloadBaseStylesheets() {
this.requireStylesheet('../static/atom', -2, true);
}
stylesheetElementForId(id) {
const escapedId = id.replace(/\\/g, '\\\\');
return document.head.querySelector(
`atom-styles style[source-path="${escapedId}"]`
);
}
resolveStylesheet(stylesheetPath) {
if (path.extname(stylesheetPath).length > 0) {
return fs.resolveOnLoadPath(stylesheetPath);
} else {
return fs.resolveOnLoadPath(stylesheetPath, ['css', 'less']);
}
}
loadStylesheet(stylesheetPath, importFallbackVariables) {
if (path.extname(stylesheetPath) === '.less') {
return this.loadLessStylesheet(stylesheetPath, importFallbackVariables);
} else {
return fs.readFileSync(stylesheetPath, 'utf8');
}
}
loadLessStylesheet(lessStylesheetPath, importFallbackVariables = false) {
if (this.lessCache == null) {
this.lessCache = new LessCompileCache({
resourcePath: this.resourcePath,
lessSourcesByRelativeFilePath: this.lessSourcesByRelativeFilePath,
importedFilePathsByRelativeImportPath: this
.importedFilePathsByRelativeImportPath,
importPaths: this.getImportPaths()
});
}
try {
if (importFallbackVariables) {
const baseVarImports = `\
@import "variables/ui-variables";
@import "variables/syntax-variables";\
`;
const relativeFilePath = path.relative(
this.resourcePath,
lessStylesheetPath
);
const lessSource = this.lessSourcesByRelativeFilePath[relativeFilePath];
let content, digest;
if (lessSource != null) {
({ content } = lessSource);
({ digest } = lessSource);
} else {
content =
baseVarImports + '\n' + fs.readFileSync(lessStylesheetPath, 'utf8');
digest = null;
}
return this.lessCache.cssForFile(lessStylesheetPath, content, digest);
} else {
return this.lessCache.read(lessStylesheetPath);
}
} catch (error) {
let detail, message;
error.less = true;
if (error.line != null) {
// Adjust line numbers for import fallbacks
if (importFallbackVariables) {
error.line -= 2;
}
message = `Error compiling Less stylesheet: \`${lessStylesheetPath}\``;
detail = `Line number: ${error.line}\n${error.message}`;
} else {
message = `Error loading Less stylesheet: \`${lessStylesheetPath}\``;
detail = error.message;
}
this.notificationManager.addError(message, { detail, dismissable: true });
throw error;
}
}
removeStylesheet(stylesheetPath) {
if (this.styleSheetDisposablesBySourcePath[stylesheetPath] != null) {
this.styleSheetDisposablesBySourcePath[stylesheetPath].dispose();
}
}
applyStylesheet(path, text, priority, skipDeprecatedSelectorsTransformation) {
this.styleSheetDisposablesBySourcePath[
path
] = this.styleManager.addStyleSheet(text, {
priority,
skipDeprecatedSelectorsTransformation,
sourcePath: path
});
return this.styleSheetDisposablesBySourcePath[path];
}
activateThemes() {
return new Promise(resolve => {
// @config.observe runs the callback once, then on subsequent changes.
this.config.observe('core.themes', () => {
this.deactivateThemes().then(() => {
this.warnForNonExistentThemes();
this.refreshLessCache(); // Update cache for packages in core.themes config
const promises = [];
for (const themeName of this.getEnabledThemeNames()) {
if (this.packageManager.resolvePackagePath(themeName)) {
promises.push(this.packageManager.activatePackage(themeName));
} else {
console.warn(
`Failed to activate theme '${themeName}' because it isn't installed.`
);
}
}
return Promise.all(promises).then(() => {
this.addActiveThemeClasses();
this.refreshLessCache(); // Update cache again now that @getActiveThemes() is populated
this.loadUserStylesheet();
this.reloadBaseStylesheets();
this.initialLoadComplete = true;
this.emitter.emit('did-change-active-themes');
resolve();
});
});
});
});
}
deactivateThemes() {
this.removeActiveThemeClasses();
this.unwatchUserStylesheet();
const results = this.getActiveThemes().map(pack =>
this.packageManager.deactivatePackage(pack.name)
);
return Promise.all(
results.filter(r => r != null && typeof r.then === 'function')
);
}
isInitialLoadComplete() {
return this.initialLoadComplete;
}
addActiveThemeClasses() {
const workspaceElement = this.viewRegistry.getView(this.workspace);
if (workspaceElement) {
for (const pack of this.getActiveThemes()) {
workspaceElement.classList.add(`theme-${pack.name}`);
}
}
}
removeActiveThemeClasses() {
const workspaceElement = this.viewRegistry.getView(this.workspace);
for (const pack of this.getActiveThemes()) {
workspaceElement.classList.remove(`theme-${pack.name}`);
}
}
refreshLessCache() {
if (this.lessCache) this.lessCache.setImportPaths(this.getImportPaths());
}
getImportPaths() {
let themePaths;
const activeThemes = this.getActiveThemes();
if (activeThemes.length > 0) {
themePaths = activeThemes
.filter(theme => theme)
.map(theme => theme.getStylesheetsPath());
} else {
themePaths = [];
for (const themeName of this.getEnabledThemeNames()) {
const themePath = this.packageManager.resolvePackagePath(themeName);
if (themePath) {
const deprecatedPath = path.join(themePath, 'stylesheets');
if (fs.isDirectorySync(deprecatedPath)) {
themePaths.push(deprecatedPath);
} else {
themePaths.push(path.join(themePath, 'styles'));
}
}
}
}
return themePaths.filter(themePath => fs.isDirectorySync(themePath));
}
};