FIX: Do not autocomplete categories or emojis in code blocks (#8433)

FIX: Do not autocomplete categories or emojis in code blocks (#8433)

Emojis and category autocomplete show up often when writing code snippets, which makes it easy to insert unwanted text by mistake.

diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6
index e668828ccb..e0b745a47f 100644
--- a/app/assets/javascripts/discourse/components/d-editor.js.es6
+++ b/app/assets/javascripts/discourse/components/d-editor.js.es6
@@ -20,7 +20,9 @@ import { siteDir } from "discourse/lib/text-direction";
 import {
   determinePostReplaceSelection,
   clipboardData,
-  safariHacksDisabled
+  safariHacksDisabled,
+  caretPosition,
+  inCodeBlock
 } from "discourse/lib/utilities";
 import toMarkdown from "discourse/lib/to-markdown";
 import deprecated from "discourse-common/lib/deprecated";
@@ -420,6 +422,10 @@ export default Component.extend({
       },
 
       onKeyUp: (text, cp) => {
+        if (inCodeBlock(text, cp)) {
+          return false;
+        }
+
         const matches = /(?:^|[^a-z])(:(?!:).?[\w-]*:?(?!:)(?:t\d?)?:?) ?$/gi.exec(
           text.substring(0, cp)
         );
@@ -511,7 +517,10 @@ export default Component.extend({
             }
             return list;
           });
-      }
+      },
+
+      triggerRule: textarea =>
+        !inCodeBlock(textarea.value, caretPosition(textarea))
     });
   },
 
diff --git a/app/assets/javascripts/discourse/lib/category-hashtags.js.es6 b/app/assets/javascripts/discourse/lib/category-hashtags.js.es6
index cac265f259..28042fff16 100644
--- a/app/assets/javascripts/discourse/lib/category-hashtags.js.es6
+++ b/app/assets/javascripts/discourse/lib/category-hashtags.js.es6
@@ -1,5 +1,9 @@
 export const SEPARATOR = ":";
-import { caretRowCol } from "discourse/lib/utilities";
+import {
+  caretRowCol,
+  caretPosition,
+  inCodeBlock
+} from "discourse/lib/utilities";
 
 export function replaceSpan($elem, categorySlug, categoryLink) {
   $elem.replaceWith(
@@ -21,10 +25,14 @@ export function categoryHashtagTriggerRule(textarea, opts) {
     if (/^#{1}\w+/.test(line)) return false;
   }
 
-  if (col < 6) {
-    // Don't trigger autocomplete when ATX-style headers are used
-    return line.slice(0, col) !== "#".repeat(col);
-  } else {
-    return true;
+  // Don't trigger autocomplete when ATX-style headers are used
+  if (col < 6 && line.slice(0, col) === "#".repeat(col)) {
+    return false;
   }
+
+  if (inCodeBlock(textarea.value, caretPosition(textarea))) {
+    return false;
+  }
+
+  return true;
 }
diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6
index 145674f3a5..4ab8eb7ccb 100644
--- a/app/assets/javascripts/discourse/lib/utilities.js.es6
+++ b/app/assets/javascripts/discourse/lib/utilities.js.es6
@@ -410,5 +410,44 @@ export function rescueThemeError(name, error, api) {
   document.body.prepend(alertDiv);
 }
 
+const CODE_BLOCKS_RULES = [
+  { rule: /`(?:[^`\n]+?\n?)+?`/gm, end: "`" },
+  { rule: /^`‍``[^]*?^`‍``/gm, end: "\n`‍``" },
+  { rule: /\[code\][^]*?\[\/code\]/gm, end: "\n[/code]" }
+];
+
+export function getCodeBlocks(value) {
+  const blocks = [];
+
+  CODE_BLOCKS_RULES.forEach(entry => {
+    const { rule, end } = entry;
+
+    let match;
+    while ((match = rule.exec(value)) != null) {
+      blocks.push([match.index, match.index + match[0].length]);
+    }
+
+    // Try to end block and see if other code blocks are found
+    if (end) {
+      while ((match = rule.exec(value + end)) != null) {
+        // Save only positions that were not found before (which end past the
+        // end of the original value).
+        if (
+          match.index < value.length &&
+          match.index + match[0].length > value.length
+        ) {
+          blocks.push([match.index, value.length]);
+        }
+      }
+    }
+  });
+
+  return blocks;
+}
+
+export function inCodeBlock(value, pos) {
+  return getCodeBlocks(value).any(([start, end]) => start <= pos && pos <= end);
+}
+
 // This prevents a mini racer crash
 export default {};
diff --git a/test/javascripts/lib/utilities-test.js.es6 b/test/javascripts/lib/utilities-test.js.es6
index 39c9b0635a..46c00735bf 100644
--- a/test/javascripts/lib/utilities-test.js.es6
+++ b/test/javascripts/lib/utilities-test.js.es6
@@ -9,7 +9,9 @@ import {
   setDefaultHomepage,
   caretRowCol,
   setCaretPosition,
-  fillMissingDates
+  fillMissingDates,
+  getCodeBlocks,
+  inCodeBlock
 } from "discourse/lib/utilities";
 
 QUnit.module("lib:utilities");
@@ -186,3 +188,50 @@ QUnit.test("fillMissingDates", assert => {
     "it returns a JSON array with 31 dates"
   );
 });
+
+QUnit.test("getCodeBlocks - works with [code]", assert => {
+  assert.deepEqual(
+    getCodeBlocks("[code]\nfoo\n[/code]\n\nbar\n\n[code]\nbaz"),
+    [
+      [0, 18],
+      [25, 35]
+    ]
+  );
+});
+
+QUnit.test("getCodeBlocks - works with backticks", assert => {
+  assert.deepEqual(getCodeBlocks("foo `bar\nbar`! `baz"), [
+    [4, 13],
+    [15, 19]
+  ]);
+});
+
+QUnit.test("getCodeBlocks - works with triple backticks", assert => {
+  assert.deepEqual(getCodeBlocks("`‍``\nfoo\n`‍``\n\nbar\n\n`‍``\nbaz"), [
+    [0, 11],
+    [18, 25]
+  ]);
+});
+
+QUnit.test("inCodeBlock", assert => {
+  const raw =
+    "bar\n\n`‍``\nfoo\n`‍``\n\nbar\n\n`foo\nfoo`\n\nbar\n\n[code]\nfoo\n[/code]\n\nbar`foo";
+
+  assert.notOk(inCodeBlock(raw, 4));
+  assert.ok(inCodeBlock(raw, 5));
+  assert.ok(inCodeBlock(raw, 16));
+  assert.notOk(inCodeBlock(raw, 17));
+
+  assert.notOk(inCodeBlock(raw, 22));
+  assert.ok(inCodeBlock(raw, 23));
+  assert.ok(inCodeBlock(raw, 32));
+  assert.notOk(inCodeBlock(raw, 33));
+
+  assert.notOk(inCodeBlock(raw, 38));
+  assert.ok(inCodeBlock(raw, 39));
+  assert.ok(inCodeBlock(raw, 57));
+  assert.notOk(inCodeBlock(raw, 58));
+
+  assert.notOk(inCodeBlock(raw, 61));
+  assert.ok(inCodeBlock(raw, 62));
+});

GitHub sha: b643526d

1 Like

Revert "FIX: Do not autocomplete categories or emojis in code blocks (#8433)"