codemirror-textmate
Version:
Textmate based tokenization for CodeMirror
228 lines • 9.83 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const CodeMirror = require("codemirror");
const monaco_textmate_1 = require("monaco-textmate");
const PCancelable = require("p-cancelable");
const Highlighter_1 = require("./Highlighter");
exports.addGrammar = Highlighter_1.Highlighter.addGrammar;
exports.activateLanguage = Highlighter_1.Highlighter.activateLanguage;
/**
* Inject grammars into grammars
* Returns an array of language ID's that were udpated
*
* @param scopeName Scope name that needs to be injected into other grammars
* @param injectInto List of host scope names
*/
function linkInjections(scopeName, injectInto) {
return __awaiter(this, void 0, void 0, function* () {
const affectedLanguages = Highlighter_1.Highlighter.linkInjections(scopeName, injectInto);
yield updateCmTmBindings(null, affectedLanguages);
return affectedLanguages;
});
}
exports.linkInjections = linkInjections;
/**
* Uninject grammars out of grammars
* Returns an array of language ID's that were udpated
*
* @param scopeName Scope name that needs to be uninjected out of other grammars
* @param unInjectFrom If provided, scope name will be uninjected only from this list of host scope names, otherwise will be uninjected from all
*/
function unlinkInjections(scopeName, unInjectFrom) {
return __awaiter(this, void 0, void 0, function* () {
const affectedLanguages = Highlighter_1.Highlighter.unlinkInjections(scopeName, unInjectFrom);
yield updateCmTmBindings(null, affectedLanguages);
return affectedLanguages;
});
}
exports.unlinkInjections = unlinkInjections;
exports.themedHighlighters = new Map();
exports.themedHighlighters.set('default', new Highlighter_1.Highlighter());
/**
* Add a Textmate theme to CodeMirror
*
* @param theme Theme object
*/
function addTheme(theme) {
// TODO: add regex check to theme.name to make sure it's valid CSS classname too
if (typeof theme.name !== 'string') {
throw new Error(`RawTheme must have 'name' property for referencing purposes`);
}
exports.themedHighlighters.set(theme.name, new Highlighter_1.Highlighter(theme));
}
exports.addTheme = addTheme;
const updateCmTmBindings = (() => {
// local "static" variables
const cmModeToTheme = new Map();
const cmThemeRecord = new WeakMap();
const tmThemeStyleNodes = new Map();
/**
* wrapper around CodeMirror.defineMode
* If CodeMirror.defineMode is directly called in the primary function below, it causes memory leak by not letting go of cm variable (forms a closure?)
*/
const defineMode = (languageId, tokenizer) => {
CodeMirror.defineMode(languageId, () => {
return {
copyState: (state) => ({ ruleStack: state.ruleStack.clone() }),
startState: () => ({ ruleStack: monaco_textmate_1.INITIAL }),
token: tokenizer,
};
});
};
return (cm, invalidateLanguages) => new PCancelable((resolve, reject, onCancel) => __awaiter(this, void 0, void 0, function* () {
onCancel.shouldReject = false;
let canceled = false;
onCancel(() => canceled = true);
if (!cm) {
if (Array.isArray(invalidateLanguages)) {
yield Promise.all(invalidateLanguages.map((lang) => __awaiter(this, void 0, void 0, function* () {
// invalidate previously defined CM mode
if (cmModeToTheme.delete(lang)) {
// preload update
yield Highlighter_1.Highlighter.loadLanguage(lang);
}
})));
}
return resolve(false);
}
const languageId = cm.getOption('mode');
const themeName = cm.getOption('theme');
// get theme name that was bound last time this mode was baked
const languageBoundTheme = cmModeToTheme.get(languageId);
const prevThemeName = cmThemeRecord.get(cm) || 'default';
const highlighter = exports.themedHighlighters.get(themeName) || exports.themedHighlighters.get('default');
const isTextMateTheme = themeName !== 'default' && exports.themedHighlighters.has(themeName);
cmThemeRecord.set(cm, themeName);
if (Highlighter_1.Highlighter.hasLanguageRegistered(languageId)) {
cmModeToTheme.set(languageId, themeName);
}
// Cleanup previous theme resources (if any)
if (typeof prevThemeName === 'string' &&
prevThemeName !== 'default' &&
prevThemeName !== themeName &&
exports.themedHighlighters.has(themeName) &&
tmThemeStyleNodes.has(prevThemeName)) {
const meta = tmThemeStyleNodes.get(prevThemeName);
if (meta.inUseBy.has(cm) && meta.inUseByCount === 1) {
tmThemeStyleNodes.delete(prevThemeName);
document.head.removeChild(meta.styleNode);
}
else {
meta.inUseBy.delete(cm);
meta.inUseByCount--;
}
}
// Allocate new theme resources (if applicable)
if (isTextMateTheme) {
if (tmThemeStyleNodes.has(themeName)) {
const meta = tmThemeStyleNodes.get(themeName);
if (!meta.inUseBy.has(cm)) {
meta.inUseBy.add(cm);
meta.inUseByCount++;
}
}
else {
const styleNode = document.createElement('style');
styleNode.textContent = highlighter.cssText;
tmThemeStyleNodes.set(themeName, { styleNode, inUseBy: new WeakSet().add(cm), inUseByCount: 1 });
document.head.appendChild(styleNode);
}
}
// Nothing much "changed", hence nothing much is needs to be done
if (typeof languageId === 'string' && typeof themeName === 'string' && typeof languageBoundTheme === 'string' &&
// new theme is same as theme that was baked with language previously
languageBoundTheme === themeName) {
return resolve(prevThemeName !== themeName);
}
// skip if language id cannot be resolved to tm grammar scope
if (!Highlighter_1.Highlighter.hasLanguageRegistered(languageId)) {
return resolve(false);
}
const tokenizer = yield highlighter.getTokenizer(languageId);
// user probably changed theme or mode in the meantime, this fn will be triggered again anyway
if (canceled) {
return resolve(false);
}
defineMode(languageId, tokenizer);
resolve(true);
}));
})();
/**
* Wrapper around `udpateCmTmBindings` that prevents race conditions and obsolute changes
* Will queue all the CM instances that need an update and will update them one by one (while merging duplicate instances)
*/
const safeUpdateCM = (() => {
const queue = [];
const resolverCallbacks = new WeakMap();
let currentActivation;
const proceed = () => __awaiter(this, void 0, void 0, function* () {
const nextCM = queue[0];
if (!nextCM) {
return;
}
currentActivation = updateCmTmBindings(nextCM);
const resolver = resolverCallbacks.get(nextCM);
resolver(yield currentActivation);
resolverCallbacks.delete(nextCM);
queue.shift();
currentActivation = null;
proceed();
});
return (cm) => __awaiter(this, void 0, void 0, function* () {
// currently happening but now obsolete
if (queue[0] === cm && currentActivation) {
currentActivation.cancel();
const prevResolver = resolverCallbacks.get(cm);
resolverCallbacks.delete(cm);
queue.shift();
queue.push(cm);
prevResolver(false);
}
// if hasn't been queued up yet then do it
if (queue.indexOf(cm) === -1) {
queue.push(cm);
}
const prom = new Promise((res) => {
resolverCallbacks.set(cm, res);
});
// No work is being done === queue not proceeding => start the queue
if (!currentActivation) {
proceed();
}
return prom;
});
})();
CodeMirror.defineInitHook((cm) => __awaiter(this, void 0, void 0, function* () {
let shouldIgnoreNextEvent = false;
let lastLanguageId = null;
function updateInstance() {
return __awaiter(this, void 0, void 0, function* () {
const langId = cm.getOption('mode');
if (shouldIgnoreNextEvent && langId === lastLanguageId) {
shouldIgnoreNextEvent = false;
return;
}
if (yield safeUpdateCM(cm)) {
lastLanguageId = langId;
shouldIgnoreNextEvent = true;
cm.setOption('mode', langId);
}
});
}
cm.on('swapDoc', updateInstance);
cm.on('optionChange', (inst, option) => {
if (option === 'mode' || option === 'theme') {
updateInstance();
}
});
updateInstance();
}));
//# sourceMappingURL=index.js.map