mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-20 07:10:02 +00:00
basic indentation support in code blocks (#7494)
Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
parent
0e24698cc5
commit
40a3ca5ba9
152
plugins/text-editor-resources/src/components/extension/indent.ts
Normal file
152
plugins/text-editor-resources/src/components/extension/indent.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2024 Hardcore Engineering Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License. You may
|
||||||
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
//
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import { Extension } from '@tiptap/core'
|
||||||
|
import { type Node } from '@tiptap/pm/model'
|
||||||
|
import { type EditorState, TextSelection, type Transaction } from '@tiptap/pm/state'
|
||||||
|
|
||||||
|
export interface IndendOptions {
|
||||||
|
indentUnit: string
|
||||||
|
allowedNodes: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const indentExtensionOptions: IndendOptions = {
|
||||||
|
indentUnit: ' ',
|
||||||
|
allowedNodes: ['mermaid', 'codeBlock']
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IndentExtension = Extension.create<IndendOptions>({
|
||||||
|
name: 'huly-indent',
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
Tab: ({ editor }) => {
|
||||||
|
if (!editor.isEditable) return false
|
||||||
|
return this.editor.commands.command(({ state, dispatch }) => {
|
||||||
|
return dispatch?.(adjustIndent(state, 1, this.options))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
'Shift-Tab': ({ editor }) => {
|
||||||
|
if (!editor.isEditable) return false
|
||||||
|
return this.editor.commands.command(({ state, dispatch }) => {
|
||||||
|
return dispatch?.(adjustIndent(state, -1, this.options))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function adjustIndent (state: EditorState, direction: -1 | 1, options: IndendOptions): Transaction | undefined {
|
||||||
|
const { selection } = state
|
||||||
|
if (selection instanceof TextSelection) {
|
||||||
|
return adjustSelectionIndent(state, selection, direction, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function adjustSelectionIndent (
|
||||||
|
state: EditorState,
|
||||||
|
selection: TextSelection,
|
||||||
|
direction: -1 | 1,
|
||||||
|
options: IndendOptions
|
||||||
|
): Transaction | undefined {
|
||||||
|
const ranges = getLinesInSelection(state, selection).filter((range) =>
|
||||||
|
options.allowedNodes.some((n) => n === range.node.type.name)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (ranges.length === 0) return
|
||||||
|
|
||||||
|
const { indentUnit } = options
|
||||||
|
|
||||||
|
const indentLevelOffset = (pos: number, direction: -1 | 1): number => {
|
||||||
|
const unitSize = indentUnit.length
|
||||||
|
const levelAdjustment = direction === -1 && pos % unitSize !== 0 ? 0 : direction
|
||||||
|
const indentPos = Math.floor((pos + levelAdjustment * unitSize) / unitSize) * unitSize
|
||||||
|
return indentPos - pos
|
||||||
|
}
|
||||||
|
|
||||||
|
const tr = state.tr
|
||||||
|
|
||||||
|
if (ranges.length === 1) {
|
||||||
|
const range = ranges[0]
|
||||||
|
const withinIndent = selection.from >= range.from && selection.to <= range.from + range.indent && range.indent > 0
|
||||||
|
if (!withinIndent && direction > 0) {
|
||||||
|
const indentOffset = indentLevelOffset(selection.from - range.from, direction)
|
||||||
|
tr.insertText(indentUnit.slice(0, indentOffset), selection.from, selection.to)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let insertionOffset = 0
|
||||||
|
for (const range of ranges) {
|
||||||
|
if (direction > 0 ? range.text === '' : range.indent === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const indentOffset = indentLevelOffset(range.indent, direction)
|
||||||
|
const from = range.from + insertionOffset
|
||||||
|
if (indentOffset > 0) {
|
||||||
|
tr.insertText(indentUnit.slice(0, indentOffset), from)
|
||||||
|
} else {
|
||||||
|
tr.insertText('', from, from - indentOffset)
|
||||||
|
}
|
||||||
|
insertionOffset += indentOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.setSelection(selection.map(tr.doc, tr.mapping))
|
||||||
|
|
||||||
|
return tr
|
||||||
|
}
|
||||||
|
|
||||||
|
function countLeadingSpace (str: string): number {
|
||||||
|
const match = str.match(/^(\s*)/)
|
||||||
|
return match !== null ? match[0].length : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LineRange {
|
||||||
|
node: Node
|
||||||
|
text: string
|
||||||
|
from: number
|
||||||
|
indent: number
|
||||||
|
to: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLinesInSelection (state: EditorState, selection: TextSelection): LineRange[] {
|
||||||
|
const { from, to } = selection // Selection start and end positions
|
||||||
|
const ranges: LineRange[] = []
|
||||||
|
|
||||||
|
state.doc.nodesBetween(from, to, (node: Node, pos: number) => {
|
||||||
|
if (!node.isTextblock) return
|
||||||
|
|
||||||
|
let currentPos = pos + 1
|
||||||
|
const lines = node.textContent.split('\n')
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const lineStart = currentPos
|
||||||
|
const lineEnd = currentPos + line.length
|
||||||
|
|
||||||
|
if (lineStart <= to && lineEnd >= from) {
|
||||||
|
ranges.push({
|
||||||
|
node,
|
||||||
|
from: lineStart,
|
||||||
|
indent: countLeadingSpace(line),
|
||||||
|
to: lineEnd,
|
||||||
|
text: line
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPos = lineEnd + 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return ranges
|
||||||
|
}
|
@ -37,6 +37,7 @@ import { Table, TableCell, TableRow } from '../components/extension/table'
|
|||||||
import { DefaultKit, type DefaultKitOptions } from './default-kit'
|
import { DefaultKit, type DefaultKitOptions } from './default-kit'
|
||||||
import { MermaidExtension, type MermaidOptions, mermaidOptions } from '../components/extension/mermaid'
|
import { MermaidExtension, type MermaidOptions, mermaidOptions } from '../components/extension/mermaid'
|
||||||
import { DrawingBoardExtension, type DrawingBoardOptions } from '../components/extension/drawingBoard'
|
import { DrawingBoardExtension, type DrawingBoardOptions } from '../components/extension/drawingBoard'
|
||||||
|
import { type IndendOptions, IndentExtension, indentExtensionOptions } from '../components/extension/indent'
|
||||||
|
|
||||||
export interface EditorKitOptions extends DefaultKitOptions {
|
export interface EditorKitOptions extends DefaultKitOptions {
|
||||||
history?: false
|
history?: false
|
||||||
@ -53,6 +54,7 @@ export interface EditorKitOptions extends DefaultKitOptions {
|
|||||||
| false
|
| false
|
||||||
drawingBoard?: DrawingBoardOptions | false
|
drawingBoard?: DrawingBoardOptions | false
|
||||||
mermaid?: MermaidOptions | false
|
mermaid?: MermaidOptions | false
|
||||||
|
indent?: IndendOptions | false
|
||||||
mode?: 'full' | 'compact'
|
mode?: 'full' | 'compact'
|
||||||
note?: NoteOptions | false
|
note?: NoteOptions | false
|
||||||
submit?: SubmitOptions | false
|
submit?: SubmitOptions | false
|
||||||
@ -260,6 +262,13 @@ async function buildEditorKit (): Promise<Extension<EditorKitOptions, any>> {
|
|||||||
staticKitExtensions.push([850, MermaidExtension.configure(this.options.mermaid ?? mermaidOptions)])
|
staticKitExtensions.push([850, MermaidExtension.configure(this.options.mermaid ?? mermaidOptions)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.indent !== false) {
|
||||||
|
staticKitExtensions.push([
|
||||||
|
860,
|
||||||
|
IndentExtension.configure(this.options.indent ?? indentExtensionOptions)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
if (this.options.toolbar !== false) {
|
if (this.options.toolbar !== false) {
|
||||||
staticKitExtensions.push([
|
staticKitExtensions.push([
|
||||||
900,
|
900,
|
||||||
|
Loading…
Reference in New Issue
Block a user