@atlaskit/editor-plugin-code-block 12.1.10 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cjs/codeBlockPlugin.js +9 -2
  3. package/dist/cjs/editor-commands/index.js +56 -4
  4. package/dist/cjs/pm-plugins/actions.js +3 -1
  5. package/dist/cjs/pm-plugins/auto-detect-state.js +11 -0
  6. package/dist/cjs/pm-plugins/auto-detect.js +57 -0
  7. package/dist/cjs/pm-plugins/main.js +1 -1
  8. package/dist/cjs/pm-plugins/toolbar.js +47 -11
  9. package/dist/cjs/pm-plugins/utils.js +0 -3
  10. package/dist/cjs/ui/CodeBlockLanguagePicker.js +15 -8
  11. package/dist/cjs/ui/LanguagePicker.js +7 -15
  12. package/dist/cjs/ui/language-picker-options.js +2 -1
  13. package/dist/cjs/utils/auto-detect-state.js +185 -0
  14. package/dist/cjs/utils/auto-detect-view.js +127 -0
  15. package/dist/cjs/utils/language-detect.js +126 -0
  16. package/dist/es2019/codeBlockPlugin.js +5 -1
  17. package/dist/es2019/editor-commands/index.js +52 -2
  18. package/dist/es2019/pm-plugins/actions.js +3 -1
  19. package/dist/es2019/pm-plugins/auto-detect-state.js +3 -0
  20. package/dist/es2019/pm-plugins/auto-detect.js +47 -0
  21. package/dist/es2019/pm-plugins/main.js +1 -1
  22. package/dist/es2019/pm-plugins/toolbar.js +41 -3
  23. package/dist/es2019/pm-plugins/utils.js +0 -3
  24. package/dist/es2019/ui/CodeBlockLanguagePicker.js +15 -8
  25. package/dist/es2019/ui/LanguagePicker.js +6 -14
  26. package/dist/es2019/ui/language-picker-options.js +2 -1
  27. package/dist/es2019/utils/auto-detect-state.js +179 -0
  28. package/dist/es2019/utils/auto-detect-view.js +108 -0
  29. package/dist/es2019/utils/language-detect.js +99 -0
  30. package/dist/esm/codeBlockPlugin.js +9 -2
  31. package/dist/esm/editor-commands/index.js +55 -3
  32. package/dist/esm/pm-plugins/actions.js +3 -1
  33. package/dist/esm/pm-plugins/auto-detect-state.js +5 -0
  34. package/dist/esm/pm-plugins/auto-detect.js +50 -0
  35. package/dist/esm/pm-plugins/main.js +1 -1
  36. package/dist/esm/pm-plugins/toolbar.js +47 -11
  37. package/dist/esm/pm-plugins/utils.js +0 -3
  38. package/dist/esm/ui/CodeBlockLanguagePicker.js +15 -8
  39. package/dist/esm/ui/LanguagePicker.js +7 -15
  40. package/dist/esm/ui/language-picker-options.js +2 -1
  41. package/dist/esm/utils/auto-detect-state.js +178 -0
  42. package/dist/esm/utils/auto-detect-view.js +120 -0
  43. package/dist/esm/utils/language-detect.js +119 -0
  44. package/dist/types/editor-commands/index.d.ts +2 -0
  45. package/dist/types/pm-plugins/actions.d.ts +2 -0
  46. package/dist/types/pm-plugins/auto-detect-state.d.ts +16 -0
  47. package/dist/types/pm-plugins/auto-detect.d.ts +5 -0
  48. package/dist/types/pm-plugins/utils.d.ts +1 -1
  49. package/dist/types/ui/CodeBlockLanguagePicker.d.ts +7 -1
  50. package/dist/types/ui/LanguagePicker.d.ts +4 -8
  51. package/dist/types/utils/auto-detect-state.d.ts +11 -0
  52. package/dist/types/utils/auto-detect-view.d.ts +8 -0
  53. package/dist/types/utils/language-detect.d.ts +3 -0
  54. package/dist/types-ts4.5/editor-commands/index.d.ts +2 -0
  55. package/dist/types-ts4.5/pm-plugins/actions.d.ts +2 -0
  56. package/dist/types-ts4.5/pm-plugins/auto-detect-state.d.ts +16 -0
  57. package/dist/types-ts4.5/pm-plugins/auto-detect.d.ts +5 -0
  58. package/dist/types-ts4.5/pm-plugins/utils.d.ts +1 -1
  59. package/dist/types-ts4.5/ui/CodeBlockLanguagePicker.d.ts +7 -1
  60. package/dist/types-ts4.5/ui/LanguagePicker.d.ts +4 -8
  61. package/dist/types-ts4.5/utils/auto-detect-state.d.ts +11 -0
  62. package/dist/types-ts4.5/utils/auto-detect-view.d.ts +8 -0
  63. package/dist/types-ts4.5/utils/language-detect.d.ts +3 -0
  64. package/package.json +12 -12
