@atlaskit/editor-plugin-code-block 13.0.0 → 13.1.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 (51) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/cjs/codeBlockPlugin.js +14 -2
  3. package/dist/cjs/editor-commands/index.js +182 -7
  4. package/dist/cjs/pm-plugins/actions.js +6 -3
  5. package/dist/cjs/pm-plugins/main.js +17 -1
  6. package/dist/cjs/pm-plugins/toolbar.js +20 -3
  7. package/dist/cjs/ui/CodeBlockLanguagePicker.js +2 -2
  8. package/dist/cjs/ui/FormatCodeErrorFlag.js +70 -0
  9. package/dist/cjs/ui/language-picker-options.js +2 -2
  10. package/dist/cjs/utils/format-code/format-code-state.js +81 -0
  11. package/dist/cjs/utils/format-code/formatter-impl.js +15 -0
  12. package/dist/cjs/utils/format-code/formatter.js +86 -0
  13. package/dist/es2019/codeBlockPlugin.js +13 -3
  14. package/dist/es2019/editor-commands/index.js +179 -2
  15. package/dist/es2019/pm-plugins/actions.js +6 -3
  16. package/dist/es2019/pm-plugins/main.js +18 -1
  17. package/dist/es2019/pm-plugins/toolbar.js +21 -4
  18. package/dist/es2019/ui/CodeBlockLanguagePicker.js +2 -2
  19. package/dist/es2019/ui/FormatCodeErrorFlag.js +62 -0
  20. package/dist/es2019/ui/language-picker-options.js +2 -2
  21. package/dist/es2019/utils/format-code/format-code-state.js +82 -0
  22. package/dist/es2019/utils/format-code/formatter-impl.js +10 -0
  23. package/dist/es2019/utils/format-code/formatter.js +47 -0
  24. package/dist/esm/codeBlockPlugin.js +14 -2
  25. package/dist/esm/editor-commands/index.js +181 -6
  26. package/dist/esm/pm-plugins/actions.js +6 -3
  27. package/dist/esm/pm-plugins/main.js +17 -1
  28. package/dist/esm/pm-plugins/toolbar.js +21 -4
  29. package/dist/esm/ui/CodeBlockLanguagePicker.js +2 -2
  30. package/dist/esm/ui/FormatCodeErrorFlag.js +61 -0
  31. package/dist/esm/ui/language-picker-options.js +2 -2
  32. package/dist/esm/utils/format-code/format-code-state.js +74 -0
  33. package/dist/esm/utils/format-code/formatter-impl.js +9 -0
  34. package/dist/esm/utils/format-code/formatter.js +75 -0
  35. package/dist/types/codeBlockPluginType.d.ts +3 -0
  36. package/dist/types/editor-commands/index.d.ts +6 -1
  37. package/dist/types/pm-plugins/actions.d.ts +6 -3
  38. package/dist/types/pm-plugins/main-state.d.ts +16 -0
  39. package/dist/types/ui/FormatCodeErrorFlag.d.ts +6 -0
  40. package/dist/types/utils/format-code/format-code-state.d.ts +4 -0
  41. package/dist/types/utils/format-code/formatter-impl.d.ts +5 -0
  42. package/dist/types/utils/format-code/formatter.d.ts +25 -0
  43. package/dist/types-ts4.5/codeBlockPluginType.d.ts +3 -0
  44. package/dist/types-ts4.5/editor-commands/index.d.ts +6 -1
  45. package/dist/types-ts4.5/pm-plugins/actions.d.ts +6 -3
  46. package/dist/types-ts4.5/pm-plugins/main-state.d.ts +16 -0
  47. package/dist/types-ts4.5/ui/FormatCodeErrorFlag.d.ts +6 -0
  48. package/dist/types-ts4.5/utils/format-code/format-code-state.d.ts +4 -0
  49. package/dist/types-ts4.5/utils/format-code/formatter-impl.d.ts +5 -0
  50. package/dist/types-ts4.5/utils/format-code/formatter.d.ts +32 -0
  51. package/package.json +12 -8
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.formatCode = void 0;
7
+ var formatCode = exports.formatCode = function formatCode(_ref) {
8
+ var content = _ref.content,
9
+ language = _ref.language;
10
+ return Promise.resolve({
11
+ content: content,
12
+ language: language,
13
+ status: 'unchanged'
14
+ });
15
+ };
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.preloadFormatterOnIntent = exports.preloadFormatterModule = exports.isSupportedFormatLanguage = exports.formatCode = void 0;
8
+ var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
9
+ var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
10
+ var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
11
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != (0, _typeof2.default)(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t3 in e) "default" !== _t3 && {}.hasOwnProperty.call(e, _t3) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t3)) && (i.get || i.set) ? o(f, _t3, i) : f[_t3] = e[_t3]); return f; })(e, t); }
12
+ var supportedFormatLanguages = ['json', 'javascript', 'jsx', 'typescript', 'tsx', 'sql'];
13
+ var isSupportedFormatLanguage = exports.isSupportedFormatLanguage = function isSupportedFormatLanguage(language) {
14
+ return supportedFormatLanguages.includes(language);
15
+ };
16
+ var formatterModulePromise;
17
+ var preloadFormatterModule = exports.preloadFormatterModule = function preloadFormatterModule() {
18
+ if (!formatterModulePromise) {
19
+ formatterModulePromise = Promise.resolve().then(function () {
20
+ return _interopRequireWildcard(require( /* webpackChunkName: "@atlaskit-internal_editor-plugin-code-block-formatter" */'./formatter-impl'));
21
+ }).catch(function (error) {
22
+ formatterModulePromise = undefined;
23
+ throw error;
24
+ });
25
+ }
26
+ return formatterModulePromise;
27
+ };
28
+ var preloadFormatterOnIntent = exports.preloadFormatterOnIntent = function preloadFormatterOnIntent() {
29
+ return function (_state, dispatch) {
30
+ if (!dispatch) {
31
+ // Hover/focus handlers are command-shaped; keep dry-runs side-effect free.
32
+ return false;
33
+ }
34
+ void preloadFormatterModule();
35
+ return false;
36
+ };
37
+ };
38
+ var formatCode = exports.formatCode = /*#__PURE__*/function () {
39
+ var _ref2 = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee(_ref) {
40
+ var content, language, formatterModule, _t, _t2;
41
+ return _regenerator.default.wrap(function (_context) {
42
+ while (1) switch (_context.prev = _context.next) {
43
+ case 0:
44
+ content = _ref.content, language = _ref.language;
45
+ _context.prev = 1;
46
+ _context.next = 2;
47
+ return preloadFormatterModule();
48
+ case 2:
49
+ formatterModule = _context.sent;
50
+ _context.next = 4;
51
+ break;
52
+ case 3:
53
+ _context.prev = 3;
54
+ _t = _context["catch"](1);
55
+ return _context.abrupt("return", {
56
+ errorType: 'formatter-load-failed',
57
+ language: language,
58
+ status: 'failed'
59
+ });
60
+ case 4:
61
+ _context.prev = 4;
62
+ _context.next = 5;
63
+ return formatterModule.formatCode({
64
+ content: content,
65
+ language: language
66
+ });
67
+ case 5:
68
+ return _context.abrupt("return", _context.sent);
69
+ case 6:
70
+ _context.prev = 6;
71
+ _t2 = _context["catch"](4);
72
+ return _context.abrupt("return", {
73
+ errorType: 'formatter-execution-failed',
74
+ language: language,
75
+ status: 'failed'
76
+ });
77
+ case 7:
78
+ case "end":
79
+ return _context.stop();
80
+ }
81
+ }, _callee, null, [[1, 3], [4, 6]]);
82
+ }));
83
+ return function formatCode(_x) {
84
+ return _ref2.apply(this, arguments);
85
+ };
86
+ }();
@@ -6,6 +6,7 @@ import { blockTypeMessages } from '@atlaskit/editor-common/messages';
6
6
  import { IconCode } from '@atlaskit/editor-common/quick-insert';
