From 5331b6595deda1f9335dc159da3295a8b98782a0 Mon Sep 17 00:00:00 2001
From: Tomas Bulva <tbulva@gitlab.com>
Date: Wed, 5 Mar 2025 13:56:59 +0100
Subject: [PATCH] Added long line truncation for frontend highlighting

This adds an truncating algorithm. We are measuring
if we have highlight clusters and in case the string
is too long and has highlights clusters we will
truncate centered on the largest cluster.

Changelog: fixed
---
 .../search/results/components/blob_chunks.vue |  20 +-
 .../javascripts/search/results/constants.js   |   3 +
 .../javascripts/search/results/utils.js       | 339 +++++++++++++++++-
 .../results/components/blob_chunks_spec.js    |  61 ++++
 spec/frontend/search/results/utils_spec.js    | 157 ++++++--
 yarn.lock                                     |   8 +-
 6 files changed, 536 insertions(+), 52 deletions(-)

diff --git a/app/assets/javascripts/search/results/components/blob_chunks.vue b/app/assets/javascripts/search/results/components/blob_chunks.vue
index 719dd1ce216a4..4256ca41e3624 100644
--- a/app/assets/javascripts/search/results/components/blob_chunks.vue
+++ b/app/assets/javascripts/search/results/components/blob_chunks.vue
@@ -3,7 +3,7 @@ import { GlTooltipDirective, GlIcon, GlLink } from '@gitlab/ui';
 import GlSafeHtmlDirective from '~/vue_shared/directives/safe_html';
 import { s__ } from '~/locale';
 import { InternalEvents } from '~/tracking';
-import { initLineHighlight } from '~/search/results/utils';
+import { initLineHighlight, isUnsupportedLanguage } from '~/search/results/utils';
 import {
   EVENT_CLICK_BLOB_RESULT_LINE,
   EVENT_CLICK_BLOB_RESULT_BLAME_LINE,
@@ -64,9 +64,7 @@ export default {
     },
   },
   mounted() {
-    this.chunk.lines.forEach(async (line, index) => {
-      this.lines[index].richText = await this.codeHighlighting(line);
-    });
+    this.chunk.lines.forEach(this.processLine);
   },
   methods: {
     codeHighlighting(line) {
@@ -74,8 +72,16 @@ export default {
         line,
         fileUrl: this.fileUrl,
         language: this.language.toLowerCase(),
+        that: this,
       });
     },
