Skip to content

feat(document): show text and code documents in a code viewer - #528

Merged
pirhoo merged 15 commits into
mainfrom
feat/code-viewer
Oct 5, 2026
Merged

pirhoo merged 15 commits into
mainfrom
feat/code-viewer

Conversation

@pirhoo

@pirhoo pirhoo commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Text, code, JSON, XML and HTML documents now open in a read-only code viewer with syntax highlighting, line numbers and search.

  • feat: add a read-only code viewer with language detection
  • feat: search inside the code viewer, including very large files
  • feat: route text, code, JSON and HTML documents to the new viewer
  • fix: stop downloading sources larger than 50 MB, even when their size is unknown
  • fix: decode sources with the encoding found at indexing
  • fix: highlight matches correctly after emoji and for Greek words
  • fix: keep blurred documents out of search
  • refactor: remove the JSON tree viewer

Preview

Capture d’écran 2026-09-30 à 10 04 40

@pirhoo
pirhoo requested a review from a team September 30, 2026 08:16
@pirhoo pirhoo added this to Sprint 50 Sep 30, 2026
@pirhoo pirhoo moved this to Done in Sprint 50 Sep 30, 2026
@pirhoo pirhoo self-assigned this Sep 30, 2026
@pirhoo
pirhoo marked this pull request as ready for review September 30, 2026 08:24

@caro3801 caro3801 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice improvement of the code display. I only have suggestions for this PR considering that the cases mentions have low probability of occurring, let me know what you think.

Comment thread src/api/resources/Document.js Outdated
Comment on lines +480 to +484
return this.contentType.indexOf('text/') === 0
|| this.contentType.endsWith('+xml')
|| this.isJson
|| codeTypes.includes(this.contentType)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: the raw content type can contain multiple info
(application/xml; charset=utf-8) the exact match branch would then and lands on "Preview is not available", while text/...; charset=... sails through on the prefix test. This index demonstrably stores parameters (isTweet at line 412 matches
'application/json; twint').
Could we strip the parameter once before testing? codeLanguage.js:14 already does the same split(';') , so the shape is familiar (may be factorized).

Suggested change
return this.contentType.indexOf('text/') === 0
|| this.contentType.endsWith('+xml')
|| this.isJson
|| codeTypes.includes(this.contentType)
}
const mimeType = this.contentType.split(';')[0].trim()
return mimeType.indexOf('text/') === 0
|| mimeType.endsWith('+xml')
|| this.isJson
|| codeTypes.includes(mimeType)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, the type is now stripped of its parameters before the checks.

Comment thread src/utils/codeSearchIndex.js Outdated
* @return {Object[]} - The `{ from, to }` document ranges, in document order.
*/
export function findIndexMatches(chunks, doc, term) {
const { folded } = foldWithSourceIndexes(term)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: foldWithSourceIndexes builds two offset arrays over the term that are destructured away immediately.
foldForFilter (added in this diff, already imported on line 1) returns the same string without them. Worth dropping foldWithSourceIndexes from the line 1 import once this changes, since it becomes unused in this file.

Suggested change
const { folded } = foldWithSourceIndexes(term)
const folded = foldForFilter(term)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, and the unused import is removed.

Comment thread src/utils/strings.js Outdated
Comment on lines +98 to +99
sourceIndexes.push(...Array(units).fill(index))
sourceEnds.push(...Array(units).fill(end))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (perf): any particular reason to drop the push loop ? These two lines allocate two throwaway arrays plus two spreads per character, on the path that folds for every candidate chunk. A plain push loop keeps the behaviour exactly and drops the allocations.

Suggested change
sourceIndexes.push(...Array(units).fill(index))
sourceEnds.push(...Array(units).fill(end))
for (let unit = 0; unit < units; unit++) {
sourceIndexes.push(index)
sourceEnds.push(end)
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason, the spread version was just extra allocations. The loop is back.

Comment on lines +245 to +248
watch(toRef(props, 'document'), async (document) => {
blurred.value = await isBlurred(document)
blurredContent.value = blurred.value ? await getBlurredContentBanner(document) : null
}, { immediate: true })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The superseded-load guards around load are careful and correct on every path, including abort, 404, too-large and unmount. The blurred-content watcher is the one async path that did not get the same superseded-load treatment, so a fast document switch can strand the new document behind the previous project's banner with search disabled.

Could we re-check the document identity after the awaits, the way isCurrentLoad does for the source?

Suggested change
watch(toRef(props, 'document'), async (document) => {
blurred.value = await isBlurred(document)
blurredContent.value = blurred.value ? await getBlurredContentBanner(document) : null
}, { immediate: true })
watch(toRef(props, 'document'), async (document) => {
const value = await isBlurred(document)
const banner = value ? await getBlurredContentBanner(document) : null
// A document swapped in while this one resolved owns the banner now.
if (document !== props.document) {
return
}
blurred.value = value
blurredContent.value = banner
}, { immediate: true })

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, a superseded result is now dropped. There's a new test for a fast document switch.

@pirhoo
pirhoo merged commit 0442dbf into main Oct 5, 2026
4 checks passed
@pirhoo
pirhoo deleted the feat/code-viewer branch October 5, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants