Merge pull request #34364 from overleaf/mj-tabs-tweaks

[web] Tweaks for editor tabs release preparation

GitOrigin-RevId: 83de2fddc5410454fa0d55a517cedc7a455b807f
This commit is contained in:
Mathias Jakobsen
2026-06-09 08:06:10 +00:00
committed by Copybot
parent f52dccbee5
commit 38e3104edf
25 changed files with 165 additions and 38 deletions
@@ -30,6 +30,7 @@ async function buildUserSettings(_req, _res, user) {
overallTheme: getOverallTheme(user),
mathPreview: user.ace.mathPreview,
breadcrumbs: user.ace.breadcrumbs,
editorTabs: user.ace.editorTabs ?? true,
nonBlinkingCursor: user.ace.nonBlinkingCursor ?? false,
referencesSearchMode: user.ace.referencesSearchMode,
darkModePdf: user.ace.darkModePdf ?? false,
@@ -425,6 +425,9 @@ async function updateUserSettings(req, res, next) {
if (body.breadcrumbs != null) {
user.ace.breadcrumbs = Boolean(body.breadcrumbs)
}
if (body.editorTabs != null) {
user.ace.editorTabs = Boolean(body.editorTabs)
}
if (body.nonBlinkingCursor != null) {
user.ace.nonBlinkingCursor = Boolean(body.nonBlinkingCursor)
}
+1
View File
@@ -106,6 +106,7 @@ export const UserSchema = new Schema(
lineHeight: { type: String },
mathPreview: { type: Boolean, default: true },
breadcrumbs: { type: Boolean, default: true },
editorTabs: { type: Boolean, default: true },
nonBlinkingCursor: { type: Boolean, default: false },
referencesSearchMode: { type: String, default: 'advanced' }, // 'advanced' or 'simple'
darkModePdf: { type: Boolean, default: false },
@@ -1358,7 +1358,9 @@
"only_project_owner_can_link_github": "",
"open_action_menu": "",
"open_advanced_reference_search": "",
"open_each_file_in_its_own_tab": "",
"open_file": "",
"open_files_in_tabs": "",
"open_link": "",
"open_path": "",
"open_pdf_in_separate_tab": "",
@@ -1837,6 +1839,7 @@
"show_all_projects": "",
"show_breadcrumbs": "",
"show_document_preamble": "",
"show_editor_tabs": "",
"show_equation_preview": "",
"show_file_tree": "",
"show_in_code": "",
@@ -33,6 +33,7 @@ type ProjectSettingsSetterContextValue = {
setPdfViewer: (pdfViewer: UserSettings['pdfViewer']) => void
setMathPreview: (mathPreview: UserSettings['mathPreview']) => void
setBreadcrumbs: (breadcrumbs: UserSettings['breadcrumbs']) => void
setEditorTabs: (editorTabs: UserSettings['editorTabs']) => void
setNonBlinkingCursor: (
nonBlinkingCursor: UserSettings['nonBlinkingCursor']
) => void
@@ -95,6 +96,8 @@ export const ProjectSettingsProvider: FC<React.PropsWithChildren> = ({
setMathPreview,
breadcrumbs,
setBreadcrumbs,
editorTabs,
setEditorTabs,
nonBlinkingCursor,
setNonBlinkingCursor,
darkModePdf,
@@ -149,6 +152,8 @@ export const ProjectSettingsProvider: FC<React.PropsWithChildren> = ({
setMathPreview,
breadcrumbs,
setBreadcrumbs,
editorTabs,
setEditorTabs,
nonBlinkingCursor,
setNonBlinkingCursor,
darkModePdf,
@@ -199,6 +204,8 @@ export const ProjectSettingsProvider: FC<React.PropsWithChildren> = ({
setMathPreview,
breadcrumbs,
setBreadcrumbs,
editorTabs,
setEditorTabs,
nonBlinkingCursor,
setNonBlinkingCursor,
darkModePdf,
@@ -24,6 +24,7 @@ export default function useUserWideSettings() {
pdfViewer,
mathPreview,
breadcrumbs,
editorTabs,
nonBlinkingCursor,
darkModePdf,
zotero,
@@ -130,6 +131,13 @@ export default function useUserWideSettings() {
[saveUserSettings]
)
const setEditorTabs = useCallback(
(editorTabs: UserSettings['editorTabs']) => {
saveUserSettings('editorTabs', editorTabs)
},
[saveUserSettings]
)
const setNonBlinkingCursor = useCallback(
(nonBlinkingCursor: UserSettings['nonBlinkingCursor']) => {
saveUserSettings('nonBlinkingCursor', nonBlinkingCursor)
@@ -196,6 +204,8 @@ export default function useUserWideSettings() {
setMathPreview,
breadcrumbs,
setBreadcrumbs,
editorTabs,
setEditorTabs,
nonBlinkingCursor,
setNonBlinkingCursor,
darkModePdf,
@@ -5,11 +5,11 @@ import FileView from '@/features/file-view/components/file-view'
import { fileViewFile } from '@/features/ide-react/util/file-view'
import MultipleSelectionPane from '@/features/ide-react/components/editor/multiple-selection-pane'
import { TabsContainer } from '@/features/source-editor/components/tabs/tabs-container'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import { useAreTabsEnabled } from '../../hooks/use-are-tabs-enabled'
export default function EditorPanel() {
const { selectedEntityCount, openEntity } = useFileTreeOpenContext()
const tabsEnabled = isSplitTestEnabled('editor-tabs')
const tabsEnabled = useAreTabsEnabled()
return (
<div className="ide-redesign-editor-container">
@@ -25,6 +25,7 @@ import getMeta from '@/utils/meta'
import EditorCloneProjectModalWrapper from '@/features/clone-project-modal/components/editor-clone-project-modal-wrapper'
import useOpenProject from '@/shared/hooks/use-open-project'
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import { useFeatureFlag } from '@/shared/context/split-test-context'
const menubarExtraComponents = importOverleafModules(
'menubarExtraComponents'
@@ -46,6 +47,8 @@ export const ToolbarMenuBar = () => {
const showSupport = getMeta('ol-showSupport')
const showDocumentation = getMeta('ol-wikiEnabled')
const hasEditorTabs = useFeatureFlag('editor-tabs')
useCommandProvider(
() => [
{
@@ -204,8 +207,14 @@ export const ToolbarMenuBar = () => {
[t]
)
const { mathPreview, setMathPreview, breadcrumbs, setBreadcrumbs } =
useProjectSettingsContext()
const {
mathPreview,
setMathPreview,
breadcrumbs,
setBreadcrumbs,
editorTabs,
setEditorTabs,
} = useProjectSettingsContext()
const toggleMathPreview = useCallback(() => {
setMathPreview(!mathPreview)
@@ -215,6 +224,10 @@ export const ToolbarMenuBar = () => {
setBreadcrumbs(!breadcrumbs)
}, [setBreadcrumbs, breadcrumbs])
const toggleEditorTabs = useCallback(() => {
setEditorTabs(!editorTabs)
}, [setEditorTabs, editorTabs])
const { setActiveModal } = useRailContext()
const openKeyboardShortcutsModal = useCallback(() => {
setActiveModal('keyboard-shortcuts')
@@ -252,6 +265,16 @@ export const ToolbarMenuBar = () => {
}
onClick={toggleBreadcrumbs}
/>
{hasEditorTabs && (
<MenuBarOption
eventKey="show_editor_tabs"
title={t('show_editor_tabs')}
leadingIcon={
editorTabs ? 'check' : <DropdownItem.EmptyLeadingIcon />
}
onClick={toggleEditorTabs}
/>
)}
<MenuBarOption
eventKey="show_equation_preview"
title={t('show_equation_preview')}
@@ -14,12 +14,12 @@ import { useFileTreeOpenContext } from './file-tree-open-context'
import { useEditorManagerContext } from './editor-manager-context'
import { debugConsole } from '@/utils/debugging'
import { disambiguatePaths } from '../util/disambiguate-paths'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import {
FileTreeFindResult,
isFileRefResult,
} from '@/features/ide-react/types/file-tree'
import { useAreTabsEnabled } from '../hooks/use-are-tabs-enabled'
type PersistedTabInfo = { id: string; lifetime: Lifetime }
@@ -70,7 +70,7 @@ export const TabsProvider: FC<React.PropsWithChildren> = ({ children }) => {
const { openEntity } = useFileTreeOpenContext()
const { openDocWithId, openFileWithId } = useEditorManagerContext()
const tabsEnabled = isSplitTestEnabled('editor-tabs')
const tabsEnabled = useAreTabsEnabled()
const { userSettings } = useUserSettingsContext()
const { previewTabs } = userSettings
@@ -0,0 +1,8 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import { useFeatureFlag } from '@/shared/context/split-test-context'
export const useAreTabsEnabled = () => {
const { editorTabs } = useProjectSettingsContext()
const featureFlag = useFeatureFlag('editor-tabs')
return featureFlag && Boolean(editorTabs)
}
@@ -2,11 +2,11 @@ import { FC } from 'react'
import { createPortal } from 'react-dom'
import ReviewPanelHeader from './review-panel-header'
import { useTabsContext } from '@/features/ide-react/context/tabs-context'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import useReviewPanelLayout from '../hooks/use-review-panel-layout'
import { useAreTabsEnabled } from '@/features/ide-react/hooks/use-are-tabs-enabled'
const ReviewPanelTabsHeaderPortal: FC = () => {
const editorTabsEnabled = useFeatureFlag('editor-tabs')
const editorTabsEnabled = useAreTabsEnabled()
const { headerSlot } = useTabsContext()
const { showHeader } = useReviewPanelLayout()
@@ -0,0 +1,18 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import { useTranslation } from 'react-i18next'
import ToggleSetting from '../toggle-setting'
export default function EditorTabsSetting() {
const { editorTabs, setEditorTabs } = useProjectSettingsContext()
const { t } = useTranslation()
return (
<ToggleSetting
id="editorTabs"
label={t('open_files_in_tabs')}
description={t('open_each_file_in_its_own_tab')}
checked={editorTabs}
onChange={setEditorTabs}
/>
)
}
@@ -3,7 +3,8 @@ import ToggleSetting from '../toggle-setting'
import { useTranslation } from 'react-i18next'
export default function PreviewTabsSetting() {
const { previewTabs, setPreviewTabs } = useProjectSettingsContext()
const { previewTabs, setPreviewTabs, editorTabs } =
useProjectSettingsContext()
const { t } = useTranslation()
return (
@@ -13,6 +14,7 @@ export default function PreviewTabsSetting() {
description={t('tabs_open_in_preview_mode_until_you_interact_with_them')}
checked={previewTabs}
onChange={setPreviewTabs}
disabled={!editorTabs}
/>
)
}
@@ -36,6 +36,7 @@ import type {
SettingsSection,
SettingsSectionHook,
} from '@/features/settings/context/types'
import EditorTabsSetting from '../components/editor-settings/editor-tabs-setting'
const [referenceSearchSettingModule] = importOverleafModules(
'referenceSearchSetting'
@@ -111,6 +112,11 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
key: 'syntaxValidation',
component: <CodeCheckSetting />,
},
{
key: 'editorTabs',
component: <EditorTabsSetting />,
hidden: !hasEditorTabs,
},
{
key: 'previewTabs',
component: <PreviewTabsSetting />,
@@ -36,6 +36,7 @@ import { useFeatureFlag } from '@/shared/context/split-test-context'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import { useLayoutContext } from '@/shared/context/layout-context'
import ReviewPanelHeaderBuffer from '@/features/review-panel/components/review-panel-header-buffer'
import { useAreTabsEnabled } from '@/features/ide-react/hooks/use-are-tabs-enabled'
const sourceEditorToolbarComponents = importOverleafModules(
'sourceEditorToolbarComponents'
@@ -160,7 +161,7 @@ const Toolbar = memo(function Toolbar() {
}, [buildOverflow, insideTable, resizeRef])
const showActions = !state.readOnly && !insideTable
const tabsVisible = useFeatureFlag('editor-tabs')
const tabsVisible = useAreTabsEnabled()
if (focusMode) {
return null
@@ -98,6 +98,9 @@ export const Tab = memo(function Tab({
const onDragLeave = useCallback(
(e: React.DragEvent) => {
e.stopPropagation()
if (e.currentTarget.contains(e.relatedTarget as Node | null)) {
return
}
throttledOnDragOver.cancel()
setDropTargetPosition(null)
},
@@ -123,7 +123,7 @@ export function TabsContextMenu() {
// focus the tab setting
window.dispatchEvent(
new CustomEvent('ui.focus-setting', {
detail: 'previewTabs',
detail: 'editorTabs',
})
)
close()
@@ -58,7 +58,6 @@ import { tooltipsReposition } from './tooltips-reposition'
import { selectionListener } from '@/features/source-editor/extensions/selection-listener'
import { contextMenu } from './context-menu'
import { tabsListener } from './tabs-listener'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
const moduleExtensions: Array<(options: Record<string, any>) => Extension> =
importOverleafModules('sourceEditorExtensions').map(
@@ -180,5 +179,5 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
fileTreeItemDrop(),
tooltipsReposition(),
selectionListener(options.setEditorSelection),
isSplitTestEnabled('editor-tabs') ? tabsListener() : [],
tabsListener(options.settings.editorTabs),
]
@@ -1,17 +1,31 @@
import { Transaction } from '@codemirror/state'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import { Compartment, Transaction, TransactionSpec } from '@codemirror/state'
import { EditorView } from '@codemirror/view'
export const TAB_USER_EDIT_EVENT = 'tab-user-edit'
export const tabsEvents = new EventTarget()
export const tabsListener = () =>
EditorView.updateListener.of(update => {
if (!update.docChanged) return
for (const transaction of update.transactions) {
if (!transaction.annotation(Transaction.remote)) {
tabsEvents.dispatchEvent(new Event(TAB_USER_EDIT_EVENT))
return
}
const tabsListenerCompartment = new Compartment()
const updateListener = EditorView.updateListener.of(update => {
if (!update.docChanged) return
for (const transaction of update.transactions) {
if (!transaction.annotation(Transaction.remote)) {
tabsEvents.dispatchEvent(new Event(TAB_USER_EDIT_EVENT))
return
}
})
}
})
export const tabsListener = (enabled: boolean) => {
return tabsListenerCompartment.of(
enabled && isSplitTestEnabled('editor-tabs') ? updateListener : []
)
}
export const setEditorTabs = (enabled: boolean): TransactionSpec => ({
effects: tabsListenerCompartment.reconfigure(
enabled && isSplitTestEnabled('editor-tabs') ? updateListener : []
),
})
@@ -64,6 +64,7 @@ import { useEditorSelectionContext } from '@/shared/context/editor-selection-con
import { useActiveEditorTheme } from '@/shared/hooks/use-active-editor-theme'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { isCmVisualEditorAvailable } from '../utils/visual-editor'
import { setEditorTabs } from '../extensions/tabs-listener'
function useCodeMirrorScope(view: EditorView) {
const { fileTreeData } = useFileTreeData()
@@ -89,6 +90,7 @@ function useCodeMirrorScope(view: EditorView) {
mode,
syntaxValidation,
mathPreview,
editorTabs,
nonBlinkingCursor,
referencesSearchMode,
} = userSettings
@@ -160,6 +162,7 @@ function useCodeMirrorScope(view: EditorView) {
mode,
syntaxValidation,
mathPreview,
editorTabs,
nonBlinkingCursor,
referencesSearchMode,
})
@@ -471,6 +474,13 @@ function useCodeMirrorScope(view: EditorView) {
})
}, [view, mathPreview])
useEffect(() => {
settingsRef.current.editorTabs = editorTabs
window.setTimeout(() => {
view.dispatch(setEditorTabs(editorTabs))
})
}, [view, editorTabs])
useEffect(() => {
settingsRef.current.nonBlinkingCursor = nonBlinkingCursor
window.setTimeout(() => {
@@ -29,6 +29,7 @@ export const defaultSettings: UserSettings = {
fontFamily: 'monaco',
lineHeight: 'normal',
mathPreview: true,
editorTabs: true,
referencesSearchMode: 'advanced',
breadcrumbs: true,
nonBlinkingCursor: false,
@@ -2,6 +2,8 @@
--editor-tab-max-path-length: 250px;
--editor-tab-divider-color: var(--neutral-70);
--editor-tab-row-height: var(--review-panel-header-height, 36px);
--editor-tab-drop-indicator-width: 5px;
--editor-tab-divider-width: 1px;
}
@include theme('light') {
@@ -41,12 +43,7 @@
flex: 1;
background: var(--bg-secondary-themed);
overflow-x: auto;
&.editor-tabs-row-hovered {
.editor-file-tab:last-child {
border-right-color: var(--border-primary-themed);
}
}
scrollbar-width: thin;
}
.editor-file-tab {
@@ -54,8 +51,9 @@
--tab-text-color: var(--content-secondary-themed);
display: flex;
border-right: 1px solid var(--editor-tab-divider-color);
border-left: 1px solid transparent;
position: relative;
border-right: var(--editor-tab-divider-width) solid
var(--editor-tab-divider-color);
padding: var(--spacing-01) var(--spacing-02) 0 var(--spacing-02);
&:first-child {
@@ -137,12 +135,27 @@
font-size: var(--font-size-01);
}
&.tab-drop-right {
border-right-color: var(--border-primary-themed);
&.tab-drop-left::after,
&.tab-drop-right::after,
.editor-tabs-row-hovered &:last-child::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
width: var(--editor-tab-drop-indicator-width);
background-color: var(--border-primary-themed);
}
&.tab-drop-left {
border-left-color: var(--border-primary-themed);
&.tab-drop-right::after,
.editor-tabs-row-hovered &:last-child::after {
right: calc(
var(--editor-tab-drop-indicator-width) /
-2 - var(--editor-tab-divider-width)
);
}
&.tab-drop-left::after {
left: calc(var(--editor-tab-drop-indicator-width) / -2);
}
&.tab-temporary {
+3
View File
@@ -1778,7 +1778,9 @@
"open_action_menu": "Open __name__ action menu",
"open_advanced_reference_search": "Open advanced reference search",
"open_as_template": "Open as Template",
"open_each_file_in_its_own_tab": "Open each file in its own tab",
"open_file": "Edit file",
"open_files_in_tabs": "Open files in tabs",
"open_link": "Go to page",
"open_path": "Open __path__",
"open_pdf_in_separate_tab": "Open PDF in separate tab",
@@ -2386,6 +2388,7 @@
"show_all_projects": "Show all projects",
"show_breadcrumbs": "Show breadcrumbs",
"show_document_preamble": "Show document preamble",
"show_editor_tabs": "Show editor tabs",
"show_equation_preview": "Show equation preview",
"show_file_tree": "Show file tree",
"show_in_code": "Show in code",
@@ -137,7 +137,7 @@ function makeEditorViewProvider() {
const editorView = new EditorView({
state: EditorState.create({
extensions: [
tabsListener(),
tabsListener(true),
EditorView.contentAttributes.of({
'data-testid': 'mock-editor-view',
}),
@@ -675,10 +675,10 @@ describe('File Tabs', function () {
'have.been.calledWithMatch',
Cypress.sinon.match({ type: 'ui.toggle-left-menu', detail: true })
)
// ...and focuses the previewTabs setting
// ...and focuses the editorTabs setting
cy.get('@dispatchEvent').should(
'have.been.calledWithMatch',
Cypress.sinon.match({ type: 'ui.focus-setting', detail: 'previewTabs' })
Cypress.sinon.match({ type: 'ui.focus-setting', detail: 'editorTabs' })
)
// and the context menu closes
+1
View File
@@ -27,6 +27,7 @@ export type UserSettings = {
mathPreview: boolean
referencesSearchMode: 'advanced' | 'simple'
breadcrumbs: boolean
editorTabs: boolean
nonBlinkingCursor: boolean
darkModePdf: boolean
zotero: RefProviderSettings