+    async processLine(line, index) {
+      if (isUnsupportedLanguage(this.language.toLowerCase())) {
+        this.lines[index].text = await this.codeHighlighting(line);
+        return;
+      }
+      this.lines[index].richText = await this.codeHighlighting(line);
+    },
     trackLineClick(lineNumber) {
       this.trackEvent(EVENT_CLICK_BLOB_RESULT_LINE, {
         property: lineNumber,
@@ -136,7 +142,9 @@ export default {
           class="code highlight gl-flex gl-grow"
           data-testid="search-blob-line-code-highlighted"
         >
-          <code v-safe-html="line.richText" class="gl-leading-normal">
+          <code
+            v-safe-html="line.richText"
+            class="gl-leading-normal gl-shrink">
           </code>
         </pre>
         <pre
@@ -144,7 +152,7 @@ export default {
           class="code gl-flex gl-grow"
           data-testid="search-blob-line-code-non-highlighted"
         >
-          <code>
+          <code class="gl-leading-normal gl-shrink">
             <span v-safe-html="line.text" class="line"></span>
           </code>
         </pre>
diff --git a/app/assets/javascripts/search/results/constants.js b/app/assets/javascripts/search/results/constants.js
index 95417ecb8c153..58c743dc8f6f4 100644
--- a/app/assets/javascripts/search/results/constants.js
+++ b/app/assets/javascripts/search/results/constants.js
@@ -22,3 +22,6 @@ export const HIGHLIGHT_MARK = '​';
 export const HIGHLIGHT_MARK_REGEX = '\u200b';
 export const HIGHLIGHT_HTML_START = '<b class="hll">';
 export const HIGHLIGHT_HTML_END = '</b>';
+export const MAXIMUM_LINE_LENGTH = 3000;
+export const ELLIPSIS = '…';
+export const MAX_GAP = 800;
diff --git a/app/assets/javascripts/search/results/utils.js b/app/assets/javascripts/search/results/utils.js
index 23d0653fba7a3..5f410b2719c57 100644
--- a/app/assets/javascripts/search/results/utils.js
+++ b/app/assets/javascripts/search/results/utils.js
@@ -9,8 +9,16 @@ import {
   HIGHLIGHT_MARK_REGEX,
   HIGHLIGHT_HTML_START,
   HIGHLIGHT_HTML_END,
+  MAXIMUM_LINE_LENGTH,
+  ELLIPSIS,
+  MAX_GAP,
 } from './constants';
 
+/**
+ * Checks if a language is unsupported for syntax highlighting
+ * @param {string} language - The language to check
+ * @returns {boolean} - True if the language is unsupported
+ */
 export const isUnsupportedLanguage = (language) => {
   const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
   const supportedLanguages = Object.keys(languageLoader);
@@ -18,10 +26,18 @@ export const isUnsupportedLanguage = (language) => {
   return LEGACY_FALLBACKS.includes(language) || isUnsupported;
 };
 
+/**
+ * Marks the search terms in a string
+ * @param {string} str - The string to mark
+ * @param {Array} highlights - Array of start/end positions for search matches
+ * @returns {string} - String with search terms marked
+ */
 export const markSearchTerm = (str = '', highlights = []) => {
+  if (!str || !highlights?.length) return str;
+
   const chars = str.split('');
-  [...highlights].reverse().forEach((highligh) => {
-    const [start, end] = highligh;
+  [...highlights].reverse().forEach((currentHighlight) => {
+    const [start, end] = currentHighlight;
     chars.splice(end + 1, 0, HIGHLIGHT_MARK);
     chars.splice(start, 0, HIGHLIGHT_MARK);
   });
@@ -29,26 +45,320 @@ export const markSearchTerm = (str = '', highlights = []) => {
   return chars.join('');
 };
 
+/**
+ * Cleans a line of text and marks search terms
+ * @param {Object} params - Input parameters
+ * @param {string} params.text - Text to clean
+ * @param {Array} params.highlights - Highlight positions
+ * @returns {string} - Cleaned text with search terms marked
+ */
 export const cleanLineAndMark = ({ text, highlights } = {}) => {
   const parsedText = highlights?.length > 0 ? markSearchTerm(text, highlights) : text;
-  return parsedText.replace(/\r?\n/, '');
+  return parsedText?.replace(/(\r\n|\r|\n)+/g, '');
 };
 
+/**
+ * Converts invisible markers to HTML highlights
+ * @param {string} highlightedString - String with invisible markers
+ * @returns {string} - String with HTML highlights
+ */
 export const highlightSearchTerm = (highlightedString) => {
-  if (highlightedString.length === 0) {
+  if (!highlightedString || highlightedString.length === 0) {
     return '';
   }
 
   const pattern = new RegExp(`${HIGHLIGHT_MARK_REGEX}(.+?)${HIGHLIGHT_MARK_REGEX}`, 'g');
+  return highlightedString.replace(pattern, `${HIGHLIGHT_HTML_START}$1${HIGHLIGHT_HTML_END}`);
+};
+
+/**
+ * Sorts highlights by starting position
+ * @param {Array} highlights - Highlight positions
+ * @returns {Array} - Sorted highlights
+ */
+const sortHighlights = (highlights) => {
+  return [...highlights].sort((a, b) => a[0] - b[0]);
+};
+
+/**
+ * Checks if highlights are close enough to be in the same cluster
+ * @param {Array} prevHighlight - Previous highlight [start, end]
+ * @param {Array} currentHighlight - Current highlight [start, end]
+ * @param {number} maxGap - Maximum gap between highlights
+ * @returns {boolean} - True if highlights are close
+ */
+const areHighlightsClose = (prevHighlight, currentHighlight, maxGap) => {
+  return currentHighlight[0] - prevHighlight[1] <= maxGap;
+};
+
+/**
+ * Groups highlights into clusters based on proximity
+ * @param {Array} highlights - Array of highlight positions
+ * @param {number} maxGap - Maximum gap between highlights
+ * @returns {Array} - Array of highlight clusters
+ */
+const findHighlightClusters = (highlights, maxGap = MAX_GAP) => {
+  if (!highlights?.length) {
+    return [];
+  }
+
+  const sortedHighlights = sortHighlights(highlights);
+  const clusters = [];
+  let currentCluster = [sortedHighlights[0]];
+
+  for (let i = 1; i < sortedHighlights.length; i += 1) {
+    const prevHighlight = currentCluster[currentCluster.length - 1];
+    const currentHighlight = sortedHighlights[i];
+
+    if (areHighlightsClose(prevHighlight, currentHighlight, maxGap)) {
+      currentCluster.push(currentHighlight);
+    } else {
+      clusters.push(currentCluster);
+      currentCluster = [currentHighlight];
+    }
+  }
+
+  clusters.push(currentCluster);
+  return clusters;
+};
+
+/**
+ * Calculates the total highlighted text length in a cluster
+ * @param {Array} highlights - Array of highlight positions
+ * @returns {number} - Total highlighted length
+ */
+const getTotalHighlightLength = (highlights) => {
+  return highlights.reduce((sum, [start, end]) => sum + (end - start), 0);
+};
+
+/**
+ * Compares two clusters to find the better one
+ * @param {Array} best - Current best cluster
+ * @param {Array} current - Cluster to compare
+ * @returns {Array} - Better cluster
+ */
+const compareClusters = (best, current) => {
+  if (current.length > best.length) {
+    return current;
+  }
+  if (current.length === best.length) {
+    const currentTotal = getTotalHighlightLength(current);
+    const bestTotal = getTotalHighlightLength(best);
+    return currentTotal > bestTotal ? current : best;
+  }
+  return best;
+};
+
+/**
+ * Finds the best cluster of highlights
+ * @param {Array} clusters - Array of highlight clusters
+ * @returns {Array} - Best cluster
+ */
+const findBestCluster = (clusters) => {
+  return clusters.reduce(compareClusters, clusters[0]);
+};
+
+/**
+ * Calculates the center position of a highlight cluster
+ * @param {Array} cluster - Cluster of highlights
+ * @returns {number} - Center position
+ */
+const calculateClusterCenter = (cluster) => {
+  return cluster.reduce((sum, [start, end]) => sum + (start + end) / 2, 0) / cluster.length;
+};
+
+/**
+ * Adjusts truncation boundaries to not break highlights
+ * @param {number} startPos - Initial start position
+ * @param {number} endPos - Initial end position
+ * @param {Array} highlights - Array of highlight positions
+ * @returns {Object} - Adjusted boundaries
+ */
+const adjustBoundariesForHighlights = (startPos, endPos, highlights) => {
+  let adjustedStart = startPos;
+  let adjustedEnd = endPos;
+
+  highlights.forEach(([start, end]) => {
+    if (adjustedStart > start && adjustedStart < end) {
+      adjustedStart = start;
+    }
+    if (adjustedEnd > start && adjustedEnd < end) {
+      adjustedEnd = end;
+    }
+  });
+
+  return { adjustedStart, adjustedEnd };
+};
+
+/**
+ * Determines the optimal starting position for truncation based on highlights
+ * @param {string} text - The full text
+ * @param {Array} highlights - Array of highlight positions
+ * @returns {Object} - Initial start and end positions
+ */
+const initialPosForHighlights = (text, highlights) => {
+  const clusters = findHighlightClusters(highlights);
+  const bestCluster = findBestCluster(clusters);
+  const clusterCenter = calculateClusterCenter(bestCluster);
+
+  const halfLength = Math.floor(MAXIMUM_LINE_LENGTH / 2);
+  let initialStartPos;
+  let initialEndPos;
+
+  if (clusterCenter > text.length - halfLength) {
+    initialEndPos = text.length;
+    initialStartPos = Math.max(0, text.length - MAXIMUM_LINE_LENGTH);
+  } else if (clusterCenter < halfLength) {
+    initialStartPos = 0;
+    initialEndPos = Math.min(text.length, MAXIMUM_LINE_LENGTH);
+  } else {
+    initialStartPos = Math.max(0, Math.floor(clusterCenter - halfLength));
+    initialEndPos = Math.min(text.length, Math.floor(clusterCenter + halfLength));
+  }
 
-  const result = highlightedString.replace(
-    pattern,
-    `${HIGHLIGHT_HTML_START}$1${HIGHLIGHT_HTML_END}`,
+  return { initialStartPos, initialEndPos };
+};
+
+/**
+ * Determines the optimal text region to keep based on highlights
+ * @param {string} text - Original text
+ * @param {Array} highlights - Array of highlight positions
+ * @returns {Object} - Boundaries and flags for truncation
+ */
+const determineOptimalTextRegion = (text, highlights) => {
+  if (!text || text.length <= MAXIMUM_LINE_LENGTH) {
+    return {
+      startPos: 0,
+      endPos: text.length,
+      addLeadingEllipsis: false,
+      addTrailingEllipsis: false,
+    };
+  }
+
+  const hasHighlightsNearEnd = highlights?.some(([, end]) => {
+    return end >= text.length - MAXIMUM_LINE_LENGTH;
+  });
+
+  if (!hasHighlightsNearEnd && (!highlights || highlights.length <= 2)) {
+    return {
+      startPos: 0,
+      endPos: MAXIMUM_LINE_LENGTH,
+      addLeadingEllipsis: false,
+      addTrailingEllipsis: true,
+    };
+  }
+
+  const { initialStartPos, initialEndPos } = initialPosForHighlights(text, highlights);
+  const { adjustedStart, adjustedEnd } = adjustBoundariesForHighlights(
+    initialStartPos,
+    initialEndPos,
+    highlights,
   );
 
-  return result;
+  return {
+    startPos: adjustedStart,
+    endPos: adjustedEnd,
+    addLeadingEllipsis: adjustedStart > 0,
+    addTrailingEllipsis: adjustedEnd < text.length,
+  };
 };
 
+/**
+ *  * Truncates HTML content while preserving HTML structure using DOMParser
+ * @param {string} html - HTML content to truncate
+ * @param {string} originalText - Original text before highlighting
+ * @param {Array} highlights - Array of highlight positions
+ * @returns {string} - Truncated HTML content
+ */
+export const truncateHtml = (html, originalText, highlights) => {
+  if (!html || !html.trim() || html.length <= MAXIMUM_LINE_LENGTH) return html;
+
+  const { startPos, endPos, addLeadingEllipsis, addTrailingEllipsis } = determineOptimalTextRegion(
+    originalText,
+    highlights,
+  );
+
+  const parser = new DOMParser();
+  const doc = parser.parseFromString(`<div>${html}</div>`, 'text/html');
+  const container = doc.body.firstChild;
+
+  const textNodes = [];
+  function collectTextNodes(node) {
+    if (node.nodeType === Node.TEXT_NODE) {
+      textNodes.push(node);
+    } else {
+      Array.from(node.childNodes).forEach((child) => collectTextNodes(child));
+    }
+  }
+  collectTextNodes(container);
+
+  let textLength = 0;
+  let hasStartedTruncating = false;
+  let hasFinishedTruncating = false;
+
+  for (let i = 0; i < textNodes.length; i += 1) {
+    const node = textNodes[i];
+    const text = node.textContent;
+
+    if (!hasStartedTruncating && textLength < startPos) {
+      if (textLength + text.length > startPos) {
+        const offset = startPos - textLength;
+        node.textContent = (addLeadingEllipsis ? ELLIPSIS : '') + text.substring(offset);
+        hasStartedTruncating = true;
+      } else {
+        node.textContent = '';
+      }
+      textLength += text.length;
+      // eslint-disable-next-line no-continue
+      continue;
+    }
+
+    hasStartedTruncating = true;
+
+    if (textLength + text.length > endPos) {
+      const remainingLength = endPos - textLength;
+      node.textContent = text.substring(0, remainingLength) + (addTrailingEllipsis ? ELLIPSIS : '');
+      hasFinishedTruncating = true;
+
+      for (let j = i + 1; j < textNodes.length; j += 1) {
+        textNodes[j].textContent = '';
+      }
+      break;
+    }
+
+    textLength += text.length;
+  }
+
+  if (!hasFinishedTruncating && addTrailingEllipsis) {
+    if (textNodes.length > 0) {
+      const lastNode = textNodes[textNodes.length - 1];
+      lastNode.textContent += ELLIPSIS;
+    }
+  }
+
+  function removeEmptyNodes(node) {
+    const childNodes = Array.from(node.childNodes);
+
+    for (const child of childNodes) {
+      if (child.nodeType === Node.ELEMENT_NODE) {
+        removeEmptyNodes(child);
+
+        if (!child.textContent.trim() && !child.querySelector('img, svg, canvas')) {
+          child.parentNode.removeChild(child);
+        }
+      }
+    }
+  }
+  removeEmptyNodes(container);
+
+  return container.innerHTML;
+};
+
+/**
+ * Highlights code syntax first, then truncates while preserving HTML structure
+ * @param {Object} linesData - Data about the line to highlight
+ * @returns {Promise<string>} - Highlighted and truncated HTML
+ */
 export const initLineHighlight = async (linesData) => {
   const { line, fileUrl } = linesData;
   let { language } = linesData;
@@ -57,12 +367,19 @@ export const initLineHighlight = async (linesData) => {
     language = 'gleam';
   }
 
+  const originalText = line.text;
+  const highlights = line.highlights || [];
+
+  const cleanedLine = cleanLineAndMark(line);
+
   if (isUnsupportedLanguage(language)) {
-    return line.text;
+    const highlightedSearchTerm = highlightSearchTerm(cleanedLine);
+    return truncateHtml(highlightedSearchTerm, originalText, highlights);
   }
 
-  const resultData = await highlight(null, cleanLineAndMark(line), language);
+  const resultData = await highlight(null, cleanedLine, language);
 
   const withHighlightedSearchTerm = highlightSearchTerm(resultData[0].highlightedContent);
-  return withHighlightedSearchTerm;
+
+  return truncateHtml(withHighlightedSearchTerm, originalText, highlights);
 };
diff --git a/spec/frontend/search/results/components/blob_chunks_spec.js b/spec/frontend/search/results/components/blob_chunks_spec.js
index fe6094aad6b43..2c1901bb4e379 100644
--- a/spec/frontend/search/results/components/blob_chunks_spec.js
+++ b/spec/frontend/search/results/components/blob_chunks_spec.js
@@ -3,12 +3,18 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 import waitForPromises from 'helpers/wait_for_promises';
 import BlobChunks from '~/search/results/components/blob_chunks.vue';
 import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
+import { isUnsupportedLanguage } from '~/search/results/utils';
 import {
   EVENT_CLICK_BLOB_RESULT_BLAME_LINE,
   EVENT_CLICK_BLOB_RESULT_LINE,
 } from '~/search/results/tracking';
 import { mockDataForBlobChunk } from '../../mock_data';
 
+jest.mock('~/search/results/utils', () => ({
+  isUnsupportedLanguage: jest.fn(),
+  initLineHighlight: jest.fn(),
+}));
+
 describe('BlobChunks', () => {
   const { bindInternalEventDocument } = useMockInternalEventsTracking();
   let wrapper;
@@ -78,6 +84,11 @@ describe('BlobChunks', () => {
 
   describe('when frontend highlighting', () => {
     beforeEach(async () => {
+      const mockHighlightedText = 'console.log("test")';
+      jest.spyOn(BlobChunks.methods, 'codeHighlighting').mockResolvedValue(mockHighlightedText);
+
+      isUnsupportedLanguage.mockReturnValue(false);
+
       createComponent(mockDataForBlobChunk);
       await waitForPromises();
     });
@@ -87,4 +98,54 @@ describe('BlobChunks', () => {
       expect(findHighlightedLineCode().at(2).text()).toBe('console.log("test")');
     });
   });
+
+  describe('processLine method', () => {
+    const mockHighlightedText = 'highlighted code';
+
+    beforeEach(() => {
+      jest.spyOn(BlobChunks.methods, 'codeHighlighting').mockResolvedValue(mockHighlightedText);
+    });
+
+    describe('with unsupported language', () => {
+      beforeEach(async () => {
+        isUnsupportedLanguage.mockReturnValue(true);
+
+        createComponent({
+          ...mockDataForBlobChunk,
+          language: 'unsupported-lang',
+        });
+
+        await waitForPromises();
+      });
+
+      it('sets line text directly when language is unsupported', () => {
+        const lineElements = wrapper.findAllByTestId('search-blob-line-code-non-highlighted');
+
+        expect(lineElements.exists()).toBe(true);
+        expect(wrapper.vm.lines[0].text).toBe(mockHighlightedText);
+        expect(wrapper.vm.lines[0].richText).toBeNull();
+      });
+    });
+
+    describe('with supported language', () => {
+      beforeEach(async () => {
+        isUnsupportedLanguage.mockReturnValue(false);
+
+        createComponent({
+          ...mockDataForBlobChunk,
+          language: 'javascript',
+        });
+
+        await waitForPromises();
+      });
+
+      it('sets richText when language is supported', () => {
+        const lineElements = wrapper.findAllByTestId('search-blob-line-code-highlighted');
+
+        expect(lineElements.exists()).toBe(true);
+        expect(wrapper.vm.lines[0].richText).toBe(mockHighlightedText);
+        expect(wrapper.vm.lines[0].text).not.toBe(mockHighlightedText);
+      });
+    });
+  });
 });
diff --git a/spec/frontend/search/results/utils_spec.js b/spec/frontend/search/results/utils_spec.js
index b4b6b4cb8c33f..eb8c7247b087c 100644
--- a/spec/frontend/search/results/utils_spec.js
+++ b/spec/frontend/search/results/utils_spec.js
@@ -11,6 +11,7 @@ import {
   isUnsupportedLanguage,
   highlightSearchTerm,
   markSearchTerm,
+  truncateHtml,
 } from '~/search/results/utils';
 
 jest.mock('~/vue_shared/components/source_viewer/workers/highlight_utils', () => ({
@@ -29,6 +30,14 @@ describe('Global Search Results Utils', () => {
     ])('correctly identifies if %s language is unsupported', (language, expected) => {
       expect(isUnsupportedLanguage(language)).toBe(expected);
     });
+
+    it('handles undefined language', () => {
+      expect(isUnsupportedLanguage(undefined)).toBe(true);
+    });
+
+    it('handles null language', () => {
+      expect(isUnsupportedLanguage(null)).toBe(true);
+    });
   });
 
   describe('initLineHighlight', () => {
@@ -36,12 +45,12 @@ describe('Global Search Results Utils', () => {
       highlight.mockClear();
 
       const result = await initLineHighlight({
-        line: { text: 'const test = true;', highlights: [[6, 8]] },
+        line: { text: 'const test = true;', highlights: [[6, 9]] },
         language: 'txt',
         fileUrl: 'test.txt',
       });
 
-      expect(result).toBe('const test = true;');
+      expect(result).toBe('const <b class="hll">test</b> = true;');
       expect(highlight).not.toHaveBeenCalled();
     });
 
@@ -55,22 +64,22 @@ describe('Global Search Results Utils', () => {
       expect(highlight).toHaveBeenCalledWith(null, 'const test = true;', 'gleam');
     });
 
-    describe('when initLineHighlight returns highlight', () => {
-      beforeEach(() => {
-        highlight.mockImplementation((_, input) =>
-          Promise.resolve([{ highlightedContent: input }]),
-        );
+    it('calls highlight with correct parameters', async () => {
+      await initLineHighlight({
+        line: { text: 'const test = true;', highlights: [[6, 9]] },
+        language: 'javascript',
+        fileUrl: 'test.js',
       });
 
-      it('calls highlight with correct parameters', async () => {
-        const result = await initLineHighlight({
-          line: { text: 'const test = true;', highlights: [[6, 9]] },
-          language: 'javascript',
-          fileUrl: 'test.js',
-        });
+      const expected = `const ${HIGHLIGHT_MARK}test${HIGHLIGHT_MARK} = true;`;
+      expect(highlight).toHaveBeenCalled();
 
-        expect(result).toBe('const <b class="hll">test</b> = true;');
-      });
+      const call = highlight.mock.calls[0];
+      expect(call[0]).toBe(null);
+      expect([...call[1]].map((c) => c.charCodeAt(0))).toEqual(
+        [...expected].map((c) => c.charCodeAt(0)),
+      );
+      expect(call[2]).toBe('javascript');
     });
   });
 
@@ -79,6 +88,10 @@ describe('Global Search Results Utils', () => {
       expect(highlightSearchTerm('')).toBe('');
     });
 
+    it('returns original string when no highlights present', () => {
+      expect(highlightSearchTerm('test string')).toBe('test string');
+    });
+
     it('replaces highlight marks with HTML tags', () => {
       const input = `console${HIGHLIGHT_MARK}log${HIGHLIGHT_MARK}(true);`;
       const expected = `console${HIGHLIGHT_HTML_START}log${HIGHLIGHT_HTML_END}(true);`;
@@ -92,30 +105,49 @@ describe('Global Search Results Utils', () => {
 
       expect(highlightSearchTerm(input)).toBe(expected);
     });
+
+    it('handles consecutive highlights', () => {
+      const input = `${HIGHLIGHT_MARK}test${HIGHLIGHT_MARK}${HIGHLIGHT_MARK}string${HIGHLIGHT_MARK}`;
+      const expected = `${HIGHLIGHT_HTML_START}test${HIGHLIGHT_HTML_END}${HIGHLIGHT_HTML_START}string${HIGHLIGHT_HTML_END}`;
+
+      expect(highlightSearchTerm(input)).toBe(expected);
+    });
   });
 
-  describe('markSearchTerm', () => {
-    it('adds highlight marks at correct positions', () => {
-      const text = 'foobar test foobar test';
-      const highlights = [
-        [7, 10],
-        [19, 22],
-      ];
+  describe('cleanLineAndMark', () => {
+    it('adds single highlight mark at correct position', () => {
+      const str = 'const testValue = true;\n';
+      const highlights = [[6, 14]];
 
-      const result = cleanLineAndMark({ text, highlights });
-      const expected = `foobar ${HIGHLIGHT_MARK}test${HIGHLIGHT_MARK} foobar ${HIGHLIGHT_MARK}test${HIGHLIGHT_MARK}`;
+      const result = cleanLineAndMark({ text: str, highlights });
+      const expected = `const ${HIGHLIGHT_MARK}testValue${HIGHLIGHT_MARK} = true;`;
 
       expect([...result].map((c) => c.charCodeAt(0))).toEqual(
         [...expected].map((c) => c.charCodeAt(0)),
       );
     });
 
-    it('adds single highlight mark at correct position', () => {
-      const text = '        return false unless licensed_and_indexing_enabled?\\n';
-      const highlights = [[28, 57]];
+    it('returns empty string for empty input', () => {
+      expect(cleanLineAndMark()).toBe(undefined);
+    });
+
+    it('handles empty highlights array', () => {
+      const str = 'const test = true;';
+
+      expect(cleanLineAndMark({ text: str, highlights: [] })).toBe(str);
+    });
+  });
 
-      const result = cleanLineAndMark({ text, highlights });
-      const expected = `        return false unless ${HIGHLIGHT_MARK}licensed_and_indexing_enabled?${HIGHLIGHT_MARK}\\n`;
+  describe('markSearchTerm', () => {
+    it('adds highlight marks at correct positions', () => {
+      const str = 'foobar test foobar test';
+      const highlights = [
+        [7, 10],
+        [19, 23],
+      ];
+
+      const result = markSearchTerm(str, highlights);
+      const expected = `foobar ${HIGHLIGHT_MARK}test${HIGHLIGHT_MARK} foobar ${HIGHLIGHT_MARK}test${HIGHLIGHT_MARK}`;
 
       expect([...result].map((c) => c.charCodeAt(0))).toEqual(
         [...expected].map((c) => c.charCodeAt(0)),
@@ -132,4 +164,71 @@ describe('Global Search Results Utils', () => {
       expect(markSearchTerm(str, [])).toBe(str);
     });
   });
+
+  describe('truncateHtml', () => {
+    it('returns original HTML if it is shorter than maximum length', () => {
+      const html = '<span>Short text</span>';
+      const text = 'Short text';
+
+      expect(truncateHtml(html, text, [])).toBe(html);
+    });
+
+    it('truncates HTML around highlights', () => {
+      const longText = `${'A'.repeat(5000)}HIGHLIGHT${'B'.repeat(500)}`;
+      const html = `<span>${longText}</span>`;
+      const highlights = [[5000, 5008]];
+
+      const result = truncateHtml(html, longText, highlights);
+
+      expect(result).toContain('HIGHLIGHT');
+      expect(result.length).toBeLessThan(html.length);
+    });
+
+    it('adds ellipsis at both ends when truncating middle', () => {
+      const prefix = `PREFIX${'A'.repeat(300)}`;
+      const middle = 'HIGHLIGHT';
+      const suffix = `${'B'.repeat(3000)}SUFFIX`;
+      const longText = prefix + middle + suffix;
+      const html = `<span>${longText}</span>`;
+      const highlights = [[prefix.length, prefix.length + middle.length]];
+
+      const result = truncateHtml(html, longText, highlights);
+
+      expect(result).toContain('…');
+      expect(result).toContain('HIGHLIGHT');
+    });
+
+    it('preserves HTML structure when truncating', () => {
+      const longText = `${'A'.repeat(500)}HIGHLIGHT${'B'.repeat(500)}`;
+      const html = `<div><span class="c1">${longText}</span></div>`;
+      const highlights = [[500, 508]];
+
+      const result = truncateHtml(html, longText, highlights);
+
+      expect(result).toContain('<span class="c1">');
+      expect(result).toContain('</span>');
+      expect(result).toContain('</div>');
+    });
+
+    it('handles empty or null input', () => {
+      expect(truncateHtml('', '', [])).toBe('');
+      expect(truncateHtml(null, '', [])).toBe(null);
+    });
+
+    it('maintains all highlight clusters when possible', () => {
+      const text1 = `${'A'.repeat(100)}FIRST${'B'.repeat(100)}`;
+      const text2 = `${'C'.repeat(100)}'SECOND${'D'.repeat(100)}`;
+      const longText = text1 + text2;
+      const html = `<span>${longText}</span>`;
+      const highlights = [
+        [100, 105], // FIRST
+        [305, 311], // SECOND
+      ];
+
+      const result = truncateHtml(html, longText, highlights);
+
+      expect(result).toContain('FIRST');
+      expect(result).toContain('SECOND');
+    });
+  });
 });
diff --git a/yarn.lock b/yarn.lock
index 8e6bd461d46b5..f506642b512e0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1412,7 +1412,8 @@
   resolved "https://registry.yarnpkg.com/@gitlab/fonts/-/fonts-1.3.0.tgz#df89c1bb6714e4a8a5d3272568aa4de7fb337267"
   integrity sha512-DoMUIN3DqjEn7wvcxBg/b7Ite5fTdF5EmuOZoBRo2j0UBGweDXmNBi+9HrTZs4cBU660dOxcf1hATFcG3npbPg==
 
-"@gitlab/noop@^1.0.1":
+"@gitlab/noop@^1.0.1", jackspeak@^3.1.2, "jackspeak@npm:@gitlab/noop@1.0.1":
+  name jackspeak
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.1.tgz#71a831146ee02732b4a61d2d3c11204564753454"
   integrity sha512-s++4wjMYeDvBp9IO59DBrWjy8SE/gFkjTDO5ck2W0S6Vv7OlqgErwL7pHngAnrSmTJAzyUG8wHGqo0ViS4jn5Q==
@@ -9393,11 +9394,6 @@ iterall@^1.2.1:
   resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea"
   integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==
 
-jackspeak@^3.1.2, "jackspeak@npm:@gitlab/noop@1.0.1":
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.1.tgz#71a831146ee02732b4a61d2d3c11204564753454"
-  integrity sha512-s++4wjMYeDvBp9IO59DBrWjy8SE/gFkjTDO5ck2W0S6Vv7OlqgErwL7pHngAnrSmTJAzyUG8wHGqo0ViS4jn5Q==
-
 jed@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4"
-- 
GitLab