mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-21 17:26:29 -05:00
feat(settings): add editor settings for indentation
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
3bc9708871
commit
395305dcb7
14 changed files with 852 additions and 15 deletions
|
@ -609,9 +609,19 @@
|
||||||
"label": "Line Wrapping",
|
"label": "Line Wrapping",
|
||||||
"help": "Breaks long lines so they're visible without scrolling."
|
"help": "Breaks long lines so they're visible without scrolling."
|
||||||
},
|
},
|
||||||
"spellCheck":{
|
"spellCheck": {
|
||||||
"label": "Spell checking",
|
"label": "Spell checking",
|
||||||
"help": "Enables browser spell checking."
|
"help": "Enables browser spell checking."
|
||||||
|
},
|
||||||
|
"indentWithTabs": {
|
||||||
|
"label": "Indentation character",
|
||||||
|
"help": "Sets which characters should be used for indentation",
|
||||||
|
"tabs": "Tabs",
|
||||||
|
"spaces": "Spaces"
|
||||||
|
},
|
||||||
|
"indentSpaces": {
|
||||||
|
"label": "Indentation size",
|
||||||
|
"help": "Sets the amount of spaces for indentation."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"global": {
|
"global": {
|
||||||
|
|
|
@ -45,6 +45,7 @@ import ReactCodeMirror from '@uiw/react-codemirror'
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react'
|
import React, { useCallback, useEffect, useMemo } from 'react'
|
||||||
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
|
import { useUiNotifications } from '../../notifications/ui-notification-boundary'
|
||||||
import { Lock as IconLock } from 'react-bootstrap-icons'
|
import { Lock as IconLock } from 'react-bootstrap-icons'
|
||||||
|
import { useCodeMirrorIndentationExtension } from './hooks/codemirror-extensions/use-code-mirror-indentation-extension'
|
||||||
|
|
||||||
export type EditorPaneProps = ScrollProps
|
export type EditorPaneProps = ScrollProps
|
||||||
|
|
||||||
|
@ -71,6 +72,7 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
|
||||||
const fileInsertExtension = useCodeMirrorFileInsertExtension()
|
const fileInsertExtension = useCodeMirrorFileInsertExtension()
|
||||||
const spellCheckExtension = useCodeMirrorSpellCheckExtension()
|
const spellCheckExtension = useCodeMirrorSpellCheckExtension()
|
||||||
const lineWrappingExtension = useCodeMirrorLineWrappingExtension()
|
const lineWrappingExtension = useCodeMirrorLineWrappingExtension()
|
||||||
|
const indentationExtension = useCodeMirrorIndentationExtension()
|
||||||
const cursorActivityExtension = useCursorActivityCallback()
|
const cursorActivityExtension = useCursorActivityCallback()
|
||||||
const autoCompletionExtension = useCodeMirrorAutocompletionsExtension()
|
const autoCompletionExtension = useCodeMirrorAutocompletionsExtension()
|
||||||
|
|
||||||
|
@ -107,7 +109,8 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
|
||||||
cursorActivityExtension,
|
cursorActivityExtension,
|
||||||
updateViewContextExtension,
|
updateViewContextExtension,
|
||||||
yjsExtension,
|
yjsExtension,
|
||||||
spellCheckExtension
|
spellCheckExtension,
|
||||||
|
indentationExtension
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
linterExtension,
|
linterExtension,
|
||||||
|
@ -120,7 +123,8 @@ export const EditorPane: React.FC<EditorPaneProps> = ({ scrollState, onScroll, o
|
||||||
updateViewContextExtension,
|
updateViewContextExtension,
|
||||||
yjsExtension,
|
yjsExtension,
|
||||||
spellCheckExtension,
|
spellCheckExtension,
|
||||||
lineWrappingExtension
|
lineWrappingExtension,
|
||||||
|
indentationExtension
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { useApplicationState } from '../../../../../hooks/common/use-application-state'
|
||||||
|
import type { Extension } from '@codemirror/state'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { indentUnit } from '@codemirror/language'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link Extension codemirror extension} that manages the indentation config.
|
||||||
|
*/
|
||||||
|
export const useCodeMirrorIndentationExtension = (): Extension => {
|
||||||
|
const indentWithTabs = useApplicationState((state) => state.editorConfig.indentWithTabs)
|
||||||
|
const indentSpaces = useApplicationState((state) => state.editorConfig.indentSpaces)
|
||||||
|
|
||||||
|
return useMemo(() => indentUnit.of(indentWithTabs ? '\t' : ' '.repeat(indentSpaces)), [indentWithTabs, indentSpaces])
|
||||||
|
}
|
|
@ -0,0 +1,587 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`EditorSettingsTabContent hides space settings indentWithTabs is true 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="list-group"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="list-group-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col-md-3"
|
||||||
|
>
|
||||||
|
settings.editor.ligatures.label
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="settings-ligatures"
|
||||||
|
title="common.on"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.on
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="settings-ligatures"
|
||||||
|
title="common.off"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.off
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-5"
|
||||||
|
>
|
||||||
|
settings.editor.ligatures.help
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="list-group-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col-md-3"
|
||||||
|
>
|
||||||
|
settings.editor.smartPaste.label
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="settings-smart-paste"
|
||||||
|
title="common.on"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.on
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="settings-smart-paste"
|
||||||
|
title="common.off"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.off
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-5"
|
||||||
|
>
|
||||||
|
settings.editor.smartPaste.help
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="list-group-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col-md-3"
|
||||||
|
>
|
||||||
|
settings.editor.syncScroll.label
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="settings-sync-scroll"
|
||||||
|
title="common.on"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.on
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="settings-sync-scroll"
|
||||||
|
title="common.off"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.off
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-5"
|
||||||
|
>
|
||||||
|
settings.editor.syncScroll.help
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="list-group-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col-md-3"
|
||||||
|
>
|
||||||
|
settings.editor.lineWrapping.label
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="settings-line-wrapping"
|
||||||
|
title="common.on"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.on
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="settings-line-wrapping"
|
||||||
|
title="common.off"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.off
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-5"
|
||||||
|
>
|
||||||
|
settings.editor.lineWrapping.help
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="list-group-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col-md-3"
|
||||||
|
>
|
||||||
|
settings.editor.spellCheck.label
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="settings-spell-check"
|
||||||
|
title="common.on"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.on
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="settings-spell-check"
|
||||||
|
title="common.off"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.off
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-5"
|
||||||
|
>
|
||||||
|
settings.editor.spellCheck.help
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="list-group-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col-md-3"
|
||||||
|
>
|
||||||
|
settings.editor.indentWithTabs.label
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="settings-indent-with-tabs"
|
||||||
|
title="settings.editor.indentWithTabs.tabs"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
settings.editor.indentWithTabs.tabs
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="settings-indent-with-tabs"
|
||||||
|
title="settings.editor.indentWithTabs.spaces"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
settings.editor.indentWithTabs.spaces
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-5"
|
||||||
|
>
|
||||||
|
settings.editor.indentWithTabs.help
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`EditorSettingsTabContent renders space settings when indentWithTabs is false 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="list-group"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="list-group-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col-md-3"
|
||||||
|
>
|
||||||
|
settings.editor.ligatures.label
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="settings-ligatures"
|
||||||
|
title="common.on"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.on
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="settings-ligatures"
|
||||||
|
title="common.off"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.off
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-5"
|
||||||
|
>
|
||||||
|
settings.editor.ligatures.help
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="list-group-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col-md-3"
|
||||||
|
>
|
||||||
|
settings.editor.smartPaste.label
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="settings-smart-paste"
|
||||||
|
title="common.on"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.on
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="settings-smart-paste"
|
||||||
|
title="common.off"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.off
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-5"
|
||||||
|
>
|
||||||
|
settings.editor.smartPaste.help
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="list-group-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col-md-3"
|
||||||
|
>
|
||||||
|
settings.editor.syncScroll.label
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="settings-sync-scroll"
|
||||||
|
title="common.on"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.on
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="settings-sync-scroll"
|
||||||
|
title="common.off"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.off
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-5"
|
||||||
|
>
|
||||||
|
settings.editor.syncScroll.help
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="list-group-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col-md-3"
|
||||||
|
>
|
||||||
|
settings.editor.lineWrapping.label
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="settings-line-wrapping"
|
||||||
|
title="common.on"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.on
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="settings-line-wrapping"
|
||||||
|
title="common.off"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.off
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-5"
|
||||||
|
>
|
||||||
|
settings.editor.lineWrapping.help
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="list-group-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col-md-3"
|
||||||
|
>
|
||||||
|
settings.editor.spellCheck.label
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="settings-spell-check"
|
||||||
|
title="common.on"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.on
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="settings-spell-check"
|
||||||
|
title="common.off"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
common.off
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-5"
|
||||||
|
>
|
||||||
|
settings.editor.spellCheck.help
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="list-group-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col-md-3"
|
||||||
|
>
|
||||||
|
settings.editor.indentWithTabs.label
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="settings-indent-with-tabs"
|
||||||
|
title="settings.editor.indentWithTabs.tabs"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
settings.editor.indentWithTabs.tabs
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="settings-indent-with-tabs"
|
||||||
|
title="settings.editor.indentWithTabs.spaces"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
settings.editor.indentWithTabs.spaces
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-5"
|
||||||
|
>
|
||||||
|
settings.editor.indentWithTabs.help
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="list-group-item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="col-md-3"
|
||||||
|
>
|
||||||
|
settings.editor.indentSpaces.label
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-4"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="w-auto form-control"
|
||||||
|
min="1"
|
||||||
|
type="number"
|
||||||
|
value="7"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="col-md-5"
|
||||||
|
>
|
||||||
|
settings.editor.indentSpaces.help
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { mockAppState } from '../../../../test-utils/mock-app-state'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { EditorSettingsTabContent } from './editor-settings-tab-content'
|
||||||
|
import { mockI18n } from '../../../../test-utils/mock-i18n'
|
||||||
|
|
||||||
|
jest.mock('../../../../hooks/common/use-application-state')
|
||||||
|
|
||||||
|
describe('EditorSettingsTabContent', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await mockI18n()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
jest.resetModules()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders space settings when indentWithTabs is false', () => {
|
||||||
|
mockAppState({
|
||||||
|
editorConfig: {
|
||||||
|
syncScroll: false,
|
||||||
|
spellCheck: false,
|
||||||
|
smartPaste: false,
|
||||||
|
lineWrapping: false,
|
||||||
|
ligatures: false,
|
||||||
|
indentWithTabs: false,
|
||||||
|
indentSpaces: 7
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const view = render(<EditorSettingsTabContent />)
|
||||||
|
expect(view.container).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hides space settings indentWithTabs is true', () => {
|
||||||
|
mockAppState({
|
||||||
|
editorConfig: {
|
||||||
|
syncScroll: false,
|
||||||
|
spellCheck: false,
|
||||||
|
smartPaste: false,
|
||||||
|
lineWrapping: false,
|
||||||
|
ligatures: false,
|
||||||
|
indentWithTabs: true,
|
||||||
|
indentSpaces: 7
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const view = render(<EditorSettingsTabContent />)
|
||||||
|
expect(view.container).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
|
@ -12,12 +12,16 @@ import React from 'react'
|
||||||
import { ListGroup } from 'react-bootstrap'
|
import { ListGroup } from 'react-bootstrap'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { SpellcheckSettingButtonGroup } from './spellcheck-setting-button-group'
|
import { SpellcheckSettingButtonGroup } from './spellcheck-setting-button-group'
|
||||||
|
import { IndentWithTabsSettingButtonGroup } from './indent-with-tabs-setting-button-group'
|
||||||
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
|
import { IndentSpacesSettingInput } from './indent-spaces-setting-input'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the editor specific settings.
|
* Shows the editor specific settings.
|
||||||
*/
|
*/
|
||||||
export const EditorSettingsTabContent: React.FC = () => {
|
export const EditorSettingsTabContent: React.FC = () => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
const useTabs = useApplicationState((state) => state.editorConfig.indentWithTabs)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
|
@ -36,6 +40,14 @@ export const EditorSettingsTabContent: React.FC = () => {
|
||||||
<SettingLine i18nKey={'editor.spellCheck'}>
|
<SettingLine i18nKey={'editor.spellCheck'}>
|
||||||
<SpellcheckSettingButtonGroup />
|
<SpellcheckSettingButtonGroup />
|
||||||
</SettingLine>
|
</SettingLine>
|
||||||
|
<SettingLine i18nKey={'editor.indentWithTabs'}>
|
||||||
|
<IndentWithTabsSettingButtonGroup />
|
||||||
|
</SettingLine>
|
||||||
|
{!useTabs && (
|
||||||
|
<SettingLine i18nKey={'editor.indentSpaces'}>
|
||||||
|
<IndentSpacesSettingInput />
|
||||||
|
</SettingLine>
|
||||||
|
)}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import React from 'react'
|
||||||
|
import { Form } from 'react-bootstrap'
|
||||||
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
|
import { setEditorIndentSpaces } from '../../../../redux/editor/methods'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input to change the number of spaces that are used for indentation in the editor.
|
||||||
|
*/
|
||||||
|
export const IndentSpacesSettingInput: React.FC = () => {
|
||||||
|
const spaces = useApplicationState((state) => state.editorConfig.indentSpaces)
|
||||||
|
|
||||||
|
const onChangeHandler = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const value = parseInt(event.target.value)
|
||||||
|
if (value > 0) {
|
||||||
|
setEditorIndentSpaces(value)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <Form.Control className={'w-auto'} type={'number'} min={1} value={spaces} onChange={onChangeHandler} />
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { useApplicationState } from '../../../../hooks/common/use-application-state'
|
||||||
|
import { setEditorIndentWithTabs } from '../../../../redux/editor/methods'
|
||||||
|
import { OnOffButtonGroup } from '../utils/on-off-button-group'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to change whether spellchecking is enabled or not in the editor.
|
||||||
|
*/
|
||||||
|
export const IndentWithTabsSettingButtonGroup: React.FC = () => {
|
||||||
|
const enabled = useApplicationState((state) => state.editorConfig.indentWithTabs)
|
||||||
|
return (
|
||||||
|
<OnOffButtonGroup
|
||||||
|
value={enabled}
|
||||||
|
onSelect={setEditorIndentWithTabs}
|
||||||
|
overrideButtonOnI18nKey={'settings.editor.indentWithTabs.tabs'}
|
||||||
|
overrideButtonOffI18nKey={'settings.editor.indentWithTabs.spaces'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,5 +1,33 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Settings On-Off Button Group accepts custom labels 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="btn-group"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-testid="onOffButtonGroupOn"
|
||||||
|
name="dark-mode"
|
||||||
|
title="test.custom-on"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
test.custom-on
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-testid="onOffButtonGroupOff"
|
||||||
|
name="dark-mode"
|
||||||
|
title="test.custom-off"
|
||||||
|
type="radio"
|
||||||
|
>
|
||||||
|
test.custom-off
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Settings On-Off Button Group can switch value 1`] = `
|
exports[`Settings On-Off Button Group can switch value 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -30,4 +30,16 @@ describe('Settings On-Off Button Group', () => {
|
||||||
})
|
})
|
||||||
expect(value).toBeFalsy()
|
expect(value).toBeFalsy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('accepts custom labels', () => {
|
||||||
|
const view = render(
|
||||||
|
<OnOffButtonGroup
|
||||||
|
value={true}
|
||||||
|
onSelect={() => {}}
|
||||||
|
overrideButtonOnI18nKey={'test.custom-on'}
|
||||||
|
overrideButtonOffI18nKey={'test.custom-off'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(view.container).toMatchSnapshot()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -16,6 +16,8 @@ enum OnOffState {
|
||||||
export interface OnOffButtonGroupProps {
|
export interface OnOffButtonGroupProps {
|
||||||
value: boolean
|
value: boolean
|
||||||
onSelect: (value: boolean) => void
|
onSelect: (value: boolean) => void
|
||||||
|
overrideButtonOnI18nKey?: string
|
||||||
|
overrideButtonOffI18nKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,8 +25,15 @@ export interface OnOffButtonGroupProps {
|
||||||
*
|
*
|
||||||
* @param onSelect callback that is executed if the on/off value has changed
|
* @param onSelect callback that is executed if the on/off value has changed
|
||||||
* @param value the current on/off value that should be visible
|
* @param value the current on/off value that should be visible
|
||||||
|
* @param overrideButtonOnI18nKey Set to override the i18n key for the on-button
|
||||||
|
* @param overrideButtonOffI18nKey Set to override the i18n key for the off-button
|
||||||
*/
|
*/
|
||||||
export const OnOffButtonGroup: React.FC<OnOffButtonGroupProps> = ({ onSelect, value }) => {
|
export const OnOffButtonGroup: React.FC<OnOffButtonGroupProps> = ({
|
||||||
|
onSelect,
|
||||||
|
value,
|
||||||
|
overrideButtonOffI18nKey,
|
||||||
|
overrideButtonOnI18nKey
|
||||||
|
}) => {
|
||||||
const buttonGroupValue = useMemo(() => (value ? OnOffState.ON : OnOffState.OFF), [value])
|
const buttonGroupValue = useMemo(() => (value ? OnOffState.ON : OnOffState.OFF), [value])
|
||||||
const onButtonSelect = useCallback(
|
const onButtonSelect = useCallback(
|
||||||
(value: OnOffState) => {
|
(value: OnOffState) => {
|
||||||
|
@ -39,16 +48,16 @@ export const OnOffButtonGroup: React.FC<OnOffButtonGroupProps> = ({ onSelect, va
|
||||||
onSelect={onButtonSelect}
|
onSelect={onButtonSelect}
|
||||||
selected={buttonGroupValue === OnOffState.ON}
|
selected={buttonGroupValue === OnOffState.ON}
|
||||||
value={OnOffState.ON}
|
value={OnOffState.ON}
|
||||||
i18nKeyTooltip={'common.on'}
|
i18nKeyTooltip={overrideButtonOnI18nKey ?? 'common.on'}
|
||||||
i18nKeyLabel={'common.on'}
|
i18nKeyLabel={overrideButtonOnI18nKey ?? 'common.on'}
|
||||||
{...testId('onOffButtonGroupOn')}
|
{...testId('onOffButtonGroupOn')}
|
||||||
/>
|
/>
|
||||||
<SettingsToggleButton
|
<SettingsToggleButton
|
||||||
onSelect={onButtonSelect}
|
onSelect={onButtonSelect}
|
||||||
selected={buttonGroupValue === OnOffState.OFF}
|
selected={buttonGroupValue === OnOffState.OFF}
|
||||||
value={OnOffState.OFF}
|
value={OnOffState.OFF}
|
||||||
i18nKeyTooltip={'common.off'}
|
i18nKeyTooltip={overrideButtonOffI18nKey ?? 'common.off'}
|
||||||
i18nKeyLabel={'common.off'}
|
i18nKeyLabel={overrideButtonOffI18nKey ?? 'common.off'}
|
||||||
{...testId('onOffButtonGroupOff')}
|
{...testId('onOffButtonGroupOff')}
|
||||||
/>
|
/>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
|
|
|
@ -10,7 +10,9 @@ import type {
|
||||||
SetEditorLineWrappingAction,
|
SetEditorLineWrappingAction,
|
||||||
SetEditorSmartPasteAction,
|
SetEditorSmartPasteAction,
|
||||||
SetEditorSyncScrollAction,
|
SetEditorSyncScrollAction,
|
||||||
SetEditorSpellCheckAction
|
SetEditorSpellCheckAction,
|
||||||
|
SetEditorIndentWithTabsAction,
|
||||||
|
SetEditorIndentSpacesAction
|
||||||
} from './types'
|
} from './types'
|
||||||
import { EditorConfigActionType } from './types'
|
import { EditorConfigActionType } from './types'
|
||||||
|
|
||||||
|
@ -54,6 +56,22 @@ export const setEditorSpellCheck = (spellCheck: boolean): void => {
|
||||||
store.dispatch(action)
|
store.dispatch(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const setEditorIndentWithTabs = (indentWithTabs: boolean): void => {
|
||||||
|
const action: SetEditorIndentWithTabsAction = {
|
||||||
|
type: EditorConfigActionType.SET_INDENT_WITH_TABS,
|
||||||
|
indentWithTabs
|
||||||
|
}
|
||||||
|
store.dispatch(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setEditorIndentSpaces = (indentSpaces: number): void => {
|
||||||
|
const action: SetEditorIndentSpacesAction = {
|
||||||
|
type: EditorConfigActionType.SET_INDENT_SPACES,
|
||||||
|
indentSpaces
|
||||||
|
}
|
||||||
|
store.dispatch(action)
|
||||||
|
}
|
||||||
|
|
||||||
export const loadFromLocalStorage = (): void => {
|
export const loadFromLocalStorage = (): void => {
|
||||||
const action: LoadFromLocalStorageAction = {
|
const action: LoadFromLocalStorageAction = {
|
||||||
type: EditorConfigActionType.LOAD_FROM_LOCAL_STORAGE
|
type: EditorConfigActionType.LOAD_FROM_LOCAL_STORAGE
|
||||||
|
|
|
@ -15,7 +15,9 @@ export const initialState: EditorConfig = {
|
||||||
syncScroll: true,
|
syncScroll: true,
|
||||||
smartPaste: true,
|
smartPaste: true,
|
||||||
spellCheck: true,
|
spellCheck: true,
|
||||||
lineWrapping: true
|
lineWrapping: true,
|
||||||
|
indentWithTabs: false,
|
||||||
|
indentSpaces: 2
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EditorConfigReducer: Reducer<EditorConfig, EditorConfigActions> = (
|
export const EditorConfigReducer: Reducer<EditorConfig, EditorConfigActions> = (
|
||||||
|
@ -61,6 +63,20 @@ export const EditorConfigReducer: Reducer<EditorConfig, EditorConfigActions> = (
|
||||||
}
|
}
|
||||||
saveToLocalStorage(newState)
|
saveToLocalStorage(newState)
|
||||||
return newState
|
return newState
|
||||||
|
case EditorConfigActionType.SET_INDENT_WITH_TABS:
|
||||||
|
newState = {
|
||||||
|
...state,
|
||||||
|
indentWithTabs: action.indentWithTabs
|
||||||
|
}
|
||||||
|
saveToLocalStorage(newState)
|
||||||
|
return newState
|
||||||
|
case EditorConfigActionType.SET_INDENT_SPACES:
|
||||||
|
newState = {
|
||||||
|
...state,
|
||||||
|
indentSpaces: action.indentSpaces
|
||||||
|
}
|
||||||
|
saveToLocalStorage(newState)
|
||||||
|
return newState
|
||||||
default:
|
default:
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
@ -72,13 +88,15 @@ export const loadFromLocalStorage = (): EditorConfig | undefined => {
|
||||||
if (!stored) {
|
if (!stored) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
const storedConfiguration = JSON.parse(stored) as Record<string, boolean>
|
const storedConfiguration = JSON.parse(stored) as Partial<EditorConfig>
|
||||||
return {
|
return {
|
||||||
ligatures: storedConfiguration?.ligatures === true ?? true,
|
ligatures: storedConfiguration?.ligatures === true ?? true,
|
||||||
syncScroll: storedConfiguration?.syncScroll === true ?? true,
|
syncScroll: storedConfiguration?.syncScroll === true ?? true,
|
||||||
smartPaste: storedConfiguration?.smartPaste === true ?? true,
|
smartPaste: storedConfiguration?.smartPaste === true ?? true,
|
||||||
spellCheck: storedConfiguration?.spellCheck === true ?? false,
|
spellCheck: storedConfiguration?.spellCheck === true ?? true,
|
||||||
lineWrapping: storedConfiguration?.lineWrapping === true ?? true
|
lineWrapping: storedConfiguration?.lineWrapping === true ?? true,
|
||||||
|
indentWithTabs: storedConfiguration?.indentWithTabs === true ?? false,
|
||||||
|
indentSpaces: storedConfiguration?.indentSpaces ?? 2
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return undefined
|
return undefined
|
||||||
|
|
|
@ -6,13 +6,14 @@
|
||||||
import type { Action } from 'redux'
|
import type { Action } from 'redux'
|
||||||
|
|
||||||
export enum EditorConfigActionType {
|
export enum EditorConfigActionType {
|
||||||
SET_EDITOR_VIEW_MODE = 'editor/view-mode/set',
|
|
||||||
SET_SYNC_SCROLL = 'editor/syncScroll/set',
|
SET_SYNC_SCROLL = 'editor/syncScroll/set',
|
||||||
LOAD_FROM_LOCAL_STORAGE = 'editor/preferences/load',
|
LOAD_FROM_LOCAL_STORAGE = 'editor/preferences/load',
|
||||||
SET_LIGATURES = 'editor/preferences/setLigatures',
|
SET_LIGATURES = 'editor/preferences/setLigatures',
|
||||||
SET_LINE_WRAPPING = 'editor/preferences/setLineWrapping',
|
SET_LINE_WRAPPING = 'editor/preferences/setLineWrapping',
|
||||||
SET_SMART_PASTE = 'editor/preferences/setSmartPaste',
|
SET_SMART_PASTE = 'editor/preferences/setSmartPaste',
|
||||||
SET_SPELL_CHECK = 'editor/preferences/setSpellCheck'
|
SET_SPELL_CHECK = 'editor/preferences/setSpellCheck',
|
||||||
|
SET_INDENT_WITH_TABS = 'editor/preferences/setIndentWithTabs',
|
||||||
|
SET_INDENT_SPACES = 'editor/preferences/setIndentSpaces'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorConfig {
|
export interface EditorConfig {
|
||||||
|
@ -21,6 +22,8 @@ export interface EditorConfig {
|
||||||
smartPaste: boolean
|
smartPaste: boolean
|
||||||
spellCheck: boolean
|
spellCheck: boolean
|
||||||
lineWrapping: boolean
|
lineWrapping: boolean
|
||||||
|
indentWithTabs: boolean
|
||||||
|
indentSpaces: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EditorConfigActions =
|
export type EditorConfigActions =
|
||||||
|
@ -29,6 +32,8 @@ export type EditorConfigActions =
|
||||||
| SetEditorSmartPasteAction
|
| SetEditorSmartPasteAction
|
||||||
| SetEditorLineWrappingAction
|
| SetEditorLineWrappingAction
|
||||||
| SetEditorSpellCheckAction
|
| SetEditorSpellCheckAction
|
||||||
|
| SetEditorIndentWithTabsAction
|
||||||
|
| SetEditorIndentSpacesAction
|
||||||
| LoadFromLocalStorageAction
|
| LoadFromLocalStorageAction
|
||||||
|
|
||||||
export interface LoadFromLocalStorageAction extends Action<EditorConfigActionType> {
|
export interface LoadFromLocalStorageAction extends Action<EditorConfigActionType> {
|
||||||
|
@ -59,3 +64,13 @@ export interface SetEditorSpellCheckAction extends Action<EditorConfigActionType
|
||||||
type: EditorConfigActionType.SET_SPELL_CHECK
|
type: EditorConfigActionType.SET_SPELL_CHECK
|
||||||
spellCheck: boolean
|
spellCheck: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SetEditorIndentWithTabsAction extends Action<EditorConfigActionType> {
|
||||||
|
type: EditorConfigActionType.SET_INDENT_WITH_TABS
|
||||||
|
indentWithTabs: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetEditorIndentSpacesAction extends Action<EditorConfigActionType> {
|
||||||
|
type: EditorConfigActionType.SET_INDENT_SPACES
|
||||||
|
indentSpaces: number
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue