diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-20 13:00:54 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-20 13:00:54 +0300 |
commit | 3cccd102ba543e02725d247893729e5c73b38295 (patch) | |
tree | f36a04ec38517f5deaaacb5acc7d949688d1e187 /app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue | |
parent | 205943281328046ef7b4528031b90fbda70c75ac (diff) |
Add latest changes from gitlab-org/gitlab@14-10-stable-eev14.10.0-rc42
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue')
-rw-r--r-- | app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue | 169 |
1 files changed, 118 insertions, 51 deletions
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue index 4a78cbacec0..edf2229a9a1 100644 --- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue @@ -1,16 +1,22 @@ <script> import { GlSafeHtmlDirective, GlLoadingIcon } from '@gitlab/ui'; -import LineNumbers from '~/vue_shared/components/line_numbers.vue'; -import { sanitize } from '~/lib/dompurify'; -import { ROUGE_TO_HLJS_LANGUAGE_MAP } from './constants'; -import { wrapLines } from './utils'; - -const LINE_SELECT_CLASS_NAME = 'hll'; +import LineHighlighter from '~/blob/line_highlighter'; +import eventHub from '~/notes/event_hub'; +import { ROUGE_TO_HLJS_LANGUAGE_MAP, LINES_PER_CHUNK } from './constants'; +import Chunk from './components/chunk.vue'; +/* + * This component is optimized to handle source code with many lines of code by splitting source code into chunks of 70 lines of code, + * we highlight and display the 1st chunk (L1-70) to the user as quickly as possible. + * + * The rest of the lines (L71+) is rendered once the browser goes into an idle state (requestIdleCallback). + * Each chunk is self-contained, this ensures when for example the width of a container on line 1000 changes, + * it does not trigger a repaint on a parent element that wraps all 1000 lines. + */ export default { components: { - LineNumbers, GlLoadingIcon, + Chunk, }, directives: { SafeHtml: GlSafeHtmlDirective, @@ -27,46 +33,94 @@ export default { content: this.blob.rawTextBlob, language: ROUGE_TO_HLJS_LANGUAGE_MAP[this.blob.language], hljs: null, + firstChunk: null, + chunks: {}, + isLoading: true, + isLineSelected: false, + lineHighlighter: null, }; }, computed: { + splitContent() { + return this.content.split('\n'); + }, lineNumbers() { - return this.content.split('\n').length; + return this.splitContent.length; }, - highlightedContent() { - let highlightedContent; - let { language } = this; + }, + async created() { + this.generateFirstChunk(); + this.hljs = await this.loadHighlightJS(); - if (this.hljs) { - if (!language) { - const hljsHighlightAuto = this.hljs.highlightAuto(this.content); + if (this.language) { + this.languageDefinition = await this.loadLanguage(); + } - highlightedContent = hljsHighlightAuto.value; - language = hljsHighlightAuto.language; - } else if (this.languageDefinition) { - highlightedContent = this.hljs.highlight(this.content, { language: this.language }).value; - } + // Highlight the first chunk as soon as highlight.js is available + this.highlightChunk(null, true); + + window.requestIdleCallback(async () => { + // Generate the remaining chunks once the browser idles to ensure the browser resources are spent on the most important things first + this.generateRemainingChunks(); + this.isLoading = false; + await this.$nextTick(); + this.lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' }); + }); + }, + methods: { + generateFirstChunk() { + const lines = this.splitContent.splice(0, LINES_PER_CHUNK); + this.firstChunk = this.createChunk(lines); + }, + generateRemainingChunks() { + const result = {}; + for (let i = 0; i < this.splitContent.length; i += LINES_PER_CHUNK) { + const chunkIndex = Math.floor(i / LINES_PER_CHUNK); + const lines = this.splitContent.slice(i, i + LINES_PER_CHUNK); + result[chunkIndex] = this.createChunk(lines, i + LINES_PER_CHUNK); } - return wrapLines(highlightedContent, language); + this.chunks = result; }, - }, - watch: { - highlightedContent() { - this.$nextTick(() => this.selectLine()); + createChunk(lines, startingFrom = 0) { + return { + content: lines.join('\n'), + startingFrom, + totalLines: lines.length, + language: this.language, + isHighlighted: false, + }; }, - $route() { + highlightChunk(index, isFirstChunk) { + const chunk = isFirstChunk ? this.firstChunk : this.chunks[index]; + + if (chunk.isHighlighted) { + return; + } + + const { highlightedContent, language } = this.highlight(chunk.content, this.language); + + Object.assign(chunk, { language, content: highlightedContent, isHighlighted: true }); + this.selectLine(); + + this.$nextTick(() => eventHub.$emit('showBlobInteractionZones', this.blob.path)); }, - }, - async mounted() { - this.hljs = await this.loadHighlightJS(); + highlight(content, language) { + let detectedLanguage = language; + let highlightedContent; + if (this.hljs) { + if (!detectedLanguage) { + const hljsHighlightAuto = this.hljs.highlightAuto(content); + highlightedContent = hljsHighlightAuto.value; + detectedLanguage = hljsHighlightAuto.language; + } else if (this.languageDefinition) { + highlightedContent = this.hljs.highlight(content, { language: this.language }).value; + } + } - if (this.language) { - this.languageDefinition = await this.loadLanguage(); - } - }, - methods: { + return { highlightedContent, language: detectedLanguage }; + }, loadHighlightJS() { // If no language can be mapped to highlight.js we load all common languages else we load only the core (smallest footprint) return !this.language ? import('highlight.js/lib/common') : import('highlight.js/lib/core'); @@ -83,21 +137,14 @@ export default { return languageDefinition; }, - selectLine() { - const hash = sanitize(this.$route.hash); - const lineToSelect = hash && this.$el.querySelector(hash); - - if (!lineToSelect) { + async selectLine() { + if (this.isLineSelected || !this.lineHighlighter) { return; } - if (this.$options.currentlySelectedLine) { - this.$options.currentlySelectedLine.classList.remove(LINE_SELECT_CLASS_NAME); - } - - lineToSelect.classList.add(LINE_SELECT_CLASS_NAME); - this.$options.currentlySelectedLine = lineToSelect; - lineToSelect.scrollIntoView({ behavior: 'smooth', block: 'center' }); + this.isLineSelected = true; + await this.$nextTick(); + this.lineHighlighter.highlightHash(this.$route.hash); }, }, userColorScheme: window.gon.user_color_scheme, @@ -105,16 +152,36 @@ export default { }; </script> <template> - <gl-loading-icon v-if="!highlightedContent" size="sm" class="gl-my-5" /> <div - v-else - class="file-content code js-syntax-highlight blob-content gl-display-flex" + class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto" :class="$options.userColorScheme" data-type="simple" + :data-path="blob.path" data-qa-selector="blob_viewer_file_content" > - <line-numbers :lines="lineNumbers" /> - <pre class="code highlight gl-pb-0!"><code v-safe-html="highlightedContent"></code> - </pre> + <chunk + v-if="firstChunk" + :lines="firstChunk.lines" + :total-lines="firstChunk.totalLines" + :content="firstChunk.content" + :starting-from="firstChunk.startingFrom" + :is-highlighted="firstChunk.isHighlighted" + :language="firstChunk.language" + /> + + <gl-loading-icon v-if="isLoading" size="sm" class="gl-my-5" /> + <chunk + v-for="(chunk, key, index) in chunks" + v-else + :key="key" + :lines="chunk.lines" + :content="chunk.content" + :total-lines="chunk.totalLines" + :starting-from="chunk.startingFrom" + :is-highlighted="chunk.isHighlighted" + :chunk-index="index" + :language="chunk.language" + @appear="highlightChunk" + /> </div> </template> |