7
7
  import { fg } from '@atlaskit/platform-feature-flags';
8
8
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
9
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
9
10
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
10
11
  import { createInsertCodeBlockTransaction, insertCodeBlockWithAnalytics } from './editor-commands';
11
12
  import { createAutoDetectPlugin } from './pm-plugins/auto-detect';
@@ -15,9 +16,11 @@ import ideUX from './pm-plugins/ide-ux';
15
16
  import { createCodeBlockInputRule } from './pm-plugins/input-rule';
16
17
  import keymap from './pm-plugins/keymaps';
17
18
  import { createPlugin } from './pm-plugins/main';
19
+ import { pluginKey } from './pm-plugins/plugin-key';
18
20
  import refreshBrowserSelectionOnChange from './pm-plugins/refresh-browser-selection';
19
21
  import { getToolbarConfig } from './pm-plugins/toolbar';
20
22
  import { createCodeBlockMenuItem } from './ui/CodeBlockMenuItem';
23
+ import { FormatCodeErrorFlag } from './ui/FormatCodeErrorFlag';
21
24
  const CODE_BLOCK_NODE_NAME = 'codeBlock';
22
25
  const codeBlockPlugin = ({
23
26
  config: options,
@@ -49,11 +52,15 @@ const codeBlockPlugin = ({
49
52
  }];
50
53
  },
51
54
  getSharedState(state) {
55
+ var _codeBlockState$forma, _codeBlockState$pendi;
52
56
  if (!state) {
53
57
  return undefined;
54
58
  }
59
+ const codeBlockState = pluginKey.getState(state);
55
60
  return {
56
- copyButtonHoverNode: copySelectionPluginKey.getState(state).codeBlockNode
61
+ copyButtonHoverNode: copySelectionPluginKey.getState(state).codeBlockNode,
62
+ formatCodeErrors: (_codeBlockState$forma = codeBlockState === null || codeBlockState === void 0 ? void 0 : codeBlockState.formatCodeErrors) !== null && _codeBlockState$forma !== void 0 ? _codeBlockState$forma : {},
63
+ pendingFormats: (_codeBlockState$pendi = codeBlockState === null || codeBlockState === void 0 ? void 0 : codeBlockState.pendingFormats) !== null && _codeBlockState$pendi !== void 0 ? _codeBlockState$pendi : {}
57
64
  };
58
65
  },
59
66
  pmPlugins() {
@@ -74,7 +81,7 @@ const codeBlockPlugin = ({
74
81
  var _api$analytics;
75
82
  return createCodeBlockInputRule(schema, api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions);
76
83
  }
77
- }, ...(expValEquals('platform_editor_code_block_auto_detection', 'isEnabled', true) ? [{
84
+ }, ...(expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true) && fg('platform_editor_code_block_language_detection_flow') ? [{
78
85
  name: 'codeBlockAutoDetect',
79
86
  plugin: () => createAutoDetectPlugin(api)
80
87
  }] : []), {
@@ -136,7 +143,10 @@ const codeBlockPlugin = ({
136
143
  }
137
144
  }],
138
145
  floatingToolbar: getToolbarConfig(options === null || options === void 0 ? void 0 : options.allowCopyToClipboard, api, options === null || options === void 0 ? void 0 : options.overrideLanguageName)
139
- }
146
+ },
147
+ contentComponent: () => expValEqualsNoExposure('platform_editor_code_block_q4_lovability', 'isEnabled', true) ? /*#__PURE__*/React.createElement(FormatCodeErrorFlag, {
148
+ api: api
149
+ }) : null
140
150
  };
