@atlaskit/editor-plugin-code-block 13.0.1 → 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.
- package/CHANGELOG.md +11 -0
- package/dist/cjs/codeBlockPlugin.js +13 -1
- package/dist/cjs/editor-commands/index.js +179 -5
- package/dist/cjs/pm-plugins/actions.js +6 -3
- package/dist/cjs/pm-plugins/main.js +17 -1
- package/dist/cjs/pm-plugins/toolbar.js +19 -2
- package/dist/cjs/ui/FormatCodeErrorFlag.js +70 -0
- package/dist/cjs/utils/format-code/format-code-state.js +81 -0
- package/dist/cjs/utils/format-code/formatter-impl.js +15 -0
- package/dist/cjs/utils/format-code/formatter.js +86 -0
- package/dist/es2019/codeBlockPlugin.js +12 -2
- package/dist/es2019/editor-commands/index.js +176 -0
- package/dist/es2019/pm-plugins/actions.js +6 -3
- package/dist/es2019/pm-plugins/main.js +18 -1
- package/dist/es2019/pm-plugins/toolbar.js +20 -3
- package/dist/es2019/ui/FormatCodeErrorFlag.js +62 -0
- package/dist/es2019/utils/format-code/format-code-state.js +82 -0
- package/dist/es2019/utils/format-code/formatter-impl.js +10 -0
- package/dist/es2019/utils/format-code/formatter.js +47 -0
- package/dist/esm/codeBlockPlugin.js +13 -1
- package/dist/esm/editor-commands/index.js +178 -4
- package/dist/esm/pm-plugins/actions.js +6 -3
- package/dist/esm/pm-plugins/main.js +17 -1
- package/dist/esm/pm-plugins/toolbar.js +20 -3
- package/dist/esm/ui/FormatCodeErrorFlag.js +61 -0
- package/dist/esm/utils/format-code/format-code-state.js +74 -0
- package/dist/esm/utils/format-code/formatter-impl.js +9 -0
- package/dist/esm/utils/format-code/formatter.js +75 -0
- package/dist/types/codeBlockPluginType.d.ts +3 -0
- package/dist/types/editor-commands/index.d.ts +6 -1
- package/dist/types/pm-plugins/actions.d.ts +6 -3
- package/dist/types/pm-plugins/main-state.d.ts +16 -0
- package/dist/types/ui/FormatCodeErrorFlag.d.ts +6 -0
- package/dist/types/utils/format-code/format-code-state.d.ts +4 -0
- package/dist/types/utils/format-code/formatter-impl.d.ts +5 -0
- package/dist/types/utils/format-code/formatter.d.ts +25 -0
- package/dist/types-ts4.5/codeBlockPluginType.d.ts +3 -0
- package/dist/types-ts4.5/editor-commands/index.d.ts +6 -1
- package/dist/types-ts4.5/pm-plugins/actions.d.ts +6 -3
- package/dist/types-ts4.5/pm-plugins/main-state.d.ts +16 -0
- package/dist/types-ts4.5/ui/FormatCodeErrorFlag.d.ts +6 -0
- package/dist/types-ts4.5/utils/format-code/format-code-state.d.ts +4 -0
- package/dist/types-ts4.5/utils/format-code/formatter-impl.d.ts +5 -0
- package/dist/types-ts4.5/utils/format-code/formatter.d.ts +32 -0
- package/package.json +3 -2
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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,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
|
};
|
|
@@ -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,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
|
+
};
|
|
@@ -10,6 +10,7 @@ import { blockTypeMessages } from '@atlaskit/editor-common/messages';
|
|
|
10
10
|
import { IconCode } from '@atlaskit/editor-common/quick-insert';
|
|
11
11
|
import { fg } from '@atlaskit/platform-feature-flags';
|
|
12
12
|
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
|
|
13
|
+
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
|
|
13
14
|
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
|
|
14
15
|
import { createInsertCodeBlockTransaction, insertCodeBlockWithAnalytics } from './editor-commands';
|
|
15
16
|
import { createAutoDetectPlugin } from './pm-plugins/auto-detect';
|
|
@@ -19,9 +20,11 @@ import ideUX from './pm-plugins/ide-ux';
|
|
|
19
20
|
import { createCodeBlockInputRule } from './pm-plugins/input-rule';
|
|
20
21
|
import keymap from './pm-plugins/keymaps';
|
|
21
22
|
import { createPlugin } from './pm-plugins/main';
|
|
23
|
+
import { pluginKey } from './pm-plugins/plugin-key';
|
|
22
24
|
import refreshBrowserSelectionOnChange from './pm-plugins/refresh-browser-selection';
|
|
23
25
|
import { getToolbarConfig } from './pm-plugins/toolbar';
|
|
24
26
|
import { createCodeBlockMenuItem } from './ui/CodeBlockMenuItem';
|
|
27
|
+
import { FormatCodeErrorFlag } from './ui/FormatCodeErrorFlag';
|
|
25
28
|
var CODE_BLOCK_NODE_NAME = 'codeBlock';
|
|
26
29
|
var codeBlockPlugin = function codeBlockPlugin(_ref) {
|
|
27
30
|
var options = _ref.config,
|
|
@@ -52,11 +55,15 @@ var codeBlockPlugin = function codeBlockPlugin(_ref) {
|
|
|
52
55
|
}];
|
|
53
56
|
},
|
|
54
57
|
getSharedState: function getSharedState(state) {
|
|
58
|
+
var _codeBlockState$forma, _codeBlockState$pendi;
|
|
55
59
|
if (!state) {
|
|
56
60
|
return undefined;
|
|
57
61
|
}
|
|
62
|
+
var codeBlockState = pluginKey.getState(state);
|
|
58
63
|
return {
|
|
59
|
-
copyButtonHoverNode: copySelectionPluginKey.getState(state).codeBlockNode
|
|
64
|
+
copyButtonHoverNode: copySelectionPluginKey.getState(state).codeBlockNode,
|
|
65
|
+
formatCodeErrors: (_codeBlockState$forma = codeBlockState === null || codeBlockState === void 0 ? void 0 : codeBlockState.formatCodeErrors) !== null && _codeBlockState$forma !== void 0 ? _codeBlockState$forma : {},
|
|
66
|
+
pendingFormats: (_codeBlockState$pendi = codeBlockState === null || codeBlockState === void 0 ? void 0 : codeBlockState.pendingFormats) !== null && _codeBlockState$pendi !== void 0 ? _codeBlockState$pendi : {}
|
|
60
67
|
};
|
|
61
68
|
},
|
|
62
69
|
pmPlugins: function pmPlugins() {
|
|
@@ -150,6 +157,11 @@ var codeBlockPlugin = function codeBlockPlugin(_ref) {
|
|
|
150
157
|
}];
|
|
151
158
|
},
|
|
152
159
|
floatingToolbar: getToolbarConfig(options === null || options === void 0 ? void 0 : options.allowCopyToClipboard, api, options === null || options === void 0 ? void 0 : options.overrideLanguageName)
|
|
160
|
+
},
|
|
161
|
+
contentComponent: function contentComponent() {
|
|
162
|
+
return expValEqualsNoExposure('platform_editor_code_block_q4_lovability', 'isEnabled', true) ? /*#__PURE__*/React.createElement(FormatCodeErrorFlag, {
|
|
163
|
+
api: api
|
|
164
|
+
}) : null;
|
|
153
165
|
}
|
|
154
166
|
};
|
|
155
167
|
};
|