mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-13 11:50:56 +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 { MermaidExtension, type MermaidOptions, mermaidOptions } from '../components/extension/mermaid'
|
||||
import { DrawingBoardExtension, type DrawingBoardOptions } from '../components/extension/drawingBoard'
|
||||
import { type IndendOptions, IndentExtension, indentExtensionOptions } from '../components/extension/indent'
|
||||
|
||||
export interface EditorKitOptions extends DefaultKitOptions {
|
||||
history?: false
|
||||
@ -53,6 +54,7 @@ export interface EditorKitOptions extends DefaultKitOptions {
|
||||
| false
|
||||
drawingBoard?: DrawingBoardOptions | false
|
||||
mermaid?: MermaidOptions | false
|
||||
indent?: IndendOptions | false
|
||||
mode?: 'full' | 'compact'
|
||||
note?: NoteOptions | false
|
||||
submit?: SubmitOptions | false
|
||||
@ -260,6 +262,13 @@ async function buildEditorKit (): Promise<Extension<EditorKitOptions, any>> {
|
||||
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) {
|
||||
staticKitExtensions.push([
|
||||
900,
|
||||
|
Loading…
Reference in New Issue
Block a user