diff --git a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
index 8917417e55e8c7f9eb3b067f9d24c60ad4577f58..da5ac7eb15863debbb498e31f433bbd9ae6f10a3 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -81,4 +81,13 @@ export default CodeBlockLowlight.extend({
   addNodeView() {
     return new VueNodeViewRenderer(CodeBlockWrapper);
   },
+
+  addProseMirrorPlugins() {
+    const parentPlugins = this.parent?.() ?? [];
+    // We don't want TipTap's VSCode paste plugin to be loaded since
+    // it conflicts with our CopyPaste plugin.
+    const i = parentPlugins.findIndex((plugin) => plugin.key.includes('VSCode'));
+    if (i >= 0) parentPlugins.splice(i, 1);
+    return parentPlugins;
+  },
 }).configure({ lowlight });
diff --git a/app/assets/javascripts/content_editor/extensions/copy_paste.js b/app/assets/javascripts/content_editor/extensions/copy_paste.js
index ab9e56196009c6bed4a10a0812def0da3dee545c..3ae5ef0025d8b203d300a078078185188706bd53 100644
--- a/app/assets/javascripts/content_editor/extensions/copy_paste.js
+++ b/app/assets/javascripts/content_editor/extensions/copy_paste.js
@@ -11,6 +11,7 @@ import CodeBlockHighlight from './code_block_highlight';
 import CodeSuggestion from './code_suggestion';
 import Diagram from './diagram';
 import Frontmatter from './frontmatter';
+import { loadingPlugin, findLoader } from './loading';
 
 const TEXT_FORMAT = 'text/plain';
 const GFM_FORMAT = 'text/x-gfm';
@@ -31,21 +32,6 @@ function parseHTML(schema, html) {
   return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body) };
 }
 
