diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index e45f9a10294c006b7db3b8a5ae33721e167d417f..269546fa9ebe77f27c61ca653849680f0c895c52 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -11,9 +11,13 @@ import initBlob from '~/pages/projects/init_blob';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
+import HighlightWorker from '~/vue_shared/components/source_viewer/workers/highlight_worker?worker';
import '~/sourcegraph/load';
import createStore from '~/code_navigation/store';
+console.time('HighlightWorker lifecycle duration');
+const highlightWorker = new HighlightWorker();
+
Vue.use(Vuex);
Vue.use(VueApollo);
Vue.use(VueRouter);
@@ -38,6 +42,7 @@ if (viewBlobEl) {
provide: {
targetBranch,
originalBranch,
+ highlightWorker,
},
render(createElement) {
return createElement(BlobContentViewer, {
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 101625a4b724dcdf48e126ea0149e76d48405199..3b49a1dc302054ee53b9b64d6674d4e240e85e21 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -15,6 +15,7 @@ import CodeIntelligence from '~/code_navigation/components/app.vue';
import LineHighlighter from '~/blob/line_highlighter';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
import { addBlameLink } from '~/blob/blob_blame_link';
+import handleHighlightMixin from '~/repository/mixins/handle_highlight';
import projectInfoQuery from '../queries/project_info.query.graphql';
import getRefMixin from '../mixins/get_ref';
import userInfoQuery from '../queries/user_info.query.graphql';
@@ -35,7 +36,7 @@ export default {
WebIdeLink,
CodeIntelligence,
},
- mixins: [getRefMixin, glFeatureFlagMixin()],
+ mixins: [getRefMixin, glFeatureFlagMixin(), handleHighlightMixin],
inject: {
originalBranch: {
default: '',
@@ -79,7 +80,9 @@ export default {
shouldFetchRawText: Boolean(this.glFeatures.highlightJs),
};
},
- result() {
+ result({ data }) {
+ this.initHighlightWorker(data.project?.repository?.blobs?.nodes[0] || {});
+
const urlHash = getLocationHash();
const plain = this.$route?.query?.plain;
@@ -384,7 +387,14 @@ export default {
:loading="isLoadingLegacyViewer"
:data-loading="isRenderingLegacyTextViewer"
/>
-
+
import('./image_viewer.vue'),
video: () => import('./video_viewer.vue'),
empty: () => import('./empty_viewer.vue'),
- text: () => import('~/vue_shared/components/source_viewer/source_viewer.vue'),
+ text: () => import('~/vue_shared/components/source_viewer/source_viewer_new.vue'),
pdf: () => import('./pdf_viewer.vue'),
lfs: () => import('./lfs_viewer.vue'),
audio: () => import('./audio_viewer.vue'),
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 494e270a66c91b5f05823f609a57d04e07dc68af..c5014f39029b3a494efd32fd0181791b3f8fb981 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -8,6 +8,7 @@ import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import PerformancePlugin from '~/performance/vue_performance_plugin';
import createStore from '~/code_navigation/store';
import RefSelector from '~/ref/components/ref_selector.vue';
+import HighlightWorker from '~/vue_shared/components/source_viewer/workers/highlight_worker?worker';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
@@ -265,6 +266,9 @@ export default function setupVueRepositoryList() {
store: createStore(),
router,
apolloProvider,
+ provide() {
+ return { highlightWorker: new HighlightWorker() };
+ },
render(h) {
return h(App);
},
diff --git a/app/assets/javascripts/repository/mixins/handle_highlight.js b/app/assets/javascripts/repository/mixins/handle_highlight.js
new file mode 100644
index 0000000000000000000000000000000000000000..dcf3f0235307a0fd2a5baf80258a03ad6e4142b8
--- /dev/null
+++ b/app/assets/javascripts/repository/mixins/handle_highlight.js
@@ -0,0 +1,81 @@
+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';
+
+export default {
+ mixins: [Tracking.mixin()],
+ inject: {
+ highlightWorker: { default: null },
+ },
+ data() {
+ return {
+ chunks: [],
+ };
+ },
+ methods: {
+ trackEvent(label, language) {
+ this.track(EVENT_ACTION, { label, property: language });
+ },
+ unsupportedLanguage(language) {
+ const supportedLanguages = Object.keys(languageLoader);
+ const unsupportedLanguage = !supportedLanguages.includes(language);
+
+ return LEGACY_FALLBACKS.includes(language) || unsupportedLanguage;
+ },
+ handleUnsupportedLanguage(language) {
+ this.trackEvent(EVENT_LABEL_FALLBACK, language);
+ this.onError();
+ },
+ initHighlightWorker({ rawTextBlob, language, simpleViewer }) {
+ if (simpleViewer?.fileType !== TEXT_FILE_TYPE) return;
+
+ if (this.unsupportedLanguage(language)) {
+ this.handleUnsupportedLanguage(language);
+ return;
+ }
+
+ const firstSeventyLines = rawTextBlob.split(/\r?\n/).slice(0, LINES_PER_CHUNK).join('\n');
+
+ this.chunks = splitIntoChunks(language, firstSeventyLines); // Render the first 70 lines (raw text) ASAP, this improves perceived performance and LCP.
+ this.highlightWorker.onmessage = this.handleWorkerMessage;
+
+ this.instructWorker(firstSeventyLines, language); // Instruct the worker to highlight the first 70 lines ASAP, this improves perceived performance.
+ this.instructWorker(rawTextBlob, language); // Then start highlighting all lines in the background.
+ },
+ handleWorkerMessage({ data }) {
+ console.timeEnd('HighlightWorker lifecycle duration');
+ this.chunks = data;
+ this.highlightHash();
+ },
+ 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;
+ }
+
+ await this.$nextTick();
+ const lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
+ lineHighlighter.highlightHash(hash);
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a86e1e69738e0c0e2ab01f642be967ee2d585fed
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
@@ -0,0 +1,153 @@
+
+
+
+
+
+
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
index d694adf7147789e0f595b3894b24164c8e529feb..90f20a98446494403a0bb1fafc144a9ff36de451 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
@@ -1,6 +1,7 @@
import wrapChildNodes from './wrap_child_nodes';
import linkDependencies from './link_dependencies';
import wrapBidiChars from './wrap_bidi_chars';
+import wrapLineNumbers from './wrap_line_numbers';
export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';
@@ -14,6 +15,7 @@ export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';
export const registerPlugins = (hljs, fileType, rawContent) => {
hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapChildNodes });
hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapBidiChars });
+ hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapLineNumbers });
hljs.addPlugin({
[HLJS_ON_AFTER_HIGHLIGHT]: (result) => linkDependencies(result, fileType, rawContent),
});
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_line_numbers.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_line_numbers.js
new file mode 100644
index 0000000000000000000000000000000000000000..3bc0d6b0ebbdd9ef4a3b86f5a2b9fa41768aa62b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_line_numbers.js
@@ -0,0 +1,19 @@
+/**
+ * Highlight.js plugin for wrapping line numbers for things like line highlighting to work.
+ *
+ * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
+ *
+ * @param {Object} Result - an object that represents the highlighted result from Highlight.js
+ */
+
+function wrapLineNumber(content, number) {
+ return `${content}
`;
+}
+
+export default (result) => {
+ // eslint-disable-next-line no-param-reassign
+ result.value = result.value
+ .split(/\r?\n/)
+ .map((content, index) => wrapLineNumber(content, index + 1))
+ .join('\n');
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
new file mode 100644
index 0000000000000000000000000000000000000000..1f07a4dd4684f526c15394e41bba8ba2483a0934
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
index 0da57f9e6fa4175721a553574e367347345f9b1a..e36bcaf576020439e56760d080653247ab98b6e4 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
@@ -1,15 +1,47 @@
-import hljs from 'highlight.js/lib/core';
-import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import hljs from 'highlight.js';
import { registerPlugins } from '../plugins/index';
+import { LINES_PER_CHUNK, ROUGE_TO_HLJS_LANGUAGE_MAP } from '../constants';
-const initHighlightJs = async (fileType, content, language) => {
- const languageDefinition = await languageLoader[language]();
-
+const initHighlightJs = (fileType, content) => {
registerPlugins(hljs, fileType, content);
- hljs.registerLanguage(language, languageDefinition.default);
};
-export const highlight = (fileType, content, language) => {
- initHighlightJs(fileType, content, language);
- return hljs.highlight(content, { language }).value;
+const splitByLineBreaks = (content = '') => content.split(/\r?\n/);
+
+const createChunk = (language, rawChunkLines, highlightedChunkLines = [], startingFrom = 0) => ({
+ highlightedContent: highlightedChunkLines.join('\n'),
+ rawContent: rawChunkLines.join('\n'),
+ startingFrom,
+ totalLines: rawChunkLines.length,
+ language,
+});
+
+const splitIntoChunks = (language, rawContent, highlightedContent) => {
+ const result = [];
+ const splitRawContent = splitByLineBreaks(rawContent);
+ const splitHighlightedContent = splitByLineBreaks(highlightedContent);
+
+ for (let i = 0; i < splitRawContent.length; i += LINES_PER_CHUNK) {
+ const chunkIndex = Math.floor(i / LINES_PER_CHUNK);
+ const highlightedChunk = splitHighlightedContent.slice(i, i + LINES_PER_CHUNK);
+ const rawChunk = splitRawContent.slice(i, i + LINES_PER_CHUNK);
+ result[chunkIndex] = createChunk(language, rawChunk, highlightedChunk, i);
+ }
+
+ return result;
+};
+
+const highlight = (fileType, rawContent, lang) => {
+ const language = ROUGE_TO_HLJS_LANGUAGE_MAP[lang.toLowerCase()];
+ let result;
+
+ if (language) {
+ initHighlightJs(fileType, rawContent, language);
+ const highlightedContent = hljs.highlight(rawContent, { language }).value;
+ result = splitIntoChunks(language, rawContent, highlightedContent);
+ }
+
+ return result;
};
+
+export { highlight, splitIntoChunks };
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js
similarity index 100%
rename from app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js
rename to app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js