summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/repository/mixins/highlight_mixin.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/repository/mixins/highlight_mixin.js')
-rw-r--r--app/assets/javascripts/repository/mixins/highlight_mixin.js106
1 files changed, 106 insertions, 0 deletions
diff --git a/app/assets/javascripts/repository/mixins/highlight_mixin.js b/app/assets/javascripts/repository/mixins/highlight_mixin.js
new file mode 100644
index 00000000000..95d0c55bb04
--- /dev/null
+++ b/app/assets/javascripts/repository/mixins/highlight_mixin.js
@@ -0,0 +1,106 @@
+import { nextTick } from 'vue';
+import {
+ LEGACY_FALLBACKS,
+ EVENT_ACTION,
+ EVENT_LABEL_FALLBACK,
+ LINES_PER_CHUNK,
+} from '~/vue_shared/components/source_viewer/constants';
+import { splitIntoChunks } from '~/vue_shared/components/source_viewer/workers/highlight_utils';
+import LineHighlighter from '~/blob/line_highlighter';
+import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import Tracking from '~/tracking';
+import { TEXT_FILE_TYPE } from '../constants';
+
+/*
+ * This mixin is intended to be used as an interface between our highlight worker and Vue components
+ */
+export default {
+ mixins: [Tracking.mixin()],
+ inject: {
+ highlightWorker: { default: null },
+ },
+ data() {
+ return {
+ chunks: [],
+ };
+ },
+ methods: {
+ trackEvent(label, language) {
+ this.track(EVENT_ACTION, { label, property: language });
+ },
+ isUnsupportedLanguage(language) {
+ const supportedLanguages = Object.keys(languageLoader);
+ const isUnsupportedLanguage = !supportedLanguages.includes(language);
+
+ return LEGACY_FALLBACKS.includes(language) || isUnsupportedLanguage;
+ },
+ handleUnsupportedLanguage(language) {
+ this.trackEvent(EVENT_LABEL_FALLBACK, language);
+ this?.onError();
+ },
+ initHighlightWorker({ rawTextBlob, language, simpleViewer }) {
+ if (simpleViewer?.fileType !== TEXT_FILE_TYPE) return;
+
+ if (this.isUnsupportedLanguage(language)) {
+ this.handleUnsupportedLanguage(language);
+ return;
+ }
+
+ /*
+ * We want to start rendering content as soon as possible, but highlighting large amounts of
+ * content can take long, so we render the content in phases:
+ *
+ * 1. `splitIntoChunks` with the first 70 lines of raw text.
+ * This ensures that we start rendering raw content in the DOM as soon as we can so that
+ * the user can see content as fast as possible (improves perceived performance and LCP).
+ * 2. `instructWorker` to start highlighting the first 70 lines.
+ * This ensures that we display highlighted** content to the user as fast as possible
+ * (improves perceived performance and makes the first 70 lines look nice).
+ * 3. `instructWorker` to start highlighting all the content.
+ * This is the longest task. It ensures that we highlight all content, since the first 70
+ * lines are already rendered, this can happen in the background.
+ */
+
+ // Render the first 70 lines (raw text) ASAP, this improves perceived performance and LCP.
+ const firstSeventyLines = rawTextBlob.split(/\r?\n/).slice(0, LINES_PER_CHUNK).join('\n');
+
+ this.chunks = splitIntoChunks(language, firstSeventyLines);
+
+ this.highlightWorker.onmessage = this.handleWorkerMessage;
+
+ // Instruct the worker to highlight the first 70 lines ASAP, this improves perceived performance.
+ this.instructWorker(firstSeventyLines, language);
+
+ // Instruct the worker to start highlighting all lines in the background.
+ this.instructWorker(rawTextBlob, language);
+ },
+ handleWorkerMessage({ data }) {
+ this.chunks = data;
+ this.highlightHash(); // highlight the line if a line number hash is present in the URL
+ },
+ instructWorker(content, language) {
+ this.highlightWorker.postMessage({ content, language });
+ },
+ async highlightHash() {
+ const { hash } = this.$route;
+ if (!hash) return;
+
+ // Make the chunk containing the line number visible
+ const lineNumber = hash.substring(hash.indexOf('L') + 1).split('-')[0];
+ const chunkToHighlight = this.chunks.find(
+ (chunk) =>
+ chunk.startingFrom <= lineNumber && chunk.startingFrom + chunk.totalLines >= lineNumber,
+ );
+
+ if (chunkToHighlight) {
+ chunkToHighlight.isHighlighted = true;
+ }
+
+ // Line numbers in the DOM needs to update first based on changes made to `chunks`.
+ await nextTick();
+
+ const lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
+ lineHighlighter.highlightHash(hash);
+ },
+ },
+};