@@ -13,9 +13,38 @@ import { changeLanguage, copyContentToClipboardWithAnalytics, removeCodeBlockWit
13
13
  import { CodeBlockLanguagePicker } from '../ui/CodeBlockLanguagePicker';
14
14
  import { WrapIcon } from '../ui/icons/WrapIcon';
15
15
  import { NONE_LANGUAGE_VALUE, PLAIN_TEXT_LANGUAGE_VALUE } from '../ui/language-picker-options';
16
+ import { autoDetectPluginKey } from './auto-detect-state';
16
17
  import { provideVisualFeedbackForCopyButton, removeVisualFeedbackForCopyButton } from './codeBlockCopySelectionPlugin';
17
18
  import { createLanguageList, DEFAULT_LANGUAGES, getLanguageIdentifier } from './language-list';
18
19
  import { pluginKey } from './plugin-key';
20
+ const getAutoDetectPickerValue = ({
21
+ autoDetectEntry,
22
+ formatMessage,
23
+ language,
24
+ languagePickerOptions
25
+ }) => {
26
+ const defaultPickerValue = language ? languagePickerOptions.find(option => language === NONE_LANGUAGE_VALUE ? option.value === PLAIN_TEXT_LANGUAGE_VALUE : option.value === language || option.alias.includes(language)) : undefined;
27
+
28
+ // A weak re-detection records noneDetected but can leave a previously auto-detected
29
+ // language on the node. Keep showing "(detected)" only while that preserved language
30
+ // still matches the node language, so manual language changes do not inherit the label.
31
+ if (defaultPickerValue && ((autoDetectEntry === null || autoDetectEntry === void 0 ? void 0 : autoDetectEntry.detectionResult) === 'detected' || (autoDetectEntry === null || autoDetectEntry === void 0 ? void 0 : autoDetectEntry.detectionResult) === 'noneDetected' && autoDetectEntry.autoDetectedLanguage === language)) {
32
+ return {
33
+ ...defaultPickerValue,
34
+ label: formatMessage(codeBlockButtonMessages.detectedLanguage, {
35
+ language: defaultPickerValue.label
36
+ })
37
+ };
38
+ }
39
+ if ((autoDetectEntry === null || autoDetectEntry === void 0 ? void 0 : autoDetectEntry.detectionResult) === 'noneDetected' && !language) {
40
+ return {
41
+ alias: [],
42
+ label: formatMessage(codeBlockButtonMessages.noneDetected),
43
+ value: NONE_LANGUAGE_VALUE
44
+ };
45
+ }
46
+ return defaultPickerValue;
47
+ };
19
48
  export const getToolbarConfig = (allowCopyToClipboard = false, api, overrideLanguageName = undefined) => {
20
49
  const languageList = createLanguageList(overrideLanguageName ? DEFAULT_LANGUAGES.map(languageOption => ({
21
50
  ...languageOption,
@@ -29,7 +58,7 @@ export const getToolbarConfig = (allowCopyToClipboard = false, api, overrideLang
29
58
  return (state, {
30
59
  formatMessage
31
60
  }) => {
32
- var _api$editorViewMode, _api$editorViewMode$s, _api$decorations$acti, _api$decorations, _api$analytics, _codeBlockState$pos, _node$attrs, _languagePicker;
61
+ var _api$editorViewMode, _api$editorViewMode$s, _api$decorations$acti, _api$decorations, _api$analytics, _codeBlockState$pos, _node$attrs, _node$attrs2, _languagePicker;
33
62
  const isViewMode = (api === null || api === void 0 ? void 0 : (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : (_api$editorViewMode$s = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode$s === void 0 ? void 0 : _api$editorViewMode$s.mode) === 'view';
34
63
  const {
35
64
  hoverDecoration
@@ -48,6 +77,10 @@ export const getToolbarConfig = (allowCopyToClipboard = false, api, overrideLang
48
77
  const isWrapped = isCodeBlockWordWrapEnabled(node);
49
78
  const areLineNumbersVisible = areCodeBlockLineNumbersVisible(node);
50
79
  const language = node === null || node === void 0 ? void 0 : (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.language;
80
+ const localId = node === null || node === void 0 ? void 0 : (_node$attrs2 = node.attrs) === null || _node$attrs2 === void 0 ? void 0 : _node$attrs2.localId;
81
+ const autoDetectState = autoDetectPluginKey.getState(state);
82
+ const autoDetectEntry = expValEquals('platform_editor_code_block_auto_detection', 'isEnabled', true) && typeof localId === 'string' ? autoDetectState === null || autoDetectState === void 0 ? void 0 : autoDetectState.languageDetectionMap[localId] : undefined;
83
+
51
84
  // Keep fresh option objects for the legacy toolbar select so reopening it
52
85
  // continues to start from the top rather than preserving the previously
53
86
  // focused option by reference.
@@ -67,7 +100,12 @@ export const getToolbarConfig = (allowCopyToClipboard = false, api, overrideLang
67
100
  };
68
101
  let languagePicker;
69
102
  if (expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true) && fg('platform_editor_code_block_add_line_number_button')) {
70
- const defaultPickerValue = language ? languagePickerOptions.find(option => language === NONE_LANGUAGE_VALUE ? option.value === PLAIN_TEXT_LANGUAGE_VALUE : option.value === language || option.alias.includes(language)) : undefined;
103
+ const autoDetectPickerValue = getAutoDetectPickerValue({
104
+ autoDetectEntry,
105
+ formatMessage,
106
+ language,
107
+ languagePickerOptions
108
+ });
71
109
  languagePicker = {
72
110
  type: 'custom',
73
111
  fallback: [],
@@ -77,7 +115,7 @@ export const getToolbarConfig = (allowCopyToClipboard = false, api, overrideLang
77
115
  }
78
116
  return /*#__PURE__*/React.createElement(CodeBlockLanguagePicker, {
79
117
  api: api,
80
- defaultValue: defaultPickerValue,
118
+ defaultValue: autoDetectPickerValue,
81
119
  editorView: view,
82
120
  filterOption: languageListFilter,
83
121
  formatMessage: formatMessage,
@@ -34,8 +34,5 @@ export function getAllChangedCodeBlocksInTransaction(tr) {
34
34
  });
35
35
  });
36
36
  });
37
- if (changedCodeBlocks.length < 1) {
38
- return null;
39
- }
40
37
  return changedCodeBlocks;
41
38
  }
@@ -1,4 +1,7 @@
1
1
  import React, { useCallback, useState } from 'react';
2
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
3
+ import { changeLanguage, detectLanguage } from '../editor-commands';
4
+ import { DETECT_LANGUAGE_VALUE } from './language-picker-options';
2
5
  import { LanguagePicker } from './LanguagePicker';
3
6
  import { getRecentLanguages, saveRecentLanguage } from './recent-languages';
4
7
  export const CodeBlockLanguagePicker = ({
@@ -9,23 +12,27 @@ export const CodeBlockLanguagePicker = ({
9
12
  formatMessage,
10
13
  languagePickerOptions
11
14
  }) => {
15
+ var _api$analytics2;
12
16
  const [recentLanguageValues, setRecentLanguageValues] = useState(() => getRecentLanguages());
13
17
  const refreshRecentLanguages = useCallback(() => {
14
18
  setRecentLanguageValues(getRecentLanguages());
15
19
  }, []);
16
- const handleLanguageSelect = useCallback(language => {
17
- saveRecentLanguage(language);
18
- setRecentLanguageValues(getRecentLanguages());
19
- }, []);
20
+ const handleSelection = useCallback((option, selectionSource) => {
21
+ var _api$analytics;
22
+ const command = option.value === DETECT_LANGUAGE_VALUE && expValEquals('platform_editor_code_block_auto_detection', 'isEnabled', true) ? detectLanguage() : changeLanguage(api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions)(option.value, selectionSource);
23
+ const commandSucceeded = command(editorView.state, editorView.dispatch);
24
+ if (commandSucceeded && option.value !== DETECT_LANGUAGE_VALUE) {
25
+ saveRecentLanguage(option.value);
26
+ setRecentLanguageValues(getRecentLanguages());
27
+ }
28
+ }, [api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions, editorView]);
20
29
  return /*#__PURE__*/React.createElement(LanguagePicker, {
21
- api: api,
22
30
  defaultValue: defaultValue,
23
- editorView: editorView,
24
31
  filterOption: filterOption,
25
32
  formatMessage: formatMessage,
26
33
  languagePickerOptions: languagePickerOptions,
27
34
  recentLanguageValues: recentLanguageValues,
28
- onLanguageSelect: handleLanguageSelect,
29
- onMenuOpen: refreshRecentLanguages
35
+ onMenuOpen: refreshRecentLanguages,
36
+ onSelection: handleSelection
30
37
  });
31
38
  };
@@ -8,7 +8,6 @@ import { akEditorLineHeight } from '@atlaskit/editor-shared-styles';
8
8
  import ChevronDownIcon from '@atlaskit/icon/core/chevron-down';
9
9
  import { Box } from '@atlaskit/primitives/compiled';
10
10
  import { PopupSelect, components } from '@atlaskit/select';
11
- import { changeLanguage } from '../editor-commands';
12
11
  import { createGroupedLanguageOptions } from './language-picker-options';
13
12
  const pickerOptionStyles = null;
14
13
  const styles = {
@@ -59,18 +58,15 @@ const getRecentlyUsedLanguages = (recentLanguageValues, optionsByValue) => {
59
58
  return recentlyUsedLanguages;
60
59
  };
61
60
  export const LanguagePicker = ({
62
- api,
63
61
  defaultValue,
64
- editorView,
65
62
  filterOption,
66
63
  formatMessage,
67
64
  languagePickerOptions,
68
65
  recentLanguageValues = [],
69
- onLanguageSelect,
70
- onMenuOpen
66
+ onMenuOpen,
67
+ onSelection
71
68
  }) => {
72
- var _api$analytics, _defaultValue$label;
73
- const editorAnalyticsAPI = api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions;
69
+ var _defaultValue$label;
74
70
  const label = (_defaultValue$label = defaultValue === null || defaultValue === void 0 ? void 0 : defaultValue.label) !== null && _defaultValue$label !== void 0 ? _defaultValue$label : formatMessage(codeBlockButtonMessages.selectLanguage);
75
71
  const selectLanguageLabel = formatMessage(codeBlockButtonMessages.selectLanguage);
76
72
  const [hasSearchQuery, setHasSearchQuery] = useState(false);
@@ -90,11 +86,8 @@ export const LanguagePicker = ({
90
86
  }
91
87
  const isSearchSelection = inputValueRef.current.trim().length > 0;
92
88
  const selectionSource = isSearchSelection ? 'search' : (_option$selectionSour = option.selectionSource) !== null && _option$selectionSour !== void 0 ? _option$selectionSour : 'all';
93
- const commandSucceeded = changeLanguage(editorAnalyticsAPI)(option.value, selectionSource)(editorView.state, editorView.dispatch);
94
- if (commandSucceeded) {
95
- onLanguageSelect === null || onLanguageSelect === void 0 ? void 0 : onLanguageSelect(option.value);
96
- }
97
- }, [editorAnalyticsAPI, editorView, onLanguageSelect]);
89
+ onSelection(option, selectionSource);
90
+ }, [onSelection]);
98
91
  const handleInputChange = useCallback((newInputValue, actionMeta) => {
99
92
  // React-select clears the input as part of selecting a value before onChange fires.
100
93
  // Keep the last user-typed query so handleChange can report search selections correctly.
@@ -119,9 +112,8 @@ export const LanguagePicker = ({
119
112
  appearance: "subtle",
120
113
  isSelected: isOpen,
121
114
  "aria-controls": ariaControls,
122
- "aria-label": selectLanguageLabel,
123
115
  testId: "code-block-language-picker-trigger"
124
- }, label)), [label, selectLanguageLabel]);
116
+ }, label)), [label]);
125
117
  return /*#__PURE__*/React.createElement(PopupSelect, {
126
118
  components: popupSelectComponents,
127
119
  filterOption: filterOption,
@@ -1,4 +1,5 @@
1
1
  import { codeBlockButtonMessages } from '@atlaskit/editor-common/messages';
2
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
2
3
  export const NONE_LANGUAGE_VALUE = 'none';
3
4
  export const DETECT_LANGUAGE_VALUE = 'autodetect';
4
5
  export const PLAIN_TEXT_LANGUAGE_VALUE = 'text';
@@ -16,7 +17,7 @@ export const createGroupedLanguageOptions = ({
16
17
  const recentlyUsedLanguageValues = new Set(recentlyUsedLanguages.map(language => language.value));
17
18
  const allLanguages = languages.filter(language => language.value !== NONE_LANGUAGE_VALUE && language.value !== PLAIN_TEXT_LANGUAGE_VALUE && !recentlyUsedLanguageValues.has(language.value));
18
19
  const plainTextOption = languages.find(language => language.value === PLAIN_TEXT_LANGUAGE_VALUE);
19
- const pinnedOptions = [getDetectLanguageOption(formatMessage)];
20
+ const pinnedOptions = expValEquals('platform_editor_code_block_auto_detection', 'isEnabled', true) ? [getDetectLanguageOption(formatMessage)] : [];
20
21
  if (plainTextOption) {
21
22
  pinnedOptions.push({
22
23
  ...plainTextOption,
@@ -0,0 +1,179 @@
1
+ import { getInsertedCodeBlocksInTransaction } from '@atlaskit/editor-common/code-block';
2
+ import { getAllChangedCodeBlocksInTransaction } from '../pm-plugins/utils';
3
+ const MIN_AUTO_DETECT_TEXT_LENGTH = 20;
4
+ export const shouldTriggerLargeChangeDetection = (lastObservedText, text) => {
5
+ if (!lastObservedText) {
6
+ return text.length > 0;
7
+ }
8
+ return Math.abs(text.length - lastObservedText.length) > lastObservedText.length / 2;
9
+ };
10
+ export const getFirstLine = text => {
11
+ var _text$split$;
12
+ return (_text$split$ = text.split('\n')[0]) !== null && _text$split$ !== void 0 ? _text$split$ : '';
13
+ };
14
+ export const hasEnoughTextForAutoDetection = text => text.trim().length >= MIN_AUTO_DETECT_TEXT_LENGTH;
15
+ export const getLocalId = node => typeof node.attrs.localId === 'string' ? node.attrs.localId : null;
16
+ export const createAutoDetectEntry = (node, pos, isPending, previous) => ({
17
+ lastObservedText: node.textContent,
18
+ lastObservedFirstLine: getFirstLine(node.textContent),
19
+ isPending,
20
+ detectionResult: previous === null || previous === void 0 ? void 0 : previous.detectionResult,
21
+ autoDetectedLanguage: previous === null || previous === void 0 ? void 0 : previous.autoDetectedLanguage,
22
+ pos
23
+ });
24
+ export const queueAutoDetection = (languageDetectionMap, node, pos, isPending) => {
25
+ const localId = getLocalId(node);
26
+ if (!localId) {
27
+ return languageDetectionMap;
28
+ }
29
+ return {
30
+ ...languageDetectionMap,
31
+ [localId]: createAutoDetectEntry(node, pos, isPending, languageDetectionMap[localId])
32
+ };
33
+ };
34
+ export const removeAutoDetection = (languageDetectionMap, localId) => {
35
+ if (!languageDetectionMap[localId]) {
36
+ return languageDetectionMap;
37
+ }
38
+ const nextLanguageDetectionMap = {
39
+ ...languageDetectionMap
40
+ };
41
+ delete nextLanguageDetectionMap[localId];
42
+ return nextLanguageDetectionMap;
43
+ };
44
+ const getCodeBlockLocalIdsRemovedFromChangedRanges = (tr, codeBlockType) => {
45
+ const localIds = new Set();
46
+ tr.steps.forEach((step, stepIndex) => {
47
+ const docAtStep = tr.docs[stepIndex];
48
+ step.getMap().forEach((oldStart, oldEnd) => {
49
+ if (oldStart === oldEnd) {
50
+ return;
51
+ }
52
+ const clampedOldEnd = Math.min(oldEnd, docAtStep.content.size);
53
+ docAtStep.nodesBetween(oldStart, clampedOldEnd, (node, pos) => {
54
+ if (node.type !== codeBlockType) {
55
+ return true;
56
+ }
57
+ const isWholeCodeBlockRemoved = pos >= oldStart && pos + node.nodeSize <= clampedOldEnd;
58
+ if (!isWholeCodeBlockRemoved) {
59
+ return false;
60
+ }
61
+ const localId = getLocalId(node);
62
+ if (localId) {
63
+ localIds.add(localId);
64
+ }
65
+ return false;
66
+ });
67
+ });
68
+ });
69
+ return localIds;
70
+ };
71
+ const getCodeBlockTransactionChanges = (tr, codeBlockType) => {
72
+ const insertedNodesWithPos = getInsertedCodeBlocksInTransaction(tr, codeBlockType);
73
+ const removedFromChangedRangesLocalIds = getCodeBlockLocalIdsRemovedFromChangedRanges(tr, codeBlockType);
74
+ const insertedLocalIds = new Set();
75
+ const insertedCodeBlocks = [];
76
+ insertedNodesWithPos.forEach(({
77
+ node,
78
+ pos
79
+ }) => {
80
+ const localId = getLocalId(node);
81
+ if (!localId) {
82
+ return;
83
+ }
84
+ insertedLocalIds.add(localId);
85
+ if (!removedFromChangedRangesLocalIds.has(localId)) {
86
+ insertedCodeBlocks.push({
87
+ localId,
88
+ node,
89
+ pos
90
+ });
91
+ }
92
+ });
93
+ const deletedLocalIds = new Set();
94
+ removedFromChangedRangesLocalIds.forEach(localId => {
95
+ if (!insertedLocalIds.has(localId)) {
96
+ deletedLocalIds.add(localId);
97
+ }
98
+ });
99
+ return {
100
+ deletedLocalIds,
101
+ insertedCodeBlocks
102
+ };
103
+ };
104
+ export const updateAutoDetectState = (tr, pluginState) => {
105
+ const {
106
+ codeBlock
107
+ } = tr.doc.type.schema.nodes;
108
+ const isPaste = tr.getMeta('paste') === true || tr.getMeta('uiEvent') === 'paste';
109
+ const isExternalContentChange = tr.getMeta('replaceDocument') === true || tr.getMeta('isRemote') === true;
110
+ const hasTrackedEntries = Object.keys(pluginState.languageDetectionMap).length > 0;
111
+
112
+ // Page loads and remote edits should not start auto-detection for code blocks.
113
+ if (!hasTrackedEntries && isExternalContentChange) {
114
+ return pluginState.languageDetectionMap;
115
+ }
116
+
117
+ // Existing entries still need mapping/deletion cleanup, but external edits should not refresh text.
118
+ const changedCodeBlockNodes = isExternalContentChange ? [] : getAllChangedCodeBlocksInTransaction(tr);
119
+ if (!hasTrackedEntries && !changedCodeBlockNodes.length) {
120
+ return pluginState.languageDetectionMap;
121
+ }
122
+ const {
123
+ deletedLocalIds,
124
+ insertedCodeBlocks
125
+ } = getCodeBlockTransactionChanges(tr, codeBlock);
126
+ let languageDetectionMap = hasTrackedEntries ? Object.fromEntries(Object.entries(pluginState.languageDetectionMap).map(([localId, entry]) => [localId, {
127
+ ...entry,
128
+ pos: tr.mapping.map(entry.pos)
129
+ }])) : pluginState.languageDetectionMap;
130
+ if (!isExternalContentChange) {
131
+ insertedCodeBlocks.forEach(({
132
+ localId,
133
+ node,
134
+ pos
135
+ }) => {
136
+ if (node.attrs.language) {
137
+ languageDetectionMap = removeAutoDetection(languageDetectionMap, localId);
138
+ return;
139
+ }
140
+ languageDetectionMap = queueAutoDetection(languageDetectionMap, node, pos, hasEnoughTextForAutoDetection(node.textContent));
141
+ });
142
+ changedCodeBlockNodes.forEach(({
143
+ node,
144
+ pos
145
+ }) => {
146
+ const localId = getLocalId(node);
147
+ if (!localId || !languageDetectionMap[localId]) {
148
+ return;
149
+ }
150
+ const currentLanguage = node.attrs.language;
151
+ const previousEntry = languageDetectionMap[localId];
152
+ if (currentLanguage && currentLanguage !== previousEntry.autoDetectedLanguage) {
153
+ languageDetectionMap = removeAutoDetection(languageDetectionMap, localId);
154
+ return;
155
+ }
156
+ const text = node.textContent;
157
+ const firstLine = getFirstLine(text);
158
+ const shouldTriggerDetection = previousEntry.isPending || isPaste || firstLine !== previousEntry.lastObservedFirstLine || shouldTriggerLargeChangeDetection(previousEntry.lastObservedText, text);
159
+ const isPending = hasEnoughTextForAutoDetection(text) && shouldTriggerDetection;
160
+
161
+ // Only pending detection refreshes the text snapshot; otherwise gradual typing
162
+ // should continue comparing against the last attempted detection.
163
+ languageDetectionMap = isPending ? queueAutoDetection(languageDetectionMap, node, pos, true) : {
164
+ ...languageDetectionMap,
165
+ [localId]: {
166
+ ...previousEntry,
167
+ isPending: false,
168
+ pos
169
+ }
170
+ };
171
+ });
172
+ }
173
+ deletedLocalIds.forEach(localId => {
174
+ if (languageDetectionMap[localId]) {
175
+ languageDetectionMap = removeAutoDetection(languageDetectionMap, localId);
176
+ }
177
+ });
178
+ return languageDetectionMap;
179
+ };
@@ -0,0 +1,108 @@
1
+ import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
2
+ import { ACTIONS } from '../pm-plugins/actions';
3
+ import { autoDetectPluginKey } from '../pm-plugins/auto-detect-state';
4
+ import { createAutoDetectEntry } from './auto-detect-state';
5
+ import { detectLanguage } from './language-detect';
6
+ const AUTO_DETECT_DEBOUNCE_MS = 500;
7
+
8
+ // Stored positions are mapped through transactions; verify the localId before using them.
9
+ const getCodeBlockFromEntry = (view, localId, entry) => {
10
+ const node = view.state.doc.nodeAt(entry.pos);
11
+ const codeBlockType = view.state.schema.nodes.codeBlock;
12
+ if ((node === null || node === void 0 ? void 0 : node.type) === codeBlockType && node.attrs.localId === localId) {
13
+ return {
14
+ node,
15
+ pos: entry.pos
16
+ };
17
+ }
18
+ return null;
19
+ };
20
+
21
+ // Runs after debounce, so it must re-read current editor state before applying language changes.
22
+ const runPendingDetection = (view, localId, api) => {
23
+ var _api$core;
24
+ const pluginState = autoDetectPluginKey.getState(view.state);
25
+ const entry = pluginState === null || pluginState === void 0 ? void 0 : pluginState.languageDetectionMap[localId];
26
+ if (!(entry !== null && entry !== void 0 && entry.isPending)) {
27
+ return;
28
+ }
29
+ const found = getCodeBlockFromEntry(view, localId, entry);
30
+ if (!found) {
31
+ return;
32
+ }
33
+ const detectedLanguage = detectLanguage(found.node.textContent);
34
+ const detectionResult = detectedLanguage ? 'detected' : 'noneDetected';
35
+ // Keep a previous auto-detected language when the latest snippet is too weak to classify.
36
+ const shouldPreserveAutoDetectedLanguage = !detectedLanguage && Boolean(entry.autoDetectedLanguage) && found.node.attrs.language === entry.autoDetectedLanguage;
37
+ const nextEntry = {
38
+ ...createAutoDetectEntry(found.node, found.pos, false, entry),
39
+ detectionResult,
40
+ autoDetectedLanguage: detectedLanguage !== null && detectedLanguage !== void 0 ? detectedLanguage : entry.autoDetectedLanguage
41
+ };
42
+
43
+ // If there is no confident detection, record the result without clearing user-visible language.
44
+ const shouldOnlyUpdateDetectionState = !detectedLanguage && (!found.node.attrs.language || shouldPreserveAutoDetectedLanguage);
45
+ api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
46
+ tr
47
+ }) => {
48
+ var _api$analytics;
49
+ if (!shouldOnlyUpdateDetectionState) {
50
+ tr.setNodeMarkup(found.pos, undefined, {
51
+ ...found.node.attrs,
52
+ language: detectedLanguage
53
+ }, found.node.marks);
54
+ }
55
+ tr.setMeta(autoDetectPluginKey, {
56
+ type: ACTIONS.SET_AUTO_DETECT_ENTRY,
57
+ data: {
58
+ localId,
59
+ entry: nextEntry
60
+ }
61
+ });
62
+ api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions.attachAnalyticsEvent({
63
+ action: ACTION.LANGUAGE_AUTO_DETECTED,
64
+ actionSubject: ACTION_SUBJECT.CODE_BLOCK,
65
+ attributes: {
66
+ language: detectedLanguage !== null && detectedLanguage !== void 0 ? detectedLanguage : 'none',
67
+ detectionResult
68
+ },
69
+ eventType: EVENT_TYPE.TRACK
70
+ })(tr);
71
+ return tr;
72
+ });
73
+ };
74
+ const clearTimer = (timers, localId) => {
75
+ const scheduledDetection = timers.get(localId);
76
+ if (scheduledDetection) {
77
+ clearTimeout(scheduledDetection.timer);
78
+ timers.delete(localId);
79
+ }
80
+ };
81
+
82
+ // Keeps one debounce timer per pending code block and drops timers for stale entries.
83
+ export const syncPendingDetectionTimers = (view, timers, api) => {
84
+ var _pluginState$language;
85
+ const pluginState = autoDetectPluginKey.getState(view.state);
86
+ 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);
87
+ const pendingLocalIds = new Set(pendingEntries.map(([localId]) => localId));
88
+ pendingEntries.forEach(([localId, entry]) => {
89
+ const scheduledDetection = timers.get(localId);
90
+ if ((scheduledDetection === null || scheduledDetection === void 0 ? void 0 : scheduledDetection.lastObservedText) === entry.lastObservedText) {
91
+ return;
92
+ }
93
+ clearTimer(timers, localId);
94
+ const timer = setTimeout(() => {
95
+ timers.delete(localId);
96
+ runPendingDetection(view, localId, api);
97
+ }, AUTO_DETECT_DEBOUNCE_MS);
98
+ timers.set(localId, {
99
+ lastObservedText: entry.lastObservedText,
100
+ timer
101
+ });
102
+ });
103
+ timers.forEach((_, localId) => {
104
+ if (!pendingLocalIds.has(localId)) {
105
+ clearTimer(timers, localId);
106
+ }
107
+ });
108
+ };
@@ -0,0 +1,99 @@
1
+ /* eslint-disable require-unicode-regexp */
2
+
3
+ // Conservative weighted-regex heuristic for common high-confidence snippets, not a full classifier.
4
+ // Ambiguous snippets intentionally return null so users can select the language manually.
5
+ const MAX_DETECTION_CHARS = 10_000;
6
+ const MIN_DETECTION_SCORE = 3; // Require at least one medium-confidence signal before auto-selecting.
7
+ const MIN_SCORE_GAP = 2; // Avoid auto-selecting when top two languages are too close to distinguish.
8
+
9
+ const hasPattern = (code, pattern) => {
10
+ // Reset stateful regexes so repeated tests always start at the beginning.
11
+ pattern.lastIndex = 0;
12
+ return pattern.test(code);
13
+ };
14
+ const scorePatterns = (code, patterns) => patterns.reduce((score, [pattern, value]) => score + (hasPattern(code, pattern) ? value : 0), 0);
15
+ const looksLikeHtmlTagPair = code => {
16
+ const openTags = new Set();
17
+ const tagPattern = /<\/?([a-z][a-z0-9-]*)\b[^<>]{0,500}>/gi;
18
+ let match;
19
+ while ((match = tagPattern.exec(code)) !== null) {
20
+ const [tag, tagName] = match;
21
+ if (tag.endsWith('/>')) {
22
+ continue;
23
+ }
24
+ if (tag.startsWith('</')) {
25
+ if (openTags.has(tagName.toLowerCase())) {
26
+ return true;
27
+ }
28
+ } else {
29
+ openTags.add(tagName.toLowerCase());
30
+ }
31
+ }
32
+ return false;
33
+ };
34
+ const looksLikeJson = code => {
35
+ const trimmed = code.trim();
36
+ if (!/^[{[]/.test(trimmed) || !/[}\]]$/.test(trimmed)) {
37
+ return false;
38
+ }
39
+ try {
40
+ const parsed = JSON.parse(trimmed);
41
+ return parsed !== null && typeof parsed === 'object';
42
+ } catch {
43
+ return false;
44
+ }
45
+ };
46
+ const getLanguageScores = code => [{
47
+ language: 'json',
48
+ score: looksLikeJson(code) ? 8 : 0
49
+ }, {
50
+ language: 'html',
51
+ score: scorePatterns(code, [[/<!doctype\s+html/i, 5], [/<\/?(?:html|head|body|div|span|script|style|section|template)\b/i, 2]]) + (looksLikeHtmlTagPair(code) ? 3 : 0)
52
+ }, {
53
+ language: 'css',
54
+ score: scorePatterns(code, [[/@[\w-]+\s+[^{]+\{/, 3], [/[.#]?[-_a-zA-Z][-_a-zA-Z0-9\s,.:#>+~*\[\]='"]+\{[^}]*\b(?:color|display|margin|padding|font|background|border|width|height|grid|flex)\s*:/, 5], [/\b(?:color|display|margin|padding|font-size|background|border|width|height)\s*:\s*[^;{}]+;/, 3]])
55
+ }, {
56
+ language: 'sql',
57
+ score: scorePatterns(code, [[/\bSELECT\b[\s\S]+\bFROM\b/i, 5], [/\b(?:INSERT\s+INTO|UPDATE\b[\s\S]+\bSET\b|DELETE\s+FROM|CREATE\s+TABLE|ALTER\s+TABLE)\b/i, 5], [/\b(?:JOIN|WHERE|GROUP\s+BY|ORDER\s+BY|LIMIT)\b/i, 2]])
58
+ }, {
59
+ language: 'typescript',
60
+ score: scorePatterns(code, [[/\b(?:interface|type)\s+[A-Z_$][\w$]*(?:\s*[=<{])/, 5], [/\bimport\s+type\b|\bexport\s+type\b/, 4], [/\b(?:const|let|var)\s+[\w$]+\s*:\s*[A-Za-z_$][\w$<>|\[\],\s]*/, 4], [/\)\s*:\s*(?:Promise<)?[A-Za-z_$][\w$<>|\[\],\s]*(?:>|\s)?\s*=>?\s*[{;]/, 3], [/\b(?:as\s+const|implements\s+[A-Z_$]|enum\s+[A-Z_$])/, 3]])
61
+ }, {
62
+ language: 'javascript',
63
+ score: scorePatterns(code, [[/\b(?:const|let|var)\s+[\w$]+\s*=/, 3], [/\bfunction\s*[\w$]*\s*\([^)]*\)\s*\{/, 3], [/=>\s*(?:\{|[\w$'"`(])/, 3], [/\b(?:import|export)\s+(?:[\w${}*,\s]+\s+from\s+)?['"][^'"]+['"]/, 3], [/\b(?:console\.log|document\.|window\.|require\()|\bmodule\.exports\b/, 3]])
64
+ }, {
65
+ language: 'python',
66
+ score: scorePatterns(code, [[/^\s*def\s+[a-zA-Z_]\w*\([^)]*\)\s*:/m, 5], [/^\s*class\s+[A-Z_]\w*(?:\([^)]*\))?\s*:/m, 4], [/^\s*(?:from\s+[\w.]+\s+import\s+\w+|import\s+[\w.]+)/m, 3], [/^\s*(?:if|elif|else|for|while|try|except)\b[^\n]*:\s*$/m, 2], [/\bprint\([^)]*\)/, 2]])
67
+ }, {
68
+ language: 'java',
69
+ score: scorePatterns(code, [[/\bpublic\s+(?:final\s+)?class\s+[A-Z]\w*/, 5], [/\bpublic\s+static\s+void\s+main\s*\(\s*String\[\]/, 5], [/\bSystem\.out\.println\s*\(/, 4], [/\b(?:private|protected|public)\s+(?:static\s+)?(?:final\s+)?[A-Z]\w*(?:<[^>]+>)?\s+\w+\s*[;(=]/, 3]])
70
+ }, {
71
+ language: 'go',
72
+ score: scorePatterns(code, [[/^\s*package\s+\w+/m, 5], [/\bfunc\s+(?:\([^)]*\)\s*)?[A-Za-z_]\w*\s*\([^)]*\)\s*(?:[A-Za-z_*\[\]]+\s*)?\{/, 5], [/\bfmt\.(?:Println|Printf)\s*\(/, 3], [/\bimport\s+\(\s*[\s\S]*?\)/, 2]])
73
+ }, {
74
+ language: 'ruby',
75
+ score: scorePatterns(code, [[/^\s*def\s+[a-z_]\w*[!?=]?/m, 4], [/^\s*class\s+[A-Z]\w*(?:\s*<\s*[A-Z]\w*)?/m, 3], [/^\s*end\s*$/m, 3], [/\b(?:puts|require|attr_reader|attr_accessor)\b/, 3], [/\bdo\s*\|[^|]+\|/, 2]])
76
+ }, {
77
+ language: 'rust',
78
+ score: scorePatterns(code, [[/\bfn\s+[a-z_]\w*\s*\([^)]*\)\s*(?:->\s*[A-Za-z_:<>]+\s*)?\{/, 5], [/\blet\s+mut\s+\w+/, 3], [/\bprintln!\s*\(/, 4], [/\buse\s+(?:std|crate|super)::/, 3], [/\b(?:impl|pub\s+struct|enum)\s+[A-Z]\w*/, 3]])
79
+ }, {
80
+ language: 'shell',
81
+ score: scorePatterns(code, [[/^#!\/usr\/bin\/(?:env\s+)?(?:ba|z|k)?sh\b/m, 6], [/^\s*(?:if|for|while)\b[\s\S]+\b(?:then|do)\b/m, 4], [/^\s*(?:fi|done)\s*$/m, 3], [/\b(?:echo|export|cd|grep|sed|awk|curl)\b[\s\S]*(?:\$\w+|&&|\|)/, 3], [/\$\{?[A-Z_][A-Z0-9_]*\}?/, 2]])
82
+ }];
83
+ export const detectLanguage = code => {
84
+ const trimmedCode = code.trim();
85
+ if (trimmedCode.length < 8) {
86
+ return null;
87
+ }
88
+ const codeForDetection = trimmedCode.slice(0, MAX_DETECTION_CHARS);
89
+ const [best, secondBest] = getLanguageScores(codeForDetection).filter(({
90
+ score
91
+ }) => score > 0).sort((a, b) => b.score - a.score);
92
+ if (!best || best.score < MIN_DETECTION_SCORE) {
93
+ return null;
94
+ }
95
+ if (secondBest && best.score - secondBest.score < MIN_SCORE_GAP) {
96
+ return null;
97
+ }
98
+ return best.language;
99
+ };
@@ -1,4 +1,5 @@
1
1
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
3
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
4
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
4
5
  import React from 'react';
@@ -11,6 +12,7 @@ import { fg } from '@atlaskit/platform-feature-flags';
11
12
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
12
13
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
13
14
  import { createInsertCodeBlockTransaction, insertCodeBlockWithAnalytics } from './editor-commands';
15
+ import { createAutoDetectPlugin } from './pm-plugins/auto-detect';
14
16
  import { codeBlockAutoFullStopTransformPlugin } from './pm-plugins/codeBlockAutoFullStopTransformPlugin';
15
17
  import { codeBlockCopySelectionPlugin, copySelectionPluginKey } from './pm-plugins/codeBlockCopySelectionPlugin';
16
18
  import ideUX from './pm-plugins/ide-ux';
@@ -74,7 +76,12 @@ var codeBlockPlugin = function codeBlockPlugin(_ref) {
74
76
  var schema = _ref3.schema;
75
77
  return createCodeBlockInputRule(schema, api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions);
76
78
  }
77
- }, {
79
+ }].concat(_toConsumableArray(expValEquals('platform_editor_code_block_auto_detection', 'isEnabled', true) ? [{
80
+ name: 'codeBlockAutoDetect',
81
+ plugin: function plugin() {
82
+ return createAutoDetectPlugin(api);
83
+ }
84
+ }] : []), [{
78
85
  name: 'codeBlockIDEKeyBindings',
79
86
  plugin: function plugin() {
80
87
  return ideUX(api);
@@ -95,7 +102,7 @@ var codeBlockPlugin = function codeBlockPlugin(_ref) {
95
102
  plugin: function plugin() {
96
103
  return codeBlockAutoFullStopTransformPlugin();
97
104
  }
98
- }];
105
+ }]);
99
106
  },
100
107
  // Workaround for a firefox issue where dom selection is off sync
101
108
  // https://product-fabric.atlassian.net/browse/ED-12442