import { union } from '@prosekit/core'
import type { EditorState } from '@prosekit/pm/state'
import { Decoration } from '@prosekit/pm/view'
import { describe, expect, it } from 'vitest'
import { defineTestExtension, setupTestFromExtension } from '../testing/index.ts'
import {
codeBlockPreviewDecorationsPluginKey,
defineCodeBlockPreviewPlugin,
HIDE_CODE_BLOCK_PREVIEW,
isCodeBlockPreviewHiddenDecoration,
} from './code-block-preview.ts'
function setupEditor() {
const extension = union(
defineTestExtension(),
defineCodeBlockPreviewPlugin(),
)
return setupTestFromExtension(extension)
}
describe('isCodeBlockPreviewHiddenDecoration', () => {
it('returns true for a decoration with the HIDE_CODE_BLOCK_PREVIEW spec', () => {
const deco = Decoration.node(0, 1, {}, HIDE_CODE_BLOCK_PREVIEW)
expect(isCodeBlockPreviewHiddenDecoration(deco)).toBe(true)
})
it('returns false for a decoration with a different spec', () => {
const deco = Decoration.node(0, 1, {}, { someOtherProp: true })
expect(isCodeBlockPreviewHiddenDecoration(deco)).toBe(false)
})
it('returns false for an inline decoration without spec', () => {
const deco = Decoration.inline(0, 1, { class: 'foo' })
expect(isCodeBlockPreviewHiddenDecoration(deco)).toBe(false)
})
})
describe('defineCodeBlockPreviewPlugin', () => {
it('adds hide-preview decoration when cursor is inside a code block', () => {
const { editor, n } = setupEditor()
const doc = n.doc(
n.codeBlock({ language: 'javascript' }, 'console.log("hello")'),
)
editor.set(doc)
expect(getCodeBlockPreviewDecorations(editor.view.state).length).toBe(1)
})
it('does not add decorations when cursor is outside a code block', () => {
const { editor, n } = setupEditor()
const doc = n.doc(
n.paragraph('hello'),
n.codeBlock({ language: 'javascript' }, 'console.log("hello")'),
)
editor.set(doc)
expect(getCodeBlockPreviewDecorations(editor.view.state).length).toBe(0)
})
it('adds decorations for code blocks overlapped by a non-empty selection', () => {
const { editor, n } = setupEditor()
// Selection spans from inside the code block into the following paragraph
const doc = n.doc(
n.codeBlock({ language: 'javascript' }, 'console.log("hello")'),
n.paragraph('world'),
)
editor.set(doc)
expect(getCodeBlockPreviewDecorations(editor.view.state).length).toBe(1)
})
it('only decorates the code block where the cursor is', () => {
const { editor, n } = setupEditor()
const doc = n.doc(
n.codeBlock({ language: 'javascript' }, 'first'),
n.codeBlock({ language: 'typescript' }, 'second'),
n.codeBlock({ language: 'python' }, 'third'),
)
editor.set(doc)
expect(getCodeBlockPreviewDecorations(editor.view.state).length).toBe(1)
})
it('returns empty decoration set when document has no code blocks', () => {
const { editor, n } = setupEditor()
const doc = n.doc(
n.paragraph('hello world'),
)
editor.set(doc)
expect(getCodeBlockPreviewDecorations(editor.view.state).length).toBe(0)
})
it('handles cursor at the start of a code block', () => {
const { editor, n } = setupEditor()
const doc = n.doc(
n.paragraph('before'),
n.codeBlock({ language: 'javascript' }, 'console.log("hello")'),
)
editor.set(doc)
expect(getCodeBlockPreviewDecorations(editor.view.state).length).toBe(1)
})
it('handles cursor at the end of a code block', () => {
const { editor, n } = setupEditor()
const doc = n.doc(
n.codeBlock({ language: 'javascript' }, 'console.log("hello")'),
n.paragraph('after'),
)
editor.set(doc)
expect(getCodeBlockPreviewDecorations(editor.view.state).length).toBe(1)
})
})
function getCodeBlockPreviewDecorations(state: EditorState): Decoration[] {
const pluginState = codeBlockPreviewDecorationsPluginKey.getState(state)
if (!pluginState) {
return []
}
const decorations = pluginState.find()
return decorations.filter(isCodeBlockPreviewHiddenDecoration)
}