Kaynağa Gözat

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

[web] Tweaks for editor tabs release preparation

GitOrigin-RevId: 83de2fddc5410454fa0d55a517cedc7a455b807f
Mathias Jakobsen 2 ay önce
ebeveyn
işleme
38e3104edf
25 değiştirilmiş dosya ile 165 ekleme ve 38 silme
  1. 1 0
      services/web/app/src/Features/Project/UserSettingsHelper.mjs
  2. 3 0
      services/web/app/src/Features/User/UserController.mjs
  3. 1 0
      services/web/app/src/models/User.mjs
  4. 3 0
      services/web/frontend/extracted-translations.json
  5. 7 0
      services/web/frontend/js/features/editor-left-menu/context/project-settings-context.tsx
  6. 10 0
      services/web/frontend/js/features/editor-left-menu/hooks/use-user-wide-settings.tsx
  7. 2 2
      services/web/frontend/js/features/ide-react/components/editor/editor-panel.tsx
  8. 25 2
      services/web/frontend/js/features/ide-react/components/toolbar/menu-bar.tsx
  9. 2 2
      services/web/frontend/js/features/ide-react/context/tabs-context.tsx
  10. 8 0
      services/web/frontend/js/features/ide-react/hooks/use-are-tabs-enabled.ts
  11. 2 2
      services/web/frontend/js/features/review-panel/components/review-panel-tabs-header-portal.tsx
  12. 18 0
      services/web/frontend/js/features/settings/components/editor-settings/editor-tabs-setting.tsx
  13. 3 1
      services/web/frontend/js/features/settings/components/editor-settings/preview-tabs-setting.tsx
  14. 6 0
      services/web/frontend/js/features/settings/context/settings-modal-context.tsx
  15. 2 1
      services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx
  16. 3 0
      services/web/frontend/js/features/source-editor/components/tabs/tab.tsx
  17. 1 1
      services/web/frontend/js/features/source-editor/components/tabs/tabs-context-menu.tsx
  18. 1 2
      services/web/frontend/js/features/source-editor/extensions/index.ts
  19. 24 10
      services/web/frontend/js/features/source-editor/extensions/tabs-listener.ts
  20. 10 0
      services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts
  21. 1 0
      services/web/frontend/js/shared/context/user-settings-context.tsx
  22. 25 12
      services/web/frontend/stylesheets/pages/editor/tabs.scss
  23. 3 0
      services/web/locales/en.json
  24. 3 3
      services/web/test/frontend/features/source-editor/components/tabs.spec.tsx
  25. 1 0
      services/web/types/user-settings.ts

+ 1 - 0
services/web/app/src/Features/Project/UserSettingsHelper.mjs

@@ -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,

+ 3 - 0
services/web/app/src/Features/User/UserController.mjs

@@ -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 - 0
services/web/app/src/models/User.mjs

@@ -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 },

+ 3 - 0
services/web/frontend/extracted-translations.json

@@ -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": "",

+ 7 - 0
services/web/frontend/js/features/editor-left-menu/context/project-settings-context.tsx

@@ -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,

+ 10 - 0
services/web/frontend/js/features/editor-left-menu/hooks/use-user-wide-settings.tsx

@@ -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,

+ 2 - 2
services/web/frontend/js/features/ide-react/components/editor/editor-panel.tsx

@@ -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 - 2
services/web/frontend/js/features/ide-react/components/toolbar/menu-bar.tsx

@@ -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')}

+ 2 - 2
services/web/frontend/js/features/ide-react/context/tabs-context.tsx

@@ -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
 

+ 8 - 0
services/web/frontend/js/features/ide-react/hooks/use-are-tabs-enabled.ts

@@ -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 - 2
services/web/frontend/js/features/review-panel/components/review-panel-tabs-header-portal.tsx

@@ -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()
 

+ 18 - 0
services/web/frontend/js/features/settings/components/editor-settings/editor-tabs-setting.tsx

@@ -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 - 1
services/web/frontend/js/features/settings/components/editor-settings/preview-tabs-setting.tsx

@@ -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}
     />
   )
 }

+ 6 - 0
services/web/frontend/js/features/settings/context/settings-modal-context.tsx

@@ -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 />,

+ 2 - 1
services/web/frontend/js/features/source-editor/components/codemirror-toolbar.tsx

@@ -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

+ 3 - 0
services/web/frontend/js/features/source-editor/components/tabs/tab.tsx

@@ -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)
     },

+ 1 - 1
services/web/frontend/js/features/source-editor/components/tabs/tabs-context-menu.tsx

@@ -123,7 +123,7 @@ export function TabsContextMenu() {
               // focus the tab setting
               window.dispatchEvent(
                 new CustomEvent('ui.focus-setting', {
-                  detail: 'previewTabs',
+                  detail: 'editorTabs',
                 })
               )
               close()

+ 1 - 2
services/web/frontend/js/features/source-editor/extensions/index.ts

@@ -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),
 ]

+ 24 - 10
services/web/frontend/js/features/source-editor/extensions/tabs-listener.ts

@@ -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 : []
+  ),
+})

+ 10 - 0
services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts

@@ -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(() => {

+ 1 - 0
services/web/frontend/js/shared/context/user-settings-context.tsx

@@ -29,6 +29,7 @@ export const defaultSettings: UserSettings = {
   fontFamily: 'monaco',
   lineHeight: 'normal',
   mathPreview: true,
+  editorTabs: true,
   referencesSearchMode: 'advanced',
   breadcrumbs: true,
   nonBlinkingCursor: false,

+ 25 - 12
services/web/frontend/stylesheets/pages/editor/tabs.scss

@@ -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-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 {
-    border-left-color: var(--border-primary-themed);
+  &.tab-drop-left::after {
+    left: calc(var(--editor-tab-drop-indicator-width) / -2);
   }
 
   &.tab-temporary {

+ 3 - 0
services/web/locales/en.json

@@ -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",

+ 3 - 3
services/web/test/frontend/features/source-editor/components/tabs.spec.tsx

@@ -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 - 0
services/web/types/user-settings.ts

@@ -27,6 +27,7 @@ export type UserSettings = {
   mathPreview: boolean
   referencesSearchMode: 'advanced' | 'simple'
   breadcrumbs: boolean
+  editorTabs: boolean
   nonBlinkingCursor: boolean
   darkModePdf: boolean
   zotero: RefProviderSettings