mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 03:06:31 -05:00
Add table formatting on paste of detected table (#957)
This commit is contained in:
parent
107f0f6fa3
commit
0b4a0afa16
14 changed files with 375 additions and 27 deletions
|
@ -68,8 +68,9 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
|||
- Easier possibility to share notes via native share-buttons on supported devices.
|
||||
- Surround selected text with a link via shortcut (ctrl+k or cmd+k).
|
||||
- A sidebar for menu options
|
||||
- Improved security by wrapping the markdown rendering into an iframe
|
||||
- The intro page content can be changed by editing `public/intro.md`
|
||||
- Improved security by wrapping the markdown rendering into an iframe.
|
||||
- The intro page content can be changed by editing `public/intro.md`.
|
||||
- When pasting tables (e.g. from LibreOffice Calc or MS Excel) they get reformatted to markdown tables.
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
@ -54,7 +54,8 @@ describe('File upload', () => {
|
|||
.then((image: string) => {
|
||||
const pasteEvent = {
|
||||
clipboardData: {
|
||||
files: [Cypress.Blob.base64StringToBlob(image, 'image/png')]
|
||||
files: [Cypress.Blob.base64StringToBlob(image, 'image/png')],
|
||||
getData: (_: string) => ''
|
||||
}
|
||||
}
|
||||
cy.get('.CodeMirror-scroll')
|
||||
|
|
|
@ -421,6 +421,9 @@
|
|||
},
|
||||
"ligatures": {
|
||||
"label": "Show ligatures"
|
||||
},
|
||||
"smartPaste": {
|
||||
"label": "Auto format pasted content"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -41,6 +41,7 @@ import { defaultKeyMap } from './key-map'
|
|||
import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar'
|
||||
import { ToolBar } from './tool-bar/tool-bar'
|
||||
import { handleUpload } from './upload-handler'
|
||||
import { handleFilePaste, handleTablePaste, PasteEvent } from './tool-bar/utils/pasteHandlers'
|
||||
|
||||
export interface EditorPaneProps {
|
||||
onContentChange: (content: string) => void
|
||||
|
@ -62,23 +63,6 @@ const onChange = (editor: Editor) => {
|
|||
}
|
||||
}
|
||||
|
||||
interface PasteEvent {
|
||||
clipboardData: {
|
||||
files: FileList
|
||||
},
|
||||
preventDefault: () => void
|
||||
}
|
||||
|
||||
const onPaste = (pasteEditor: Editor, event: PasteEvent) => {
|
||||
if (event && event.clipboardData && event.clipboardData.files && event.clipboardData.files.length > 0) {
|
||||
event.preventDefault()
|
||||
const files: FileList = event.clipboardData.files
|
||||
if (files && files.length >= 1) {
|
||||
handleUpload(files[0], pasteEditor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface DropEvent {
|
||||
pageX: number,
|
||||
pageY: number,
|
||||
|
@ -92,6 +76,7 @@ interface DropEvent {
|
|||
export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentChange, content, scrollState, onScroll, onMakeScrollSource }) => {
|
||||
const { t } = useTranslation()
|
||||
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
||||
const smartPasteEnabled = useSelector((state: ApplicationState) => state.editorConfig.smartPaste)
|
||||
const [showMaxLengthWarning, setShowMaxLengthWarning] = useState(false)
|
||||
const maxLengthWarningAlreadyShown = useRef(false)
|
||||
const [editor, setEditor] = useState<Editor>()
|
||||
|
@ -103,6 +88,19 @@ export const EditorPane: React.FC<EditorPaneProps & ScrollProps> = ({ onContentC
|
|||
const [editorScroll, setEditorScroll] = useState<ScrollInfo>()
|
||||
const onEditorScroll = useCallback((editor: Editor, data: ScrollInfo) => setEditorScroll(data), [])
|
||||
|
||||
const onPaste = useCallback((pasteEditor: Editor, event: PasteEvent) => {
|
||||
if (!event || !event.clipboardData) {
|
||||
return
|
||||
}
|
||||
if (smartPasteEnabled) {
|
||||
const tableInserted = handleTablePaste(event, pasteEditor)
|
||||
if (tableInserted) {
|
||||
return
|
||||
}
|
||||
}
|
||||
handleFilePaste(event, pasteEditor)
|
||||
}, [smartPasteEnabled])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor || !onScroll || !editorScroll) {
|
||||
return
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { convertClipboardTableToMarkdown, isTable } from './table-extractor'
|
||||
|
||||
describe('isTable detection: ', () => {
|
||||
|
||||
it('empty string is no table', () => {
|
||||
expect(isTable(''))
|
||||
.toBe(false)
|
||||
})
|
||||
|
||||
it('single line is no table', () => {
|
||||
const input = 'some none table'
|
||||
expect(isTable(input))
|
||||
.toBe(false)
|
||||
})
|
||||
|
||||
it('multiple lines without tabs are no table', () => {
|
||||
const input = 'some none table\nanother line'
|
||||
expect(isTable(input))
|
||||
.toBe(false)
|
||||
})
|
||||
|
||||
it('code blocks are no table', () => {
|
||||
const input = '```python\ndef a:\n\tprint("a")\n\tprint("b")```'
|
||||
expect(isTable(input))
|
||||
.toBe(false)
|
||||
})
|
||||
|
||||
it('tab-indented text is no table', () => {
|
||||
const input = '\tsome tab indented text\n\tabc\n\tdef'
|
||||
expect(isTable(input))
|
||||
.toBe(false)
|
||||
})
|
||||
|
||||
it('not equal number of tabs is no table', () => {
|
||||
const input = '1 ...\n2\tabc\n3\td\te\tf\n4\t16'
|
||||
expect(isTable(input))
|
||||
.toBe(false)
|
||||
})
|
||||
|
||||
it('table without newline at end is valid', () => {
|
||||
const input = '1\t1\n2\t4\n3\t9\n4\t16\n5\t25'
|
||||
expect(isTable(input))
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
it('table with newline at end is valid', () => {
|
||||
const input = '1\t1\n2\t4\n3\t9\n4\t16\n5\t25\n'
|
||||
expect(isTable(input))
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
it('table with some first cells missing is valid', () => {
|
||||
const input = '1\t1\n\t0\n\t0\n4\t16\n5\t25\n'
|
||||
expect(isTable(input))
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
it('table with some last cells missing is valid', () => {
|
||||
const input = '1\t1\n2\t\n3\t\n4\t16\n'
|
||||
expect(isTable(input))
|
||||
.toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Conversion from clipboard table to markdown format', () => {
|
||||
it('normal table without newline at end converts right', () => {
|
||||
const input = '1\t1\ta\n2\t4\tb\n3\t9\tc\n4\t16\td'
|
||||
expect(convertClipboardTableToMarkdown(input))
|
||||
.toEqual('| #1 | #2 | #3 |\n| -- | -- | -- |\n| 1 | 1 | a |\n| 2 | 4 | b |\n| 3 | 9 | c |\n| 4 | 16 | d |')
|
||||
})
|
||||
|
||||
it('normal table with newline at end converts right', () => {
|
||||
const input = '1\t1\n2\t4\n3\t9\n4\t16\n'
|
||||
expect(convertClipboardTableToMarkdown(input))
|
||||
.toEqual('| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| 2 | 4 |\n| 3 | 9 |\n| 4 | 16 |')
|
||||
})
|
||||
|
||||
it('table with some first cells missing converts right', () => {
|
||||
const input = '1\t1\n\t0\n\t0\n4\t16\n'
|
||||
expect(convertClipboardTableToMarkdown(input))
|
||||
.toEqual('| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| | 0 |\n| | 0 |\n| 4 | 16 |')
|
||||
})
|
||||
|
||||
it('table with some last cells missing converts right', () => {
|
||||
const input = '1\t1\n2\t\n3\t\n4\t16\n'
|
||||
expect(convertClipboardTableToMarkdown(input))
|
||||
.toEqual('| #1 | #2 |\n| -- | -- |\n| 1 | 1 |\n| 2 | |\n| 3 | |\n| 4 | 16 |')
|
||||
})
|
||||
|
||||
it('empty input results in empty output', () => {
|
||||
expect(convertClipboardTableToMarkdown('')).toEqual('')
|
||||
})
|
||||
})
|
58
src/components/editor-page/editor-pane/table-extractor.ts
Normal file
58
src/components/editor-page/editor-pane/table-extractor.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { createNumberRangeArray } from '../../common/number-range/number-range'
|
||||
|
||||
export const isTable = (text: string): boolean => {
|
||||
// Tables must consist of multiple rows and columns
|
||||
if (!text.includes('\n') || !text.includes('\t')) {
|
||||
return false
|
||||
}
|
||||
// Code within code blocks should not be parsed as a table
|
||||
if (text.startsWith('```')) {
|
||||
return false
|
||||
}
|
||||
|
||||
const lines = text.split(/\r?\n/)
|
||||
.filter(line => line.trim() !== '')
|
||||
|
||||
// Tab-indented text should not be matched as a table
|
||||
if (lines.every(line => line.startsWith('\t'))) {
|
||||
return false
|
||||
}
|
||||
// Every line should have the same amount of tabs (table columns)
|
||||
const tabsPerLines = lines.map(line => line.match(/\t/g)?.length ?? 0)
|
||||
return tabsPerLines.every(line => line === tabsPerLines[0])
|
||||
}
|
||||
|
||||
export const convertClipboardTableToMarkdown = (pasteData: string): string => {
|
||||
if (pasteData.trim() === '') {
|
||||
return ''
|
||||
}
|
||||
const tableRows = pasteData.split(/\r?\n/)
|
||||
.filter(row => row.trim() !== '')
|
||||
const tableCells = tableRows.reduce((cellsInRow, row, index) => {
|
||||
cellsInRow[index] = row.split('\t')
|
||||
return cellsInRow
|
||||
}, [] as string[][])
|
||||
const arrayMaxRows = createNumberRangeArray(tableCells.length)
|
||||
const arrayMaxColumns = createNumberRangeArray(Math.max(...tableCells.map(row => row.length)))
|
||||
|
||||
const headRow1 = arrayMaxColumns
|
||||
.map(col => `| #${ col + 1 } `)
|
||||
.join('') + '|'
|
||||
const headRow2 = arrayMaxColumns
|
||||
.map(col => `| -${ '-'.repeat((col + 1).toString().length) } `)
|
||||
.join('') + '|'
|
||||
const body = arrayMaxRows
|
||||
.map(row => {
|
||||
return arrayMaxColumns
|
||||
.map(col => '| ' + tableCells[row][col] + ' ')
|
||||
.join('') + '|'
|
||||
})
|
||||
.join('\n')
|
||||
return `${ headRow1 }\n${ headRow2 }\n${ body }`
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import React, { ChangeEvent, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { ApplicationState } from '../../../../../redux'
|
||||
import { setEditorSmartPaste } from '../../../../../redux/editor/methods'
|
||||
import { EditorPreferenceInput, EditorPreferenceInputType } from './editor-preference-input'
|
||||
|
||||
export const EditorPreferenceSmartPasteSelect: React.FC = () => {
|
||||
const smartPasteEnabled = useSelector((state: ApplicationState) => Boolean(state.editorConfig.smartPaste)
|
||||
.toString())
|
||||
const saveSmartPaste = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
|
||||
const smartPasteActivated: boolean = event.target.value === 'true'
|
||||
setEditorSmartPaste(smartPasteActivated)
|
||||
}, [])
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<EditorPreferenceInput
|
||||
onChange={ saveSmartPaste }
|
||||
value={ smartPasteEnabled }
|
||||
property={ 'smartPaste' }
|
||||
type={ EditorPreferenceInputType.BOOLEAN }
|
||||
>
|
||||
<option value='true'>{ t(`common.yes`) }</option>
|
||||
<option value='false'>{ t(`common.no`) }</option>
|
||||
</EditorPreferenceInput>
|
||||
)
|
||||
}
|
|
@ -19,6 +19,7 @@ import { EditorPreferenceLigaturesSelect } from './editor-preference-ligatures-s
|
|||
import { EditorPreferenceNumberProperty } from './editor-preference-number-property'
|
||||
import { EditorPreferenceProperty } from './editor-preference-property'
|
||||
import { EditorPreferenceSelectProperty } from './editor-preference-select-property'
|
||||
import { EditorPreferenceSmartPasteSelect } from './editor-preference-smart-paste-select'
|
||||
|
||||
export const EditorPreferences: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
@ -57,6 +58,9 @@ export const EditorPreferences: React.FC = () => {
|
|||
<ListGroup.Item>
|
||||
<EditorPreferenceLigaturesSelect/>
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item>
|
||||
<EditorPreferenceSmartPasteSelect/>
|
||||
</ListGroup.Item>
|
||||
<ListGroup.Item>
|
||||
<EditorPreferenceInput onChange={ () => alert('This feature is not yet implemented.') }
|
||||
property={ EditorPreferenceProperty.SPELL_CHECK }
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Mock } from 'ts-mockery'
|
||||
import { Editor } from 'codemirror'
|
||||
import { isCursorInCodefence } from './codefenceDetection'
|
||||
|
||||
Mock.configure('jest')
|
||||
|
||||
const mockEditor = (content: string, line: number) => {
|
||||
const contentLines = content.split('\n')
|
||||
return Mock.of<Editor>({
|
||||
getCursor() {
|
||||
return {
|
||||
line: line,
|
||||
ch: 0
|
||||
}
|
||||
},
|
||||
getDoc() {
|
||||
return {
|
||||
getLine(ln: number) {
|
||||
return contentLines[ln] ?? ''
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('Check whether cursor is in codefence', () => {
|
||||
|
||||
it('returns false for empty document', () => {
|
||||
const editor = mockEditor('', 0)
|
||||
expect(isCursorInCodefence(editor))
|
||||
.toBe(false)
|
||||
})
|
||||
|
||||
it('returns true with one open codefence directly above', () => {
|
||||
const editor = mockEditor('```\n', 1)
|
||||
expect(isCursorInCodefence(editor))
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
it('returns true with one open codefence and empty lines above', () => {
|
||||
const editor = mockEditor('```\n\n\n', 3)
|
||||
expect(isCursorInCodefence(editor))
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
it('returns false with one completed codefence above', () => {
|
||||
const editor = mockEditor('```\n\n```\n', 3)
|
||||
expect(isCursorInCodefence(editor))
|
||||
.toBe(false)
|
||||
})
|
||||
|
||||
it('returns true with one completed and one open codefence above', () => {
|
||||
const editor = mockEditor('```\n\n```\n\n```\n\n', 6)
|
||||
expect(isCursorInCodefence(editor))
|
||||
.toBe(true)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Editor } from 'codemirror'
|
||||
|
||||
export const isCursorInCodefence = (editor: Editor): boolean => {
|
||||
const currentLine = editor.getCursor().line
|
||||
let codefenceCount = 0
|
||||
for (let line = currentLine; line >= 0; --line) {
|
||||
const markdownContentLine = editor.getDoc().getLine(line)
|
||||
if (markdownContentLine.startsWith('```')) {
|
||||
codefenceCount++
|
||||
}
|
||||
}
|
||||
return codefenceCount % 2 === 1
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Editor } from 'codemirror'
|
||||
import { convertClipboardTableToMarkdown, isTable } from '../../table-extractor'
|
||||
import { handleUpload } from '../../upload-handler'
|
||||
import { insertAtCursor } from './toolbarButtonUtils'
|
||||
import { isCursorInCodefence } from './codefenceDetection'
|
||||
|
||||
type ClipboardDataFormats = 'text' | 'url' | 'text/plain' | 'text/uri-list' | 'text/html'
|
||||
|
||||
export interface PasteEvent {
|
||||
clipboardData: {
|
||||
files: FileList,
|
||||
getData: (format: ClipboardDataFormats) => string
|
||||
},
|
||||
preventDefault: () => void
|
||||
}
|
||||
|
||||
export const handleTablePaste = (event: PasteEvent, editor: Editor): boolean => {
|
||||
const pasteText = event.clipboardData.getData('text')
|
||||
if (!pasteText || isCursorInCodefence(editor) || !isTable(pasteText)) {
|
||||
return false
|
||||
}
|
||||
event.preventDefault()
|
||||
const markdownTable = convertClipboardTableToMarkdown(pasteText)
|
||||
insertAtCursor(editor, markdownTable)
|
||||
return true
|
||||
}
|
||||
|
||||
export const handleFilePaste = (event: PasteEvent, editor: Editor): boolean => {
|
||||
if (!event.clipboardData.files || event.clipboardData.files.length < 1) {
|
||||
return false
|
||||
}
|
||||
event.preventDefault()
|
||||
const files: FileList = event.clipboardData.files
|
||||
if (files && files.length >= 1) {
|
||||
handleUpload(files[0], editor)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -13,6 +13,7 @@ import {
|
|||
SetEditorConfigAction,
|
||||
SetEditorLigaturesAction,
|
||||
SetEditorPreferencesAction,
|
||||
SetEditorSmartPasteAction,
|
||||
SetEditorSyncScrollAction
|
||||
} from './types'
|
||||
|
||||
|
@ -48,7 +49,7 @@ export const setEditorMode = (editorMode: EditorMode): void => {
|
|||
export const setEditorSyncScroll = (syncScroll: boolean): void => {
|
||||
const action: SetEditorSyncScrollAction = {
|
||||
type: EditorConfigActionType.SET_SYNC_SCROLL,
|
||||
syncScroll: syncScroll
|
||||
syncScroll
|
||||
}
|
||||
store.dispatch(action)
|
||||
}
|
||||
|
@ -56,7 +57,15 @@ export const setEditorSyncScroll = (syncScroll: boolean): void => {
|
|||
export const setEditorLigatures = (ligatures: boolean): void => {
|
||||
const action: SetEditorLigaturesAction = {
|
||||
type: EditorConfigActionType.SET_LIGATURES,
|
||||
ligatures: ligatures
|
||||
ligatures
|
||||
}
|
||||
store.dispatch(action)
|
||||
}
|
||||
|
||||
export const setEditorSmartPaste = (smartPaste: boolean): void => {
|
||||
const action: SetEditorSmartPasteAction = {
|
||||
type: EditorConfigActionType.SET_SMART_PASTE,
|
||||
smartPaste
|
||||
}
|
||||
store.dispatch(action)
|
||||
}
|
||||
|
@ -64,7 +73,7 @@ export const setEditorLigatures = (ligatures: boolean): void => {
|
|||
export const mergeEditorPreferences = (preferences: EditorConfiguration): void => {
|
||||
const action: SetEditorPreferencesAction = {
|
||||
type: EditorConfigActionType.MERGE_EDITOR_PREFERENCES,
|
||||
preferences: preferences
|
||||
preferences
|
||||
}
|
||||
store.dispatch(action)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
SetEditorConfigAction,
|
||||
SetEditorLigaturesAction,
|
||||
SetEditorPreferencesAction,
|
||||
SetEditorSmartPasteAction,
|
||||
SetEditorSyncScrollAction
|
||||
} from './types'
|
||||
|
||||
|
@ -21,6 +22,7 @@ const initialState: EditorConfig = {
|
|||
editorMode: EditorMode.BOTH,
|
||||
ligatures: true,
|
||||
syncScroll: true,
|
||||
smartPaste: true,
|
||||
preferences: {
|
||||
theme: 'one-dark',
|
||||
keyMap: 'sublime',
|
||||
|
@ -57,6 +59,13 @@ export const EditorConfigReducer: Reducer<EditorConfig, EditorConfigActions> = (
|
|||
}
|
||||
saveToLocalStorage(newState)
|
||||
return newState
|
||||
case EditorConfigActionType.SET_SMART_PASTE:
|
||||
newState = {
|
||||
...state,
|
||||
smartPaste: (action as SetEditorSmartPasteAction).smartPaste
|
||||
}
|
||||
saveToLocalStorage(newState)
|
||||
return newState
|
||||
case EditorConfigActionType.MERGE_EDITOR_PREFERENCES:
|
||||
newState = {
|
||||
...state,
|
||||
|
|
|
@ -12,18 +12,20 @@ export enum EditorConfigActionType {
|
|||
SET_EDITOR_VIEW_MODE = 'editor/mode/set',
|
||||
SET_SYNC_SCROLL = 'editor/syncScroll/set',
|
||||
MERGE_EDITOR_PREFERENCES = 'editor/preferences/merge',
|
||||
SET_LIGATURES = 'editor/preferences/setLigatures'
|
||||
SET_LIGATURES = 'editor/preferences/setLigatures',
|
||||
SET_SMART_PASTE = 'editor/preferences/setSmartPaste'
|
||||
}
|
||||
|
||||
export interface EditorConfig {
|
||||
editorMode: EditorMode;
|
||||
syncScroll: boolean;
|
||||
editorMode: EditorMode
|
||||
syncScroll: boolean
|
||||
ligatures: boolean
|
||||
smartPaste: boolean
|
||||
preferences: EditorConfiguration
|
||||
}
|
||||
|
||||
export interface EditorConfigActions extends Action<EditorConfigActionType> {
|
||||
type: EditorConfigActionType;
|
||||
type: EditorConfigActionType
|
||||
}
|
||||
|
||||
export interface SetEditorSyncScrollAction extends EditorConfigActions {
|
||||
|
@ -34,6 +36,10 @@ export interface SetEditorLigaturesAction extends EditorConfigActions {
|
|||
ligatures: boolean
|
||||
}
|
||||
|
||||
export interface SetEditorSmartPasteAction extends EditorConfigActions {
|
||||
smartPaste: boolean
|
||||
}
|
||||
|
||||
export interface SetEditorConfigAction extends EditorConfigActions {
|
||||
mode: EditorMode
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue