Module for loading per language visual editor extensions

GitOrigin-RevId: 5244e754337a1e48b8de63e881648b2397500b25
This commit is contained in:
Domagoj Kriskovic
2026-06-15 08:06:28 +00:00
committed by Copybot
parent 9743c7006e
commit 7b3692ce6f
4 changed files with 70 additions and 23 deletions
+1 -1
View File
@@ -1027,7 +1027,7 @@ module.exports = {
tprFileViewNotOriginalImporter: [],
contactUsModal: [],
sourceEditorExtensions: [],
sourceEditorMarkdownExtensions: [],
sourceEditorVisualExtensions: [],
sourceEditorComponents: [],
pdfLogEntryHeaderActionComponents: [],
pdfLogEntryComponents: [],
@@ -157,7 +157,7 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
? historyOT(options.currentDoc.currentDocument)
: ranges(),
trackDetachedComments(options.currentDoc),
visual(options.visual),
visual(options.docName, options.visual),
mathPreview(options.settings.mathPreview),
reviewTooltip(options.editorContextMenuEnabled),
contextMenu(options.editorContextMenuEnabled),
@@ -22,20 +22,20 @@ import { commandTooltip } from '../command-tooltip'
import { tableGeneratorTheme } from './table-generator'
import { debugConsole } from '@/utils/debugging'
import { PreviewPath } from '../../../../../../types/preview-path'
import { getFileExtension } from '../../utils/file'
type Options = {
visual: boolean
previewByPath: (path: string) => PreviewPath | null
}
// Language-specific visual editor extensions provided by modules (e.g. the
// markdown visual editor). They live in the visual bundle so they are only
// active in visual mode and react to switching editor modes.
const visualEditorExtensions: Array<(options: Options) => Extension> =
importOverleafModules('sourceEditorMarkdownExtensions').map(
(item: { import: { extension: (options: Options) => Extension } }) =>
item.import.extension
)
// Module-provided visual editors registered via the
// `sourceEditorVisualExtensions` hook. Module exposes `getExtensions(ext)`,
// returning the visual-mode extensions for that file extension. A module match takes precedence
// over the LaTeX fallback
const moduleVisualExtensionProviders: Array<{
import: { getExtensions: (ext: string) => Extension }
}> = importOverleafModules('sourceEditorVisualExtensions')
const visualConf = new Compartment()
@@ -56,12 +56,16 @@ const visualState = StateField.define<boolean>({
})
const configureVisualExtensions = (options: Options) =>
options.visual ? extension(options) : []
options.visual ? sharedVisualExtensions() : []
export const visual = (docName: string, options: Options): Extension => {
const extensions =
visualModuleExtensions(docName) ?? latexVisualExtensions(options)
export const visual = (options: Options): Extension => {
return [
visualState.init(() => options.visual),
visualConf.of(configureVisualExtensions(options)),
visualOnly(options.visual, extensions),
]
}
@@ -78,9 +82,17 @@ export const setVisual = (options: Options): TransactionSpec => {
}
}
export const sourceOnly = (visual: boolean, extension: Extension) => {
// Loads `extension` only while the editor is in a particular mode, reacting to
// mode switches via `toggleVisualEffect`. `activeWhenVisual` selects which mode:
// `true` for visual-only, `false` for source-only.
const modeOnly = (
activeWhenVisual: boolean,
visual: boolean,
extension: Extension
) => {
const conf = new Compartment()
const configure = (visual: boolean) => (visual ? [] : extension)
const configure = (visual: boolean) =>
visual === activeWhenVisual ? extension : []
return [
conf.of(configure(visual)),
@@ -98,6 +110,12 @@ export const sourceOnly = (visual: boolean, extension: Extension) => {
]
}
export const visualOnly = (visual: boolean, extension: Extension) =>
modeOnly(true, visual, extension)
export const sourceOnly = (visual: boolean, extension: Extension) =>
modeOnly(false, visual, extension)
const parsedAttributesConf = new Compartment()
/**
@@ -175,19 +193,39 @@ const scrollJumpAdjuster = EditorState.transactionExtender.of(tr => {
return {}
})
const extension = (options: Options) => [
const sharedVisualExtensions = () => [
visualTheme,
visualHighlightStyle,
mousedown,
scrollJumpAdjuster,
showContentWhenParsed,
EditorView.contentAttributes.of({ 'aria-label': 'Visual Editor editing' }),
]
const latexVisualExtensions = (options: Options): Extension => [
listItemMarker,
atomicDecorations(options),
visualEditorExtensions.map(extension => extension(options)),
markDecorations, // NOTE: must be after atomicDecorations, so that mark decorations wrap inline widgets
visualKeymap,
commandTooltip,
scrollJumpAdjuster,
showContentWhenParsed,
pasteHtml,
tableGeneratorTheme,
EditorView.contentAttributes.of({ 'aria-label': 'Visual Editor editing' }),
]
// Returns the visual-mode extensions provided by a module for the active document
const visualModuleExtensions = (docName: string): Extension | null => {
const fileExt = getFileExtension(docName)
if (!fileExt) {
return null
}
for (const provider of moduleVisualExtensionProviders) {
const extensions = provider.import.getExtensions(fileExt)
if (Array.isArray(extensions) && extensions.length === 0) {
continue
}
return extensions
}
return null
}
@@ -1,10 +1,11 @@
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import { isValidTeXFile } from '../../../main/is-valid-tex-file'
import { getFileExtension } from './file'
const visualEditorProviders = importOverleafModules('visualEditorProviders')
const cmVisualEditorProviders = importOverleafModules(
'sourceEditorMarkdownExtensions'
)
const cmVisualEditorProviders: Array<{
import: { getExtensions: (ext: string) => unknown }
}> = importOverleafModules('sourceEditorVisualExtensions')
/**
* This currently covers LaTeX and Markdown. Other file
@@ -16,8 +17,16 @@ export function isCmVisualEditorAvailable(filename: string): boolean {
return true
}
const extension = getFileExtension(filename)
if (extension === null) {
return false
}
for (const provider of cmVisualEditorProviders) {
if (provider.import.isCmVisualEditorFile?.(filename)) {
const extensions = provider.import.getExtensions(extension)
if (
Array.isArray(extensions) ? extensions.length > 0 : extensions != null
) {
return true
}
}