@atlaskit/editor-plugin-code-block 13.0.1 → 13.1.1

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 (45) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/cjs/codeBlockPlugin.js +13 -1
  3. package/dist/cjs/editor-commands/index.js +179 -5
  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 +19 -2
  7. package/dist/cjs/ui/FormatCodeErrorFlag.js +70 -0
  8. package/dist/cjs/utils/format-code/format-code-state.js +81 -0
  9. package/dist/cjs/utils/format-code/formatter-impl.js +15 -0
  10. package/dist/cjs/utils/format-code/formatter.js +86 -0
  11. package/dist/es2019/codeBlockPlugin.js +12 -2
  12. package/dist/es2019/editor-commands/index.js +176 -0
  13. package/dist/es2019/pm-plugins/actions.js +6 -3
  14. package/dist/es2019/pm-plugins/main.js +18 -1
  15. package/dist/es2019/pm-plugins/toolbar.js +22 -3
  16. package/dist/es2019/ui/FormatCodeErrorFlag.js +62 -0
  17. package/dist/es2019/utils/format-code/format-code-state.js +82 -0
  18. package/dist/es2019/utils/format-code/formatter-impl.js +10 -0
  19. package/dist/es2019/utils/format-code/formatter.js +47 -0
  20. package/dist/esm/codeBlockPlugin.js +13 -1
  21. package/dist/esm/editor-commands/index.js +178 -4
  22. package/dist/esm/pm-plugins/actions.js +6 -3
  23. package/dist/esm/pm-plugins/main.js +17 -1
  24. package/dist/esm/pm-plugins/toolbar.js +20 -3
  25. package/dist/esm/ui/FormatCodeErrorFlag.js +61 -0
  26. package/dist/esm/utils/format-code/format-code-state.js +74 -0
  27. package/dist/esm/utils/format-code/formatter-impl.js +9 -0
  28. package/dist/esm/utils/format-code/formatter.js +75 -0
  29. package/dist/types/codeBlockPluginType.d.ts +3 -0
  30. package/dist/types/editor-commands/index.d.ts +6 -1
  31. package/dist/types/pm-plugins/actions.d.ts +6 -3
  32. package/dist/types/pm-plugins/main-state.d.ts +16 -0
  33. package/dist/types/ui/FormatCodeErrorFlag.d.ts +6 -0
  34. package/dist/types/utils/format-code/format-code-state.d.ts +4 -0
  35. package/dist/types/utils/format-code/formatter-impl.d.ts +5 -0
  36. package/dist/types/utils/format-code/formatter.d.ts +25 -0
  37. package/dist/types-ts4.5/codeBlockPluginType.d.ts +3 -0
  38. package/dist/types-ts4.5/editor-commands/index.d.ts +6 -1
  39. package/dist/types-ts4.5/pm-plugins/actions.d.ts +6 -3
  40. package/dist/types-ts4.5/pm-plugins/main-state.d.ts +16 -0
  41. package/dist/types-ts4.5/ui/FormatCodeErrorFlag.d.ts +6 -0
  42. package/dist/types-ts4.5/utils/format-code/format-code-state.d.ts +4 -0
  43. package/dist/types-ts4.5/utils/format-code/formatter-impl.d.ts +5 -0
  44. package/dist/types-ts4.5/utils/format-code/formatter.d.ts +32 -0
  45. package/package.json +9 -5
@@ -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() {
@@ -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;
@@ -15,6 +15,7 @@ import { copySelectionPluginKey } from '../pm-plugins/codeBlockCopySelectionPlug
15
15
  import { pluginKey } from '../pm-plugins/plugin-key';
16
16
  import { transformToCodeBlockAction } from '../pm-plugins/transform-to-code-block';
17
17
  import { createAutoDetectEntry, getLocalId, hasEnoughTextForAutoDetection } from '../utils/auto-detect-state';
18
+ import { formatCode, isSupportedFormatLanguage } from '../utils/format-code/formatter';
18
19
  export const removeCodeBlockWithAnalytics = editorAnalyticsAPI => {
19
20
  return withAnalytics(editorAnalyticsAPI, {
20
21
  action: ACTION.DELETED,
@@ -123,6 +124,181 @@ export const detectLanguage = () => (state, dispatch) => {
123
124
  }
124
125
  return true;
125
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
+ };
126
302
  export const copyContentToClipboardWithAnalytics = editorAnalyticsAPI => (state, dispatch) => {
127
303
  const {
128
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,7 +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
-
84
+ const isFormatCodePending = typeof localId === 'string' && Boolean(codeBlockState.pendingFormats[localId]);
83
85
  // Keep fresh option objects for the legacy toolbar select so reopening it
84
86
  // continues to start from the top rather than preserving the previously
85
87
  // focused option by reference.
@@ -223,13 +225,30 @@ 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] : []),
250
+ // eslint-disable-next-line @atlaskit/platform/no-preconditioning
251
+ ...(expValEquals('platform_editor_code_block_q4_lovability', 'isEnabled', true) && fg('platform_editor_code_block_add_line_number_button') && fg('platform_editor_code_block_formatting') ? [formatCodeButton] : []), ...copyAndDeleteButtonMenuItems],
233
252
  scrollable: true
234
253
  };
235
254
  };
@@ -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
+ };
@@ -0,0 +1,82 @@
1
+ import { ACTIONS } from '../../pm-plugins/actions';
2
+ export const mapPendingFormats = (pendingFormats, tr, newState) => {
3
+ const entries = Object.entries(pendingFormats);
4
+ if (entries.length === 0) {
5
+ return pendingFormats;
6
+ }
7
+ let nextPendingFormats = pendingFormats;
8
+ entries.forEach(([localId, pendingFormat]) => {
9
+ const {
10
+ deleted,
11
+ pos
12
+ } = tr.mapping.mapResult(pendingFormat.pos, 1);
13
+ const codeBlockNode = newState.doc.nodeAt(pos);
14
+ const shouldRemovePendingFormat = deleted || (codeBlockNode === null || codeBlockNode === void 0 ? void 0 : codeBlockNode.type) !== newState.schema.nodes.codeBlock || (codeBlockNode === null || codeBlockNode === void 0 ? void 0 : codeBlockNode.attrs.localId) !== localId;
15
+ const shouldUpdatePendingFormat = pos !== pendingFormat.pos;
16
+ if (shouldRemovePendingFormat || shouldUpdatePendingFormat) {
17
+ if (nextPendingFormats === pendingFormats) {
18
+ nextPendingFormats = {
19
+ ...pendingFormats
20
+ };
21
+ }
22
+ }
23
+ if (shouldRemovePendingFormat) {
24
+ delete nextPendingFormats[localId];
25
+ return;
26
+ }
27
+ if (shouldUpdatePendingFormat) {
28
+ nextPendingFormats[localId] = {
29
+ ...pendingFormat,
30
+ pos
31
+ };
32
+ }
33
+ });
34
+ return nextPendingFormats;
35
+ };
36
+ function removeRecordEntry(record, key) {
37
+ const nextRecord = {
38
+ ...record
39
+ };
40
+ delete nextRecord[key];
41
+ return nextRecord;
42
+ }
43
+ export const applyFormatCodeMeta = (pluginState, meta) => {
44
+ switch (meta === null || meta === void 0 ? void 0 : meta.type) {
45
+ case ACTIONS.START_FORMAT_CODE:
46
+ return {
47
+ ...pluginState,
48
+ pendingFormats: {
49
+ ...pluginState.pendingFormats,
50
+ [meta.data.localId]: {
51
+ languageSource: meta.data.languageSource,
52
+ pos: meta.data.pos,
53
+ requestId: meta.data.requestId
54
+ }
55
+ }
56
+ };
57
+ case ACTIONS.RESOLVE_FORMAT_CODE:
58
+ {
59
+ const pendingFormats = removeRecordEntry(pluginState.pendingFormats, meta.data.localId);
60
+ const formatCodeErrors = removeRecordEntry(pluginState.formatCodeErrors, meta.data.localId);
61
+ return {
62
+ ...pluginState,
63
+ formatCodeErrors: meta.data.outcome === 'failed' ? {
64
+ ...formatCodeErrors,
65
+ [meta.data.localId]: {
66
+ errorType: meta.data.errorType,
67
+ localId: meta.data.localId,
68
+ languageSource: meta.data.languageSource
69
+ }
70
+ } : formatCodeErrors,
71
+ pendingFormats
72
+ };
73
+ }
74
+ case ACTIONS.CLEAR_FORMAT_CODE_ERROR:
75
+ return {
76
+ ...pluginState,
77
+ formatCodeErrors: removeRecordEntry(pluginState.formatCodeErrors, meta.data.localId)
78
+ };
79
+ default:
80
+ return pluginState;
81
+ }
82
+ };
@@ -0,0 +1,10 @@
1
+ export const formatCode = ({
2
+ content,
3
+ language
4
+ }) => {
5
+ return Promise.resolve({
6
+ content,
7
+ language,
8
+ status: 'unchanged'
9
+ });
10
+ };
@@ -0,0 +1,47 @@
1
+ const supportedFormatLanguages = ['json', 'javascript', 'jsx', 'typescript', 'tsx', 'sql'];
2
+ export const isSupportedFormatLanguage = language => supportedFormatLanguages.includes(language);
3
+ let formatterModulePromise;
4
+ export const preloadFormatterModule = () => {
5
+ if (!formatterModulePromise) {
6
+ formatterModulePromise = import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-code-block-formatter" */'./formatter-impl').catch(error => {
7
+ formatterModulePromise = undefined;
8
+ throw error;
9
+ });
10
+ }
11
+ return formatterModulePromise;
12
+ };
13
+ export const preloadFormatterOnIntent = () => (_state, dispatch) => {
14
+ if (!dispatch) {
15
+ // Hover/focus handlers are command-shaped; keep dry-runs side-effect free.
16
+ return false;
17
+ }
18
+ void preloadFormatterModule();
19
+ return false;
20
+ };
21
+ export const formatCode = async ({
22
+ content,
23
+ language
24
+ }) => {
25
+ let formatterModule;
26
+ try {
27
+ formatterModule = await preloadFormatterModule();
28
+ } catch {
29
+ return {
30
+ errorType: 'formatter-load-failed',
31
+ language,
32
+ status: 'failed'
33
+ };
34
+ }
35
+ try {
36
+ return await formatterModule.formatCode({
37
+ content,
38
+ language
39
+ });
40
+ } catch {
41
+ return {
42
+ errorType: 'formatter-execution-failed',
43
+ language,
44
+ status: 'failed'
45
+ };
46
+ }
47
+ };