mirror of
https://github.com/atom/atom.git
synced 2026-01-13 08:57:59 -05:00
465 lines
14 KiB
JavaScript
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));
|
|
}
|
|
};
|