Merge pull request #32884 from overleaf/em-editor-switch-by-filetype

[web] Split editor mode preference by file type

GitOrigin-RevId: 2574623c9c1c88cc66de72c19cffb4428a632e96
This commit is contained in:
Eric Mc Sween
2026-04-20 10:34:58 -04:00
committed by Copybot
parent 99f1551e4b
commit 628e05a278
6 changed files with 64 additions and 20 deletions

View File

@@ -6,18 +6,21 @@ import {
SetStateAction,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import customLocalStorage from '@/infrastructure/local-storage'
import usePersistedState from '@/shared/hooks/use-persisted-state'
import getMeta from '@/utils/meta'
import { useUnstableStoreSync } from '@/shared/hooks/use-unstable-store-sync'
import { sendMB } from '@/infrastructure/event-tracking'
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
import { getVisualEditorStorageKey } from '@/features/source-editor/utils/visual-editor'
// Context value type
export type EditorPropertiesContextValue = {
showVisual: boolean
setShowVisual: Dispatch<SetStateAction<boolean>>
showVisualForFile: (filename: string) => boolean
showSymbolPalette: boolean
setShowSymbolPalette: Dispatch<SetStateAction<boolean>>
toggleSymbolPalette: () => void
@@ -35,31 +38,58 @@ export const EditorPropertiesContext = createContext<
EditorPropertiesContextValue | undefined
>(undefined)
function showVisualFallbackValue() {
function migrateTexVisualMode(): boolean {
const projectId = getMeta('ol-project_id')
const editorModeKey = `editor.mode.${projectId}`
const editorModeVal = customLocalStorage.getItem(editorModeKey)
if (editorModeVal) {
// clean up the old key
customLocalStorage.removeItem(editorModeKey)
return editorModeVal === 'rich-text'
}
return editorModeVal === 'rich-text'
return false
}
export function showVisualForFile(filename: string): boolean {
const key = getVisualEditorStorageKey(filename)
const stored = customLocalStorage.getItem(key)
if (stored !== null) {
return stored === 'visual'
}
if (key === 'editor.lastUsedMode') {
return migrateTexVisualMode()
}
return false
}
export const EditorPropertiesProvider: FC<PropsWithChildren> = ({
children,
}) => {
const [showVisual, setShowVisual] = usePersistedState(
`editor.lastUsedMode`,
showVisualFallbackValue(),
{
converter: {
toPersisted: showVisual => (showVisual ? 'visual' : 'code'),
fromPersisted: mode => mode === 'visual',
},
}
const { openDocName } = useEditorOpenDocContext()
const [showVisual, setShowVisualState] = useState(() =>
openDocName != null ? showVisualForFile(openDocName) : false
)
useEffect(() => {
setShowVisualState(
openDocName != null ? showVisualForFile(openDocName) : false
)
}, [openDocName])
const setShowVisual: Dispatch<SetStateAction<boolean>> = useCallback(
value => {
setShowVisualState(prev => {
const actual = typeof value === 'function' ? value(prev) : value
if (openDocName != null) {
const key = getVisualEditorStorageKey(openDocName)
customLocalStorage.setItem(key, actual ? 'visual' : 'code')
}
return actual
})
},
[openDocName]
)
// Sync the showVisual state with the exposed store
@@ -83,6 +113,7 @@ export const EditorPropertiesProvider: FC<PropsWithChildren> = ({
const value = {
showVisual,
setShowVisual,
showVisualForFile,
showSymbolPalette,
setShowSymbolPalette,
toggleSymbolPalette,

View File

@@ -49,7 +49,7 @@ export const FileTreeOpenProvider: FC<React.PropsWithChildren> = ({
const { eventEmitter, projectJoined } = useIdeReactContext()
const { openDocWithId, openInitialDoc } = useEditorManagerContext()
const { currentDocumentId } = useEditorOpenDocContext()
const { showVisual } = useEditorPropertiesContext()
const { showVisualForFile } = useEditorPropertiesContext()
const { setOpenFile } = useLayoutContext()
const [openEntity, setOpenEntity] = useState<
FileTreeDocumentFindResult | FileTreeFileRefFindResult | null
@@ -93,7 +93,8 @@ export const FileTreeOpenProvider: FC<React.PropsWithChildren> = ({
setOpenEntity(selected)
const editorMode =
isVisualEditorAvailable(selected.entity.name) && showVisual
isVisualEditorAvailable(selected.entity.name) &&
showVisualForFile(selected.entity.name)
? 'visual'
: 'code'
@@ -127,7 +128,7 @@ export const FileTreeOpenProvider: FC<React.PropsWithChildren> = ({
window.dispatchEvent(new CustomEvent('file-view:file-opened'))
}
},
[fileTreeReady, openDocWithId, projectOwner, setOpenFile, showVisual]
[fileTreeReady, openDocWithId, projectOwner, setOpenFile, showVisualForFile]
)
const handleFileTreeDelete = useCallback(

View File

@@ -103,9 +103,9 @@ export const ReactContextRoot: FC<
<Providers.UserProvider>
<Providers.SnapshotProvider>
<Providers.DetachProvider>
<Providers.EditorPropertiesProvider>
<Providers.EditorOpenDocProvider>
<Providers.EditorViewProvider>
<Providers.EditorOpenDocProvider>
<Providers.EditorPropertiesProvider>
<Providers.EditorProvider>
<Providers.TutorialProvider>
<Providers.FileTreeDataProvider>
@@ -151,9 +151,9 @@ export const ReactContextRoot: FC<
</Providers.FileTreeDataProvider>
</Providers.TutorialProvider>
</Providers.EditorProvider>
</Providers.EditorOpenDocProvider>
</Providers.EditorPropertiesProvider>
</Providers.EditorViewProvider>
</Providers.EditorPropertiesProvider>
</Providers.EditorOpenDocProvider>
</Providers.DetachProvider>
</Providers.SnapshotProvider>
</Providers.UserProvider>

View File

@@ -27,3 +27,13 @@ export function getVisualEditorComponent(filename: string) {
}
return null
}
export function getVisualEditorStorageKey(filename: string): string {
for (const provider of visualEditorProviders) {
if (provider.import.isVisualEditorAvailable(filename)) {
const id = provider.import.id
return id != null ? `editor.lastUsedMode.${id}` : 'editor.lastUsedMode'
}
}
return 'editor.lastUsedMode'
}

View File

@@ -92,6 +92,7 @@ const VisualEditorPropertiesProvider: FC<React.PropsWithChildren> = ({
const value = {
showVisual,
setShowVisual,
showVisualForFile: () => showVisual,
showSymbolPalette: true,
setShowSymbolPalette: () => undefined,
toggleSymbolPalette: () => undefined,

View File

@@ -636,6 +636,7 @@ export function makeEditorPropertiesProvider(
const value = {
showVisual,
setShowVisual,
showVisualForFile: () => showVisual,
showSymbolPalette,
setShowSymbolPalette,
toggleSymbolPalette,