UNPKG

@atlaskit/editor-plugin-code-block

Version:

Code block plugin for @atlaskit/editor-core

120 lines (116 loc) 5.72 kB
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); } }); };