Merge pull request #32642 from overleaf/ab-non-blinking-cursor-editor-setting

[web] Add non-blinking cursor editor setting

GitOrigin-RevId: 078efca21c1e87905894aadc1430c1728a38685d
This commit is contained in:
Alexandre Bourdin
2026-04-13 08:04:03 +00:00
committed by Copybot
parent 3f62eac181
commit 50db96878d
16 changed files with 155 additions and 0 deletions
@@ -44,6 +44,7 @@ async function buildUserSettings(req, res, user) {
overallTheme: await getOverallTheme(req, res, user),
mathPreview: user.ace.mathPreview,
breadcrumbs: user.ace.breadcrumbs,
nonBlinkingCursor: user.ace.nonBlinkingCursor ?? false,
referencesSearchMode: user.ace.referencesSearchMode,
darkModePdf: user.ace.darkModePdf ?? false,
zotero: user.ace.zotero,
@@ -421,6 +421,9 @@ async function updateUserSettings(req, res, next) {
if (body.breadcrumbs != null) {
user.ace.breadcrumbs = Boolean(body.breadcrumbs)
}
if (body.nonBlinkingCursor != null) {
user.ace.nonBlinkingCursor = Boolean(body.nonBlinkingCursor)
}
if (body.referencesSearchMode != null) {
const mode = body.referencesSearchMode === 'simple' ? 'simple' : 'advanced'
user.ace.referencesSearchMode = mode
+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 },
nonBlinkingCursor: { type: Boolean, default: false },
referencesSearchMode: { type: String, default: 'advanced' }, // 'advanced' or 'simple'
darkModePdf: { type: Boolean, default: false },
zotero: refProviderSettingsSchema,
@@ -1231,6 +1231,7 @@
"no_symbols_found": "",
"no_thanks_cancel_now": "",
"no_token": "",
"non_blinking_cursor": "",
"normal": "",
"normally_x_price_per_month": "",
"normally_x_price_per_year": "",
@@ -1497,6 +1498,7 @@
"redirect_url": "",
"redo": "",
"reduce_costs_group_licenses": "",
"reduces_visual_distraction_by_keeping_the_cursor_solid": "",
"reference_error_relink_hint": "",
"reference_manager_linked": "",
"reference_manager_lowercase": "",
@@ -33,6 +33,9 @@ type ProjectSettingsSetterContextValue = {
setPdfViewer: (pdfViewer: UserSettings['pdfViewer']) => void
setMathPreview: (mathPreview: UserSettings['mathPreview']) => void
setBreadcrumbs: (breadcrumbs: UserSettings['breadcrumbs']) => void
setNonBlinkingCursor: (
nonBlinkingCursor: UserSettings['nonBlinkingCursor']
) => void
setDarkModePdf: (darkModePdf: UserSettings['darkModePdf']) => void
setZotero: (zotero: UserSettings['zotero']) => void
setMendeley: (mendeley: UserSettings['mendeley']) => void
@@ -92,6 +95,8 @@ export const ProjectSettingsProvider: FC<React.PropsWithChildren> = ({
setMathPreview,
breadcrumbs,
setBreadcrumbs,
nonBlinkingCursor,
setNonBlinkingCursor,
darkModePdf,
setDarkModePdf,
zotero,
@@ -144,6 +149,8 @@ export const ProjectSettingsProvider: FC<React.PropsWithChildren> = ({
setMathPreview,
breadcrumbs,
setBreadcrumbs,
nonBlinkingCursor,
setNonBlinkingCursor,
darkModePdf,
setDarkModePdf,
zotero,
@@ -192,6 +199,8 @@ export const ProjectSettingsProvider: FC<React.PropsWithChildren> = ({
setMathPreview,
breadcrumbs,
setBreadcrumbs,
nonBlinkingCursor,
setNonBlinkingCursor,
darkModePdf,
setDarkModePdf,
zotero,
@@ -24,6 +24,7 @@ export default function useUserWideSettings() {
pdfViewer,
mathPreview,
breadcrumbs,
nonBlinkingCursor,
darkModePdf,
zotero,
mendeley,
@@ -129,6 +130,13 @@ export default function useUserWideSettings() {
[saveUserSettings]
)
const setNonBlinkingCursor = useCallback(
(nonBlinkingCursor: UserSettings['nonBlinkingCursor']) => {
saveUserSettings('nonBlinkingCursor', nonBlinkingCursor)
},
[saveUserSettings]
)
const setDarkModePdf = useCallback(
(darkModePdf: UserSettings['darkModePdf']) => {
saveUserSettings('darkModePdf', darkModePdf)
@@ -188,6 +196,8 @@ export default function useUserWideSettings() {
setMathPreview,
breadcrumbs,
setBreadcrumbs,
nonBlinkingCursor,
setNonBlinkingCursor,
darkModePdf,
setDarkModePdf,
zotero,
@@ -0,0 +1,19 @@
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import ToggleSetting from '../toggle-setting'
import { useTranslation } from 'react-i18next'
export default function NonBlinkingCursorSetting() {
const { nonBlinkingCursor, setNonBlinkingCursor } =
useProjectSettingsContext()
const { t } = useTranslation()
return (
<ToggleSetting
id="non-blinking-cursor"
label={t('non_blinking_cursor')}
description={t('reduces_visual_distraction_by_keeping_the_cursor_solid')}
checked={nonBlinkingCursor}
onChange={setNonBlinkingCursor}
/>
)
}
@@ -11,6 +11,7 @@ import SpellCheckSetting from '@/features/settings/components/editor-settings/sp
import DictionarySetting from '@/features/settings/components/editor-settings/dictionary-setting'
import { useTranslation } from 'react-i18next'
import BreadcrumbsSetting from '@/features/settings/components/editor-settings/breadcrumbs-setting'
import NonBlinkingCursorSetting from '@/features/settings/components/editor-settings/non-blinking-cursor-setting'
import MathPreviewSetting from '@/features/settings/components/editor-settings/math-preview-setting'
import RootDocumentSetting from '@/features/settings/components/compiler-settings/root-document-setting'
import CompilerSetting from '@/features/settings/components/compiler-settings/compiler-setting'
@@ -111,6 +112,10 @@ export const SettingsModalProvider: FC<React.PropsWithChildren> = ({
key: 'autoPairDelimiters',
component: <AutoCloseBracketsSetting />,
},
{
key: 'nonBlinkingCursor',
component: <NonBlinkingCursorSetting />,
},
{
key: 'syntaxValidation',
component: <CodeCheckSetting />,
@@ -9,6 +9,8 @@ import browser from './browser'
* copied to make use of a custom version of rectangleMarkerForRange which calls
* fullHeightCoordsAtPos when in Source mode, extending the top and bottom
* of the coords to cover the full line height.
*
* Whenever possible, we should avoid making changes to this extension.
*/
export const drawSelection = () => {
return [cursorLayer, selectionLayer, Prec.highest(hideNativeSelection)]
@@ -35,6 +35,7 @@ import importOverleafModules from '../../../../macros/import-overleaf-module.mac
import { emptyLineFiller } from './empty-line-filler'
import { goToLinePanel } from './go-to-line'
import { drawSelection } from './draw-selection'
import { nonBlinkingCursor } from './non-blinking-cursor'
import { sourceOnly, visual } from './visual/visual'
import { inlineBackground } from './inline-background'
import { indentationMarkers } from './indentation-markers'
@@ -75,6 +76,7 @@ export const createExtensions = (options: Record<string, any>): Extension[] => [
closedText: '▸',
}),
drawSelection(),
nonBlinkingCursor(),
// A built-in facet that is set to true to allow multiple selections.
// This makes the editor more like a code editor than Google Docs or Microsoft Word,
// which only have single selections.
@@ -0,0 +1,36 @@
import { StateEffect, StateField, TransactionSpec } from '@codemirror/state'
import { EditorView } from '@codemirror/view'
const nonBlinkingCursorEffect = StateEffect.define<boolean>()
const nonBlinkingCursorTheme = EditorView.baseTheme({
'&.cm-non-blinking-cursor .cm-cursorLayer': {
animationName: 'none !important',
},
})
const nonBlinkingCursorField = StateField.define<boolean>({
create: () => false,
update: (value, tr) => {
for (const effect of tr.effects) {
if (effect.is(nonBlinkingCursorEffect)) {
value = effect.value
}
}
return value
},
provide(field) {
return [
EditorView.editorAttributes.from(field, field => ({
class: field ? 'cm-non-blinking-cursor' : '',
})),
nonBlinkingCursorTheme,
]
},
})
export const setNonBlinkingCursor = (enabled: boolean): TransactionSpec => ({
effects: nonBlinkingCursorEffect.of(enabled),
})
export const nonBlinkingCursor = () => nonBlinkingCursorField
@@ -44,6 +44,7 @@ import { useMetadataContext } from '@/features/ide-react/context/metadata-contex
import { useUserContext } from '@/shared/context/user-context'
import { useReferencesContext } from '@/features/ide-react/context/references-context'
import { setMathPreview } from '@/features/source-editor/extensions/math-preview'
import { setNonBlinkingCursor } from '@/features/source-editor/extensions/non-blinking-cursor'
import { useRangesContext } from '@/features/review-panel/context/ranges-context'
import { updateRanges } from '@/features/source-editor/extensions/ranges'
import { useThreadsContext } from '@/features/review-panel/context/threads-context'
@@ -88,6 +89,7 @@ function useCodeMirrorScope(view: EditorView) {
mode,
syntaxValidation,
mathPreview,
nonBlinkingCursor,
referencesSearchMode,
} = userSettings
const activeOverallTheme = useActiveOverallTheme()
@@ -158,6 +160,7 @@ function useCodeMirrorScope(view: EditorView) {
mode,
syntaxValidation,
mathPreview,
nonBlinkingCursor,
referencesSearchMode,
})
@@ -467,6 +470,13 @@ function useCodeMirrorScope(view: EditorView) {
})
}, [view, mathPreview])
useEffect(() => {
settingsRef.current.nonBlinkingCursor = nonBlinkingCursor
window.setTimeout(() => {
view.dispatch(setNonBlinkingCursor(nonBlinkingCursor))
})
}, [view, nonBlinkingCursor])
useEffect(() => {
settingsRef.current.referencesSearchMode = referencesSearchMode
}, [referencesSearchMode])
@@ -31,6 +31,7 @@ export const defaultSettings: UserSettings = {
mathPreview: true,
referencesSearchMode: 'advanced',
breadcrumbs: true,
nonBlinkingCursor: false,
darkModePdf: false,
zotero: {
enabled: true,
+2
View File
@@ -1594,6 +1594,7 @@
"no_thanks_cancel_now": "No thanks, I still want to cancel",
"no_token": "No token",
"no_update_email": "No, update email",
"non_blinking_cursor": "Non-blinking cursor",
"non_deletable_entity": "The specified entity may not be deleted",
"normal": "Normal",
"normally_x_price_per_month": "Normally __price__ per month",
@@ -1952,6 +1953,7 @@
"redirecting": "Redirecting",
"redo": "Redo",
"reduce_costs_group_licenses": "You can cut down on paperwork and reduce costs with our discounted group licenses.",
"reduces_visual_distraction_by_keeping_the_cursor_solid": "Reduces visual distraction by keeping the cursor solid",
"refer_to_doc_link": "If needed, you can refer to the <a href=\"__link__\">Overleaf API documentation</a>.",
"reference_error_relink_hint": "If this error persists, try re-linking your account here:",
"reference_manager_linked": "Reference manager linked",
@@ -0,0 +1,51 @@
import { screen, render } from '@testing-library/react'
import { expect } from 'chai'
import fetchMock from 'fetch-mock'
import { SettingsModalProvider } from '@/features/settings/context/settings-modal-context'
import { EditorProviders } from '../../../helpers/editor-providers'
import NonBlinkingCursorSetting from '@/features/settings/components/editor-settings/non-blinking-cursor-setting'
describe('<NonBlinkingCursorSetting />', function () {
afterEach(function () {
fetchMock.removeRoutes().clearHistory()
})
it('can toggle', async function () {
render(
<EditorProviders>
<SettingsModalProvider>
<NonBlinkingCursorSetting />
</SettingsModalProvider>
</EditorProviders>
)
const saveSettingsMock = fetchMock.post(
`express:/user/settings`,
{
status: 200,
},
{ delay: 0 }
)
const toggle = screen.getByLabelText('Non-blinking cursor')
const startingCheckedValue = (toggle as HTMLInputElement).checked
// Toggle the checkbox
toggle.click()
expect((toggle as HTMLInputElement).checked).to.equal(!startingCheckedValue)
expect(
saveSettingsMock.callHistory.called(`/user/settings`, {
body: { nonBlinkingCursor: !startingCheckedValue },
})
).to.be.true
// Toggle back to original value
toggle.click()
expect((toggle as HTMLInputElement).checked).to.equal(startingCheckedValue)
expect(
saveSettingsMock.callHistory.called(`/user/settings`, {
body: { nonBlinkingCursor: startingCheckedValue },
})
).to.be.true
})
})
+1
View File
@@ -27,6 +27,7 @@ export type UserSettings = {
mathPreview: boolean
referencesSearchMode: 'advanced' | 'simple'
breadcrumbs: boolean
nonBlinkingCursor: boolean
darkModePdf: boolean
zotero: RefProviderSettings
mendeley: RefProviderSettings