141
151
  };
142
152
  export default codeBlockPlugin;
@@ -7,6 +7,7 @@ import { editorCommandToPMCommand } from '@atlaskit/editor-common/preset';
7
7
  import { findCodeBlock } from '@atlaskit/editor-common/transforms';
8
8
  import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
9
9
  import { findParentNodeOfType, findSelectedNodeOfType, isNodeSelection, removeParentNodeOfType, removeSelectedNode, safeInsert } from '@atlaskit/editor-prosemirror/utils';
10
+ import { fg } from '@atlaskit/platform-feature-flags';
10
11
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
11
12
  import { ACTIONS } from '../pm-plugins/actions';
12
13
  import { autoDetectPluginKey } from '../pm-plugins/auto-detect-state';
@@ -14,6 +15,7 @@ import { copySelectionPluginKey } from '../pm-plugins/codeBlockCopySelectionPlug
14
15
  import { pluginKey } from '../pm-plugins/plugin-key';
15
16
  import { transformToCodeBlockAction } from '../pm-plugins/transform-to-code-block';
16
17
  import { createAutoDetectEntry, getLocalId, hasEnoughTextForAutoDetection } from '../utils/auto-detect-state';
18
+ import { formatCode, isSupportedFormatLanguage } from '../utils/format-code/formatter';
17
19
  export const removeCodeBlockWithAnalytics = editorAnalyticsAPI => {
18
20
  return withAnalytics(editorAnalyticsAPI, {
19
21
  action: ACTION.DELETED,
@@ -53,12 +55,12 @@ export const changeLanguage = editorAnalyticsAPI => (language, selectionSource)
53
55
  }
54
56
  const node = state.doc.nodeAt(pos);
55
57
  const localId = node === null || node === void 0 ? void 0 : node.attrs.localId;
56
- const previousAutoDetectEntry = expValEquals('platform_editor_code_block_auto_detection', 'isEnabled', true) ? (_autoDetectPluginKey$ = autoDetectPluginKey.getState(state)) === null || _autoDetectPluginKey$ === void 0 ? void 0 : _autoDetectPluginKey$.languageDetectionMap[localId] : undefined;
58
+ const previousAutoDetectEntry = expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true) && fg('platform_editor_code_block_language_detection_flow') ? (_autoDetectPluginKey$ = autoDetectPluginKey.getState(state)) === null || _autoDetectPluginKey$ === void 0 ? void 0 : _autoDetectPluginKey$.languageDetectionMap[localId] : undefined;
57
59
  const tr = state.tr.setNodeMarkup(pos, codeBlock, {
58
60
  ...(node === null || node === void 0 ? void 0 : node.attrs),
59
61
  language
60
62
  }).setMeta('scrollIntoView', false);
61
- if (expValEquals('platform_editor_code_block_auto_detection', 'isEnabled', true)) {
63
+ if (expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true) && fg('platform_editor_code_block_language_detection_flow')) {
62
64
  tr.setMeta(autoDetectPluginKey, {
63
65
  type: ACTIONS.REMOVE_AUTO_DETECT_ENTRY,
64
66
  data: {
@@ -122,6 +124,181 @@ export const detectLanguage = () => (state, dispatch) => {
122
124
  }
123
125
  return true;
124
126
  };
127
+ const setResolveFormatCodeMeta = (tr, {
128
+ languageSource,
129
+ localId,
130
+ outcome,
131
+ requestId,
132
+ errorType
133
+ }) => tr.setMeta(pluginKey, {
134
+ type: ACTIONS.RESOLVE_FORMAT_CODE,
135
+ data: {
136
+ languageSource,
137
+ localId,
138
+ outcome,
139
+ requestId,
140
+ ...(errorType ? {
141
+ errorType
142
+ } : {})
143
+ }
144
+ });
145
+ const replaceCodeBlockText = ({
146
+ codeBlockNode,
147
+ content,
148
+ pos,
149
+ tr
150
+ }) => {
151
+ const from = pos + 1;
152
+ const to = pos + codeBlockNode.nodeSize - 1;
153
+ tr.delete(from, to);
154
+ if (content) {
155
+ tr.insertText(content, from);
156
+ }
157
+
158
+ // The editor scroll plugin scrolls doc-changing transactions by default.
159
+ return tr.setMeta('scrollIntoView', false);
160
+ };
161
+ const attachFormatCodeAnalytics = ({
162
+ editorAnalyticsAPI,
163
+ languageSource,
164
+ result,
165
+ tr
166
+ }) => {
167
+ if (result.status === 'failed') {
168
+ editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent({
169
+ action: ACTION.ERRORED,
170
+ actionSubject: ACTION_SUBJECT.CODE_BLOCK,
171
+ attributes: {
172
+ errorType: result.errorType,
173
+ language: result.language,
174
+ languageSource
175
+ },
176
+ eventType: EVENT_TYPE.TRACK
177
+ })(tr);
178
+ return;
179
+ }
180
+ editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent({
181
+ action: ACTION.FORMATTED,
182
+ actionSubject: ACTION_SUBJECT.CODE_BLOCK,
183
+ attributes: {
184
+ language: result.language,
185
+ languageSource,
186
+ outcome: result.status
187
+ },
188
+ eventType: EVENT_TYPE.TRACK
189
+ })(tr);
190
+ };
191
+ const createResolveFormatCodeTransaction = ({
192
+ editorAnalyticsAPI,
193
+ localId,
194
+ pendingFormat,
195
+ result,
196
+ tr
197
+ }) => {
198
+ const {
199
+ languageSource,
200
+ requestId
201
+ } = pendingFormat;
202
+ const codeBlockNode = tr.doc.nodeAt(pendingFormat.pos);
203
+ const hasMatchingCodeBlock = (codeBlockNode === null || codeBlockNode === void 0 ? void 0 : codeBlockNode.type) === tr.doc.type.schema.nodes.codeBlock && (codeBlockNode === null || codeBlockNode === void 0 ? void 0 : codeBlockNode.attrs.localId) === localId;
204
+ if (!hasMatchingCodeBlock) {
205
+ // Keep failure telemetry even when the target block is no longer available.
206
+ if (result.status === 'failed') {
207
+ attachFormatCodeAnalytics({
208
+ editorAnalyticsAPI,
209
+ languageSource,
210
+ result,
211
+ tr
212
+ });
213
+ }
214
+ return setResolveFormatCodeMeta(tr, {
215
+ languageSource,
216
+ localId,
217
+ outcome: 'unchanged',
218
+ requestId
219
+ });
220
+ }
221
+ let resultTransaction = tr;
222
+ if (result.status === 'formatted') {
223
+ resultTransaction = replaceCodeBlockText({
224
+ codeBlockNode,
225
+ content: result.content,
226
+ pos: pendingFormat.pos,
227
+ tr
228
+ });
229
+ }
230
+ attachFormatCodeAnalytics({
231
+ editorAnalyticsAPI,
232
+ languageSource,
233
+ result,
234
+ tr: resultTransaction
235
+ });
236
+ return setResolveFormatCodeMeta(resultTransaction, {
237
+ errorType: result.status === 'failed' ? result.errorType : undefined,
238
+ languageSource,
239
+ localId,
240
+ outcome: result.status,
241
+ requestId
242
+ });
243
+ };
244
+ export const createFormatCodeOnClick = ({
245
+ api,
246
+ editorAnalyticsAPI
247
+ }) => (state, dispatch) => {
248
+ var _autoDetectPluginKey$2, _api$core;
249
+ const currentCodeBlockState = pluginKey.getState(state);
250
+ const currentPos = currentCodeBlockState === null || currentCodeBlockState === void 0 ? void 0 : currentCodeBlockState.pos;
251
+ if (!currentCodeBlockState || typeof currentPos !== 'number') {
252
+ return false;
253
+ }
254
+ const currentNode = state.doc.nodeAt(currentPos);
255
+ if (!currentNode || currentNode.type !== state.schema.nodes.codeBlock) {
256
+ return false;
257
+ }
258
+ const currentLanguage = currentNode.attrs.language;
259
+ if (!isSupportedFormatLanguage(currentLanguage)) {
260
+ return true;
261
+ }
262
+ const currentLocalId = currentNode.attrs.localId;
263
+ if (currentCodeBlockState.pendingFormats[currentLocalId]) {
264
+ return true;
265
+ }
266
+ const autoDetectEntry = (_autoDetectPluginKey$2 = autoDetectPluginKey.getState(state)) === null || _autoDetectPluginKey$2 === void 0 ? void 0 : _autoDetectPluginKey$2.languageDetectionMap[currentLocalId];
267
+ const languageSource = (autoDetectEntry === null || autoDetectEntry === void 0 ? void 0 : autoDetectEntry.autoDetectedLanguage) === currentLanguage ? 'auto-detected' : 'selected';
268
+ const content = currentNode.textContent;
269
+ const requestId = crypto.randomUUID();
270
+ api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
271
+ tr
272
+ }) => tr.setMeta(pluginKey, {
273
+ type: ACTIONS.START_FORMAT_CODE,
274
+ data: {
275
+ languageSource,
276
+ localId: currentLocalId,
277
+ pos: currentPos,
278
+ requestId
279
+ }
280
+ }));
281
+ void formatCode({
282
+ content,
283
+ language: currentLanguage
284
+ }).then(result => {
285
+ var _api$codeBlock, _api$codeBlock$shared, _api$core2;
286
+ const pendingFormat = api === null || api === void 0 ? void 0 : (_api$codeBlock = api.codeBlock) === null || _api$codeBlock === void 0 ? void 0 : (_api$codeBlock$shared = _api$codeBlock.sharedState.currentState()) === null || _api$codeBlock$shared === void 0 ? void 0 : _api$codeBlock$shared.pendingFormats[currentLocalId];
287
+ if (!pendingFormat || pendingFormat.requestId !== requestId) {
288
+ return;
289
+ }
290
+ api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(({
291
+ tr
292
+ }) => createResolveFormatCodeTransaction({
293
+ editorAnalyticsAPI,
294
+ localId: currentLocalId,
295
+ pendingFormat,
296
+ result,
297
+ tr
298
+ }));
299
+ });
300
+ return true;
301
+ };
125
302
  export const copyContentToClipboardWithAnalytics = editorAnalyticsAPI => (state, dispatch) => {
126
303
  const {
127
304
  schema: {
@@ -1,7 +1,10 @@
1
1
  export const ACTIONS = {
2
+ CLEAR_FORMAT_CODE_ERROR: 'CLEAR_FORMAT_CODE_ERROR',
3
+ REMOVE_AUTO_DETECT_ENTRY: 'REMOVE_AUTO_DETECT_ENTRY',
4
+ RESOLVE_FORMAT_CODE: 'RESOLVE_FORMAT_CODE',
5
+ SET_AUTO_DETECT_ENTRY: 'SET_AUTO_DETECT_ENTRY',
2
6
  SET_COPIED_TO_CLIPBOARD: 'SET_COPIED_TO_CLIPBOARD',
3
- SET_SHOULD_IGNORE_FOLLOWING_MUTATIONS: 'SET_SHOULD_IGNORE_FOLLOWING_MUTATIONS',
4
7
  SET_IS_WRAPPED: 'SET_IS_WRAPPED',
5
- SET_AUTO_DETECT_ENTRY: 'SET_AUTO_DETECT_ENTRY',
6
- REMOVE_AUTO_DETECT_ENTRY: 'REMOVE_AUTO_DETECT_ENTRY'
8
+ SET_SHOULD_IGNORE_FOLLOWING_MUTATIONS: 'SET_SHOULD_IGNORE_FOLLOWING_MUTATIONS',
9
+ START_FORMAT_CODE: 'START_FORMAT_CODE'
7
10
  };
@@ -10,6 +10,7 @@ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
10
10
  import { ignoreFollowingMutations, resetShouldIgnoreFollowingMutations } from '../editor-commands';
11
11
  import { codeBlockNodeView } from '../nodeviews/code-block';
12
12
  import { codeBlockClassNames } from '../ui/class-names';
13
+ import { applyFormatCodeMeta, mapPendingFormats } from '../utils/format-code/format-code-state';
13
14
  import { ACTIONS } from './actions';
14
15
  import { generateInitialDecorations, updateCodeBlockDecorations, updateDecorationSetWithWordWrappedDecorator } from './decorators';
15
16
  import { pluginKey } from './plugin-key';
@@ -81,7 +82,9 @@ export const createPlugin = ({
81
82
  return {
82
83
  pos: node ? node.pos : null,
83
84
  contentCopied: false,
85
+ formatCodeErrors: {},
84
86
  isNodeSelected: false,
87
+ pendingFormats: {},
85
88
  shouldIgnoreFollowingMutations: false,
86
89
  decorations: DecorationSet.create(state.doc, initialDecorations)
87
90
  };
@@ -116,7 +119,17 @@ export const createPlugin = ({
116
119
  isNodeSelected: tr.selection instanceof NodeSelection,
117
120
  decorations: updatedDecorationSet
118
121
  };
119
- return newPluginState;
122
+ if (!expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true)) {
123
+ return newPluginState;
124
+ }
125
+
126
+ // Successful format results change the doc and carry format meta.
127
+ const formatCodePluginState = applyFormatCodeMeta(newPluginState, meta);
128
+ return {
129
+ ...formatCodePluginState,
130
+ // Pending format requests can outlive unrelated document edits.
131
+ pendingFormats: mapPendingFormats(formatCodePluginState.pendingFormats, tr, newState)
132
+ };
120
133
  }
121
134
  if (tr.selectionSet) {
122
135
  const node = findCodeBlock(newState, tr.selection);
@@ -138,6 +151,10 @@ export const createPlugin = ({
138
151
  shouldIgnoreFollowingMutations: meta.data
139
152
  };
140
153
  }
154
+ if (expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true)) {
155
+ // Failed/unchanged format results and dismissals are meta-only.
156
+ return applyFormatCodeMeta(pluginState, meta);
157
+ }
141
158
  return pluginState;
142
159
  }
143
160
  },
@@ -3,16 +3,18 @@ import { areCodeBlockLineNumbersVisible, isCodeBlockWordWrapEnabled } from '@atl
3
3
  import commonMessages, { codeBlockButtonMessages } from '@atlaskit/editor-common/messages';
4
4
  import { areToolbarFlagsEnabled } from '@atlaskit/editor-common/toolbar-flag-check';
5
5
  import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
6
+ import AngleBracketsIcon from '@atlaskit/icon/core/angle-brackets';
6
7
  import CopyIcon from '@atlaskit/icon/core/copy';
7
8
  import DeleteIcon from '@atlaskit/icon/core/delete';
8
9
  import ListNumberedIcon from '@atlaskit/icon/core/list-numbered';
9
10
  import TextWrapIcon from '@atlaskit/icon/core/text-wrap';
10
11
  import { fg } from '@atlaskit/platform-feature-flags';
11
12
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
12
- import { changeLanguage, copyContentToClipboardWithAnalytics, removeCodeBlockWithAnalytics, resetCopiedState, toggleLineNumbersForCodeBlockNode, toggleWordWrapStateForCodeBlockNode } from '../editor-commands';
13
+ import { changeLanguage, copyContentToClipboardWithAnalytics, createFormatCodeOnClick, removeCodeBlockWithAnalytics, resetCopiedState, toggleLineNumbersForCodeBlockNode, toggleWordWrapStateForCodeBlockNode } from '../editor-commands';
13
14
  import { CodeBlockLanguagePicker } from '../ui/CodeBlockLanguagePicker';
14
15
  import { WrapIcon } from '../ui/icons/WrapIcon';
15
16
  import { NONE_LANGUAGE_VALUE, PLAIN_TEXT_LANGUAGE_VALUE } from '../ui/language-picker-options';
17
+ import { isSupportedFormatLanguage, preloadFormatterOnIntent } from '../utils/format-code/formatter';
16
18
  import { autoDetectPluginKey } from './auto-detect-state';
17
19
  import { provideVisualFeedbackForCopyButton, removeVisualFeedbackForCopyButton } from './codeBlockCopySelectionPlugin';
18
20
  import { createLanguageList, DEFAULT_LANGUAGES, getLanguageIdentifier } from './language-list';
@@ -79,8 +81,7 @@ export const getToolbarConfig = (allowCopyToClipboard = false, api, overrideLang
79
81
  const language = node === null || node === void 0 ? void 0 : (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.language;
80
82
  const localId = node === null || node === void 0 ? void 0 : (_node$attrs2 = node.attrs) === null || _node$attrs2 === void 0 ? void 0 : _node$attrs2.localId;
81
83
  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
-
84
+ const isFormatCodePending = typeof localId === 'string' && Boolean(codeBlockState.pendingFormats[localId]);
84
85
  // Keep fresh option objects for the legacy toolbar select so reopening it
85
86
  // continues to start from the top rather than preserving the previously
86
87
  // focused option by reference.
@@ -100,6 +101,7 @@ export const getToolbarConfig = (allowCopyToClipboard = false, api, overrideLang
100
101
  };
101
102
  let languagePicker;
102
103
  if (expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true) && fg('platform_editor_code_block_add_line_number_button')) {
104
+ const autoDetectEntry = typeof localId === 'string' && fg('platform_editor_code_block_language_detection_flow') ? autoDetectState === null || autoDetectState === void 0 ? void 0 : autoDetectState.languageDetectionMap[localId] : undefined;
103
105
  const autoDetectPickerValue = getAutoDetectPickerValue({
104
106
  autoDetectEntry,
105
107
  formatMessage,
@@ -223,13 +225,28 @@ export const getToolbarConfig = (allowCopyToClipboard = false, api, overrideLang
223
225
  tabIndex: null,
224
226
  selected: areLineNumbersVisible
225
227
  };
228
+ const canFormatCode = node.textContent.length > 0 && isSupportedFormatLanguage(language);
229
+ const formatCodeButton = {
230
+ id: 'editor.codeBlock.formatCode',
231
+ type: 'button',
232
+ supportsViewMode: false,
233
+ disabled: !canFormatCode || isFormatCodePending,
234
+ icon: AngleBracketsIcon,
235
+ onClick: createFormatCodeOnClick({
236
+ api,
237
+ editorAnalyticsAPI
238
+ }),
239
+ onFocus: preloadFormatterOnIntent(),
240
+ onMouseEnter: preloadFormatterOnIntent(),
241
+ title: formatMessage(canFormatCode ? codeBlockButtonMessages.formatCode : codeBlockButtonMessages.formatCodeUnavailable)
242
+ };
226
243
  return {
227
244
  title: 'CodeBlock floating controls',
228
245
  // Ignored via go/ees005
229
246
  // eslint-disable-next-line @atlaskit/editor/no-as-casting
230
247
  getDomRef: view => findDomRefAtPos(pos, view.domAtPos.bind(view)),
231
248
  nodeType,
232
- items: [(_languagePicker = languagePicker) !== null && _languagePicker !== void 0 ? _languagePicker : languageSelect, ...(areAnyNewToolbarFlagsEnabled ? [] : [separator]), codeBlockWrapButton, ...(expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true) && fg('platform_editor_code_block_add_line_number_button') ? [codeBlockLineNumbersButton] : []), ...copyAndDeleteButtonMenuItems],
249
+ items: [(_languagePicker = languagePicker) !== null && _languagePicker !== void 0 ? _languagePicker : languageSelect, ...(areAnyNewToolbarFlagsEnabled ? [] : [separator]), codeBlockWrapButton, ...(expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true) && fg('platform_editor_code_block_add_line_number_button') ? [codeBlockLineNumbersButton, formatCodeButton] : []), ...copyAndDeleteButtonMenuItems],
233
250
  scrollable: true
234
251
  };
235
252
  };
@@ -1,5 +1,5 @@
1
1
  import React, { useCallback, useState } from 'react';
2
- import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
2
+ import { fg } from '@atlaskit/platform-feature-flags';
3
3
  import { changeLanguage, detectLanguage } from '../editor-commands';
4
4
  import { DETECT_LANGUAGE_VALUE } from './language-picker-options';
5
5
  import { LanguagePicker } from './LanguagePicker';
@@ -19,7 +19,7 @@ export const CodeBlockLanguagePicker = ({
19
19
  }, []);
20
20
  const handleSelection = useCallback((option, selectionSource) => {
21
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);
22
+ const command = option.value === DETECT_LANGUAGE_VALUE && fg('platform_editor_code_block_language_detection_flow') ? 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
23
  const commandSucceeded = command(editorView.state, editorView.dispatch);
24
24
  if (commandSucceeded && option.value !== DETECT_LANGUAGE_VALUE) {
25
25
  saveRecentLanguage(option.value);
@@ -0,0 +1,62 @@
1
+ import React, { useCallback } from 'react';
2
+ import { useIntl } from 'react-intl';
3
+ import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
4
+ import { codeBlockButtonMessages } from '@atlaskit/editor-common/messages';
5
+ import AkFlag, { FlagGroup } from '@atlaskit/flag';
6
+ import StatusErrorIcon from '@atlaskit/icon/core/status-error';
7
+ import { ACTIONS } from '../pm-plugins/actions';
8
+ import { pluginKey } from '../pm-plugins/plugin-key';
9
+ const FormatCodeErrorFlagItem = ({
10
+ formatCodeError
11
+ }) => {
12
+ const {
13
+ formatMessage
14
+ } = useIntl();
15
+ return /*#__PURE__*/React.createElement(AkFlag, {
16
+ description: formatMessage(formatCodeError.languageSource === 'auto-detected' ? codeBlockButtonMessages.formatCodeFailedAutoDetectedDescription : codeBlockButtonMessages.formatCodeFailedDescription),
17
+ icon: /*#__PURE__*/React.createElement(StatusErrorIcon, {
18
+ color: "var(--ds-icon-danger, #C9372C)",
19
+ label: ""
20
+ }),
21
+ id: formatCodeError.localId,
22
+ title: formatMessage(codeBlockButtonMessages.formatCodeFailed)
23
+ });
24
+ };
25
+ export const FormatCodeErrorFlag = ({
26
+ api
27
+ }) => {
28
+ const {
29
+ formatCodeErrors
30
+ } = useSharedPluginStateWithSelector(api, ['codeBlock'], states => {
31
+ var _states$codeBlockStat, _states$codeBlockStat2;
32
+ return {
33
+ formatCodeErrors: (_states$codeBlockStat = (_states$codeBlockStat2 = states.codeBlockState) === null || _states$codeBlockStat2 === void 0 ? void 0 : _states$codeBlockStat2.formatCodeErrors) !== null && _states$codeBlockStat !== void 0 ? _states$codeBlockStat : {}
34
+ };
35
+ });
36
+ const onDismissed = useCallback(localId => {
37
+ var _api$core, _api$core2;
38
+ api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(({
39
+ tr
40
+ }) => {
41
+ tr.setMeta(pluginKey, {
42
+ type: ACTIONS.CLEAR_FORMAT_CODE_ERROR,
43
+ data: {
44
+ localId
45
+ }
46
+ });
47
+ return tr;
48
+ });
49
+ api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.focus();
50
+ }, [api]);
51
+ const onFlagGroupDismissed = useCallback(localId => onDismissed(String(localId)), [onDismissed]);
52
+ const activeFormatCodeErrors = Object.values(formatCodeErrors);
53
+ if (activeFormatCodeErrors.length === 0) {
54
+ return null;
55
+ }
56
+ return /*#__PURE__*/React.createElement(FlagGroup, {
57
+ onDismissed: onFlagGroupDismissed
58
+ }, activeFormatCodeErrors.map(formatCodeError => /*#__PURE__*/React.createElement(FormatCodeErrorFlagItem, {
59
+ formatCodeError: formatCodeError,
60
+ key: formatCodeError.localId
61
+ })));
62
+ };
@@ -1,5 +1,5 @@
1
1
  import { codeBlockButtonMessages } from '@atlaskit/editor-common/messages';
2
- import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
2
+ import { fg } from '@atlaskit/platform-feature-flags';
3
3
  export const NONE_LANGUAGE_VALUE = 'none';
4
4
  export const DETECT_LANGUAGE_VALUE = 'autodetect';
5
5
  export const PLAIN_TEXT_LANGUAGE_VALUE = 'text';
@@ -17,7 +17,7 @@ export const createGroupedLanguageOptions = ({
17
17
  const recentlyUsedLanguageValues = new Set(recentlyUsedLanguages.map(language => language.value));
18
18
  const allLanguages = languages.filter(language => language.value !== NONE_LANGUAGE_VALUE && language.value !== PLAIN_TEXT_LANGUAGE_VALUE && !recentlyUsedLanguageValues.has(language.value));
19
19
  const plainTextOption = languages.find(language => language.value === PLAIN_TEXT_LANGUAGE_VALUE);
20
- const pinnedOptions = expValEquals('platform_editor_code_block_auto_detection', 'isEnabled', true) ? [getDetectLanguageOption(formatMessage)] : [];
20
+ const pinnedOptions = fg('platform_editor_code_block_language_detection_flow') ? [getDetectLanguageOption(formatMessage)] : [];
21
21
  if (plainTextOption) {
22
22
  pinnedOptions.push({
23
23
  ...plainTextOption,