-const findLoader = (editor, loaderId) => {
-  let position;
-
-  editor.view.state.doc.descendants((descendant, pos) => {
-    if (descendant.type.name === 'loading' && descendant.attrs.id === loaderId) {
-      position = pos;
-      return false;
-    }
-
-    return true;
-  });
-
-  return position;
-};
-
 export default Extension.create({
   name: 'copyPaste',
   priority: EXTENSION_PRIORITY_HIGHEST,
@@ -74,13 +60,20 @@ export default Extension.create({
 
         Promise.resolve()
           .then(() => {
-            editor.commands.insertContent({ type: 'loading', attrs: { id: loaderId } });
+            editor
+              .chain()
+              .deleteSelection()
+              .setMeta(loadingPlugin, {
+                add: { loaderId, pos: editor.state.selection.from },
+              })
+              .run();
+
             return promise;
           })
           .then(async ({ document }) => {
             if (!document) return;
 
-            const pos = findLoader(editor, loaderId);
+            const pos = findLoader(editor.state, loaderId);
             if (!pos) return;
 
             const { firstChild, childCount } = document.content;
@@ -91,7 +84,7 @@ export default Extension.create({
 
             editor
               .chain()
-              .deleteRange({ from: pos, to: pos + 1 })
+              .setMeta(loadingPlugin, { remove: { loaderId } })
               .insertContentAt(pos, toPaste.toJSON(), {
                 updateSelection: false,
               })
@@ -114,6 +107,7 @@ export default Extension.create({
     const handleCutAndCopy = (view, event) => {
       const slice = view.state.selection.content();
       const gfmContent = this.options.serializer.serialize({ doc: slice.content });
+
       const documentFragment = DOMSerializer.fromSchema(view.state.schema).serializeFragment(
         slice.content,
       );
diff --git a/app/assets/javascripts/content_editor/extensions/loading.js b/app/assets/javascripts/content_editor/extensions/loading.js
index 0115fb10d5d843a01c2b4f74908fc7064fe9360a..942ac650925816559e3cb4a72c819f342c79938c 100644
--- a/app/assets/javascripts/content_editor/extensions/loading.js
+++ b/app/assets/javascripts/content_editor/extensions/loading.js
@@ -1,4 +1,52 @@
 import { Node } from '@tiptap/core';
+import { Decoration, DecorationSet } from '@tiptap/pm/view';
+import { Plugin } from '@tiptap/pm/state';
+
+const createDotsLoader = () => {
+  const root = document.createElement('span');
+  root.classList.add('gl-display-inline-flex', 'gl-align-items-center');
+  root.innerHTML = '<span class="gl-dots-loader gl-mx-2"><span></span></span>';
+  return root;
+};
+
+export const loadingPlugin = new Plugin({
+  state: {
+    init() {
+      return DecorationSet.empty;
+    },
+    apply(tr, set) {
+      let transformedSet = set.map(tr.mapping, tr.doc);
+      const action = tr.getMeta(this);
+
+      if (action?.add) {
+        const deco = Decoration.widget(action.add.pos, createDotsLoader(), {
+          id: action.add.loaderId,
+          side: -1,
+        });
+        transformedSet = transformedSet.add(tr.doc, [deco]);
+      } else if (action?.remove) {
+        transformedSet = transformedSet.remove(
+          transformedSet.find(null, null, (spec) => spec.id === action.remove.loaderId),
+        );
+      }
+      return transformedSet;
+    },
+  },
+  props: {
+    decorations(state) {
+      return this.getState(state);
+    },
+  },
+});
+
+export const findLoader = (state, loaderId) => {
+  const decos = loadingPlugin.getState(state);
+  const found = decos.find(null, null, (spec) => spec.id === loaderId);
+
+  return found.length ? found[0].from : null;
+};
+
+export const findAllLoaders = (state) => loadingPlugin.getState(state).find();
 
 export default Node.create({
   name: 'loading',
@@ -13,11 +61,7 @@ export default Node.create({
     };
   },
 
-  renderHTML() {
-    return [
-      'span',
-      { class: 'gl-display-inline-flex gl-align-items-center' },
-      ['span', { class: 'gl-dots-loader gl-mx-2' }, ['span']],
-    ];
+  addProseMirrorPlugins() {
+    return [loadingPlugin];
   },
 });
diff --git a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
index fc8460c7f8489e3241cfb28d065fad1a555c4c70..6f0c0ee6ed5878ba29aeb5f5f21d1fc25a0a8916 100644
--- a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
+++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js
@@ -61,6 +61,18 @@ describe('content_editor/extensions/code_block_highlight', () => {
 
       expect(editorHtmlOutput.classList.toString()).toContain('content-editor-code-block');
     });
+
+    it('includes the lowlight plugin', () => {
+      expect(tiptapEditor.state.plugins).toContainEqual(
+        expect.objectContaining({ key: expect.stringContaining('lowlight') }),
+      );
+    });
+
+    it('does not include the VSCode paste plugin', () => {
+      expect(tiptapEditor.state.plugins).not.toContainEqual(
+        expect.objectContaining({ key: expect.stringContaining('VSCode') }),
+      );
+    });
   });
 
   describe.each`
diff --git a/spec/frontend/content_editor/extensions/copy_paste_spec.js b/spec/frontend/content_editor/extensions/copy_paste_spec.js
index 4729b1c12238e722afe4bb16b92e0adefe6b9455..49839db44cf1745b42350069545198fab7fca90e 100644
--- a/spec/frontend/content_editor/extensions/copy_paste_spec.js
+++ b/spec/frontend/content_editor/extensions/copy_paste_spec.js
@@ -1,8 +1,9 @@
 import CopyPaste from '~/content_editor/extensions/copy_paste';
 import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
-import Loading from '~/content_editor/extensions/loading';
+import Loading, { findAllLoaders } from '~/content_editor/extensions/loading';
 import Diagram from '~/content_editor/extensions/diagram';
 import Frontmatter from '~/content_editor/extensions/frontmatter';
+import Selection from '~/content_editor/extensions/selection';
 import Heading from '~/content_editor/extensions/heading';
 import Bold from '~/content_editor/extensions/bold';
 import BulletList from '~/content_editor/extensions/bullet_list';
@@ -13,12 +14,7 @@ import eventHubFactory from '~/helpers/event_hub_factory';
 import { ALERT_EVENT } from '~/content_editor/constants';
 import waitForPromises from 'helpers/wait_for_promises';
 import MarkdownSerializer from '~/content_editor/services/markdown_serializer';
-import {
-  createTestEditor,
-  createDocBuilder,
-  waitUntilNextDocTransaction,
-  sleep,
-} from '../test_utils';
+import { createTestEditor, createDocBuilder, waitUntilNextDocTransaction } from '../test_utils';
 
 const CODE_BLOCK_HTML = '<pre class="js-syntax-highlight" lang="javascript">var a = 2;</pre>';
 const CODE_SUGGESTION_HTML =
@@ -35,13 +31,11 @@ describe('content_editor/extensions/copy_paste', () => {
   let p;
   let bold;
   let italic;
-  let loading;
   let heading;
   let codeBlock;
   let bulletList;
   let listItem;
   let renderMarkdown;
-  let resolveRenderMarkdownPromise;
   let resolveRenderMarkdownPromiseAndWait;
 
   let eventHub;
@@ -52,7 +46,6 @@ describe('content_editor/extensions/copy_paste', () => {
     renderMarkdown = jest.fn().mockImplementation(
       () =>
         new Promise((resolve) => {
-          resolveRenderMarkdownPromise = resolve;
           resolveRenderMarkdownPromiseAndWait = (data) =>
             waitUntilNextDocTransaction({ tiptapEditor, action: () => resolve(data) });
         }),
@@ -65,6 +58,7 @@ describe('content_editor/extensions/copy_paste', () => {
         Bold,
         Italic,
         Loading,
+        Selection,
         CodeBlockHighlight,
         Diagram,
         Frontmatter,
@@ -76,17 +70,18 @@ describe('content_editor/extensions/copy_paste', () => {
     });
 
     ({
-      builders: { doc, p, bold, italic, heading, loading, codeBlock, bulletList, listItem },
+      builders: { doc, p, bold, italic, heading, codeBlock, bulletList, listItem },
     } = createDocBuilder({
       tiptapEditor,
       names: {
         bold: { markType: Bold.name },
         italic: { markType: Italic.name },
-        loading: { nodeType: Loading.name },
         heading: { nodeType: Heading.name },
         bulletList: { nodeType: BulletList.name },
         listItem: { nodeType: ListItem.name },
         codeBlock: { nodeType: CodeBlockHighlight.name },
+        diagram: { nodeType: Diagram.name },
+        frontmatter: { nodeType: Frontmatter.name },
       },
     }));
   });
@@ -110,17 +105,6 @@ describe('content_editor/extensions/copy_paste', () => {
     });
   };
 
-  const triggerPasteEventHandlerAndWaitForTransaction = (event) => {
-    return waitUntilNextDocTransaction({
-      tiptapEditor,
-      action: () => {
-        tiptapEditor.view.someProp('handlePaste', (eventHandler) => {
-          return eventHandler(tiptapEditor.view, event);
-        });
-      },
-    });
-  };
-
   it.each`
     types                                                | data                                                  | formatDesc
     ${['text/plain']}                                    | ${{}}                                                 | ${'plain text'}
@@ -185,35 +169,21 @@ describe('content_editor/extensions/copy_paste', () => {
 
   describe('when pasting raw markdown source', () => {
     it('shows a loading indicator while markdown is being processed', async () => {
-      const expectedDoc = doc(p(loading({ id: expect.any(String) })));
+      await triggerPasteEventHandler(buildClipboardEvent());
 
-      await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
-
-      expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+      expect(findAllLoaders(tiptapEditor.state)).toHaveLength(1);
     });
 
     it('pastes in the correct position if some content is added before the markdown is processed', async () => {
       const expectedDoc = doc(p(bold('some markdown'), 'some content'));
       const resolvedValue = '<strong>some markdown</strong>';
 
-      await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+      await triggerPasteEventHandler(buildClipboardEvent());
 
       tiptapEditor.commands.insertContent('some content');
-      await resolveRenderMarkdownPromiseAndWait(resolvedValue);
-
-      expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
-      expect(tiptapEditor.state.selection.from).toEqual(26); // end of the document
-    });
 
-    it('does not paste anything if the loading indicator is deleted before the markdown is processed', async () => {
-      const expectedDoc = doc(p());
-
-      await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
-      tiptapEditor.chain().selectAll().deleteSelection().run();
-      resolveRenderMarkdownPromise('some markdown');
+      await resolveRenderMarkdownPromiseAndWait(resolvedValue);
 
-      // wait some time to be sure no transaction happened
-      await sleep();
       expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
     });
 
@@ -227,7 +197,7 @@ describe('content_editor/extensions/copy_paste', () => {
       it('transforms pasted text into a prosemirror node', async () => {
         const expectedDoc = doc(p(bold('bold text')));
 
-        await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+        await triggerPasteEventHandler(buildClipboardEvent());
         await resolveRenderMarkdownPromiseAndWait(resolvedValue);
 
         expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
@@ -239,7 +209,7 @@ describe('content_editor/extensions/copy_paste', () => {
 
           tiptapEditor.commands.setContent('Initial text and ');
 
-          await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+          await triggerPasteEventHandler(buildClipboardEvent());
           await resolveRenderMarkdownPromiseAndWait(resolvedValue);
 
           expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
@@ -253,7 +223,7 @@ describe('content_editor/extensions/copy_paste', () => {
           tiptapEditor.commands.setContent('Initial text and ');
           tiptapEditor.commands.setTextSelection({ from: 13, to: 17 });
 
-          await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+          await triggerPasteEventHandler(buildClipboardEvent());
           await resolveRenderMarkdownPromiseAndWait(resolvedValue);
 
           expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
@@ -274,7 +244,7 @@ describe('content_editor/extensions/copy_paste', () => {
 
           tiptapEditor.commands.setContent('Initial text and ');
 
-          await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+          await triggerPasteEventHandler(buildClipboardEvent());
           await resolveRenderMarkdownPromiseAndWait(resolvedValue);
 
           expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
@@ -289,7 +259,7 @@ describe('content_editor/extensions/copy_paste', () => {
 
         const expectedDoc = doc(p(bold('bold text')), p('some code'));
 
-        await triggerPasteEventHandlerAndWaitForTransaction(
+        await triggerPasteEventHandler(
           buildClipboardEvent({
             types: ['text/html'],
             data: {
@@ -309,7 +279,7 @@ describe('content_editor/extensions/copy_paste', () => {
         const resolvedValue = '<strong>bold text</strong>';
         const expectedDoc = doc(p(bold('bold text')));
 
-        await triggerPasteEventHandlerAndWaitForTransaction(
+        await triggerPasteEventHandler(
           buildClipboardEvent({
             types: ['text/x-gfm', 'text/plain', 'text/html'],
             data: {
@@ -332,7 +302,7 @@ describe('content_editor/extensions/copy_paste', () => {
           bulletList(listItem(p('Cat')), listItem(p('Dog')), listItem(p('Turtle'))),
         );
 
-        await triggerPasteEventHandlerAndWaitForTransaction(
+        await triggerPasteEventHandler(
           buildClipboardEvent({
             types: ['text/plain', 'text/html'],
             data: {
@@ -359,7 +329,7 @@ describe('content_editor/extensions/copy_paste', () => {
           ),
         );
 
-        await triggerPasteEventHandlerAndWaitForTransaction(
+        await triggerPasteEventHandler(
           buildClipboardEvent({
             types: ['vscode-editor-data', 'text/plain', 'text/html'],
             data: {
@@ -380,7 +350,7 @@ describe('content_editor/extensions/copy_paste', () => {
 
         const expectedDoc = doc(p(bold('bold text')));
 
-        await triggerPasteEventHandlerAndWaitForTransaction(
+        await triggerPasteEventHandler(
           buildClipboardEvent({
             types: ['vscode-editor-data', 'text/plain', 'text/html'],
             data: {
diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb
index 5271b5bd45ca7b82c28f8086d5e312d73c979705..6bfb60c3f3498fce84442a9906d13b4517b7cedd 100644
--- a/spec/support/shared_examples/features/content_editor_shared_examples.rb
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -466,6 +466,15 @@ def assert_selected(text)
       end
     end
 
+    it 'does not show a loading indicator after undo paste' do
+      type_in_content_editor [modifier_key, 'v']
+      type_in_content_editor [modifier_key, 'z']
+
+      page.within content_editor_testid do
+        expect(page).not_to have_css('.gl-dots-loader')
+      end
+    end
+
     it 'pastes raw text without formatting if shift + ctrl + v is pressed' do
       type_in_content_editor [modifier_key, :shift, 'v']