@atlaskit/editor-plugin-code-block
Version:
Code block plugin for @atlaskit/editor-core
120 lines (116 loc) • 5.72 kB
JavaScript
import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
import { fg } from '@atlaskit/platform-feature-flags';
import { ACTIONS } from '../pm-plugins/actions';
import { autoDetectPluginKey } from '../pm-plugins/auto-detect-state';
import { createAutoDetectEntry } from './auto-detect-state';
import { detectLanguage } from './language-detect';
const AUTO_DETECT_DEBOUNCE_MS = 500;
// Stored positions are mapped through transactions; verify the localId before using them.
const getCodeBlockFromEntry = (view, localId, entry) => {
const node = view.state.doc.nodeAt(entry.pos);
const codeBlockType = view.state.schema.nodes.codeBlock;
if ((node === null || node === void 0 ? void 0 : node.type) === codeBlockType && node.attrs.localId === localId) {
return {
node,
pos: entry.pos
};
}
return null;
};
// Runs after debounce, so it must re-read current editor state before applying language changes.
const runPendingDetection = (view, localId, api) => {
var _api$core;
const pluginState = autoDetectPluginKey.getState(view.state);
const entry = pluginState === null || pluginState === void 0 ? void 0 : pluginState.languageDetectionMap[localId];
if (!(entry !== null && entry !== void 0 && entry.isPending)) {
return;
}
const found = getCodeBlockFromEntry(view, localId, entry);
if (!found) {
return;
}
const detectedLanguage = detectLanguage(found.node.textContent);
const detectionResult = detectedLanguage ? 'detected' : 'noneDetected';
const detectionPhase = entry.detectionResult ? 'redetection' : 'initial';
// Keep a previous auto-detected language when the latest snippet is too weak to classify.
const shouldPreserveAutoDetectedLanguage = !detectedLanguage && Boolean(entry.autoDetectedLanguage) && found.node.attrs.language === entry.autoDetectedLanguage;
const nextEntry = {
...createAutoDetectEntry(found.node, found.pos, false, entry),
detectionResult,
autoDetectedLanguage: detectedLanguage !== null && detectedLanguage !== void 0 ? detectedLanguage : entry.autoDetectedLanguage
};
// If there is no confident detection, record the result without clearing user-visible language.
const shouldOnlyUpdateDetectionState = !detectedLanguage && (!found.node.attrs.language || shouldPreserveAutoDetectedLanguage);
const hasDetectedLanguageChange = Boolean(detectedLanguage) && found.node.attrs.language !== detectedLanguage;
const shouldClearStaleLanguage = !detectedLanguage && Boolean(found.node.attrs.language) && !shouldPreserveAutoDetectedLanguage;
const shouldUpdateNodeLanguage = fg('platform_editor_code_block_dogfooding_patch') ? hasDetectedLanguageChange || shouldClearStaleLanguage : !shouldOnlyUpdateDetectionState;
api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
tr
}) => {
if (shouldUpdateNodeLanguage) {
tr.setNodeMarkup(found.pos, undefined, {
...found.node.attrs,
language: detectedLanguage
}, found.node.marks);
}
tr.setMeta(autoDetectPluginKey, {
type: ACTIONS.SET_AUTO_DETECT_ENTRY,
data: {
localId,
entry: nextEntry
}
});
// When platform_editor_code_block_dogfooding_patch is cleaned up, merge this with the previous shouldUpdateNodeLanguage check.
if (!fg('platform_editor_code_block_dogfooding_patch') || shouldUpdateNodeLanguage) {
var _api$analytics;
api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.attachAnalyticsEvent({
action: ACTION.LANGUAGE_AUTO_DETECTED,
actionSubject: ACTION_SUBJECT.CODE_BLOCK,
attributes: {
language: detectedLanguage !== null && detectedLanguage !== void 0 ? detectedLanguage : 'none',
detectionResult,
...(fg('platform_editor_code_block_dogfooding_patch') ? {
detectionPhase
} : {})
},
eventType: EVENT_TYPE.TRACK
})(tr);
}
// Language detection runs in the background and should not move the viewport.
return tr.setMeta('scrollIntoView', false);
});
};
const clearTimer = (timers, localId) => {
const scheduledDetection = timers.get(localId);
if (scheduledDetection) {
clearTimeout(scheduledDetection.timer);
timers.delete(localId);
}
};
// Keeps one debounce timer per pending code block and drops timers for stale entries.
export const syncPendingDetectionTimers = (view, timers, api) => {
var _pluginState$language;
const pluginState = autoDetectPluginKey.getState(view.state);
const pendingEntries = Object.entries((_pluginState$language = pluginState === null || pluginState === void 0 ? void 0 : pluginState.languageDetectionMap) !== null && _pluginState$language !== void 0 ? _pluginState$language : {}).filter(([, entry]) => entry.isPending);
const pendingLocalIds = new Set(pendingEntries.map(([localId]) => localId));
pendingEntries.forEach(([localId, entry]) => {
const scheduledDetection = timers.get(localId);
if ((scheduledDetection === null || scheduledDetection === void 0 ? void 0 : scheduledDetection.lastObservedText) === entry.lastObservedText) {
return;
}
clearTimer(timers, localId);
const timer = setTimeout(() => {
timers.delete(localId);
runPendingDetection(view, localId, api);
}, AUTO_DETECT_DEBOUNCE_MS);
timers.set(localId, {
lastObservedText: entry.lastObservedText,
timer
});
});
timers.forEach((_, localId) => {
if (!pendingLocalIds.has(localId)) {
clearTimer(timers, localId);
}
});
};