-
Notifications
You must be signed in to change notification settings - Fork 327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Editing markdown documentation with buttons #12217
base: develop
Are you sure you want to change the base?
Changes from 25 commits
8d0baa0
488e3d3
f3b1b0e
b112950
534cc0f
4f24d18
4bcab86
d0f958f
2708b29
87075cc
a9d2eb8
ac854f7
ce970c4
c22b0b2
93e2f68
4c839e3
bab6f22
7dae675
e3b96d5
ae963da
86947e9
fb165cf
802a3f3
4f90de4
10c8156
29fad32
7334941
309d297
a0b38d3
237f7a8
c9bad7a
97d6940
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,17 +22,20 @@ const editing = computed(() => !readonly.value && focused.value) | |
|
||
const vueHost = new VueHostInstance() | ||
const editorRoot = useTemplateRef<ComponentInstance<typeof CodeMirrorRoot>>('editorRoot') | ||
const { editorView, readonly, putTextAt } = useCodeMirror(editorRoot, { | ||
content: () => content, | ||
extensions: [ | ||
minimalSetup, | ||
EditorView.lineWrapping, | ||
highlightStyle(useCssModule()), | ||
EditorView.clipboardInputFilter.of(transformPastedText), | ||
ensoMarkdown(), | ||
], | ||
vueHost: () => vueHost, | ||
}) | ||
const { editorView, readonly, putTextAt, toggleHeader, toggleQuote, toggleList } = useCodeMirror( | ||
editorRoot, | ||
{ | ||
content: () => content, | ||
extensions: [ | ||
minimalSetup, | ||
EditorView.lineWrapping, | ||
highlightStyle(useCssModule()), | ||
EditorView.clipboardInputFilter.of(transformPastedText), | ||
ensoMarkdown(), | ||
], | ||
vueHost: () => vueHost, | ||
}, | ||
) | ||
|
||
useLinkTitles(editorView, { readonly }) | ||
|
||
|
@@ -55,6 +58,9 @@ defineExpose({ | |
const pos = editorView.posAtCoords(coords, false) | ||
putTextAt(text, pos, pos) | ||
}, | ||
toggleHeader, | ||
toggleQuote, | ||
toggleList, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We should render the toolbar in this component. Instead of exposing the formatting functions, we can use slots to inject the functionality that isn't shared between graph editor and dashboard (fullscreen, insert-image). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
}) | ||
</script> | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,274 @@ | ||
import { ensoMarkdown } from '@/components/MarkdownEditor/markdown' | ||
import { | ||
HeaderLevel, | ||
toggleHeader, | ||
toggleList, | ||
toggleQuote, | ||
} from '@/util/codemirror/markdownEditing' | ||
import { setVueHost } from '@/util/codemirror/vueHostExt' | ||
import { EditorState } from '@codemirror/state' | ||
import { EditorView } from '@codemirror/view' | ||
import { expect, test } from 'vitest' | ||
|
||
/** | ||
* Setup editor with selection ranging from the first occurence of '|' in the `source` string to the last occurence of '|'. | ||
* If there is a single '|', it points at the cursor position. | ||
*/ | ||
const setupEditor = (source: string) => { | ||
const selectionStart = source.indexOf('|') | ||
const selectionEnd = source.lastIndexOf('|') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will be off by 1 because it is computed before the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch, though it does not matter match in these tests. We are not too worried about tricky edge-cases, as even in the worse case edits are easily reversible even by hand. |
||
const selection = { anchor: selectionStart, head: selectionEnd } | ||
const doc = source.replaceAll('|', '') | ||
const view = new EditorView({ | ||
state: EditorState.create({ | ||
doc, | ||
extensions: ensoMarkdown(), | ||
selection, | ||
}), | ||
}) | ||
const vueHost = { | ||
register: () => ({ | ||
unregister: () => {}, | ||
update: () => {}, | ||
}), | ||
teleportations: new Map(), | ||
} | ||
view.dispatch({ effects: setVueHost.of(vueHost) }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These tests shouldn't need a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed it |
||
return view | ||
} | ||
|
||
interface TestCase { | ||
desc?: string | ||
source: string | ||
expected: string | ||
} | ||
|
||
interface HeaderTestCase extends TestCase { | ||
headerLevel: HeaderLevel | ||
} | ||
|
||
const headerTestCases: HeaderTestCase[] = [ | ||
{ | ||
source: 'Some| text', | ||
headerLevel: 1, | ||
expected: '# Some text', | ||
}, | ||
{ | ||
source: '**Bold| text**', | ||
headerLevel: 1, | ||
expected: '# **Bold text**', | ||
}, | ||
{ | ||
source: '|Some| text', | ||
headerLevel: 1, | ||
expected: '# Some text', | ||
}, | ||
{ | ||
source: '|Some| text', | ||
headerLevel: 2, | ||
expected: '## Some text', | ||
}, | ||
{ | ||
source: '## |Some text', | ||
headerLevel: 1, | ||
expected: '# Some text', | ||
}, | ||
{ | ||
source: '### |Some text', | ||
headerLevel: 1, | ||
expected: '# Some text', | ||
}, | ||
{ | ||
source: 'Fir|st line\nSecond| line', | ||
headerLevel: 1, | ||
expected: '# First line\n# Second line', | ||
}, | ||
{ | ||
source: '# Fir|st line\n# Second| line', | ||
headerLevel: 1, | ||
expected: 'First line\nSecond line', | ||
}, | ||
{ | ||
source: '# |Header', | ||
headerLevel: 1, | ||
expected: 'Header', | ||
}, | ||
{ | ||
source: '# **Bo|ld**', | ||
headerLevel: 1, | ||
expected: '**Bold**', | ||
}, | ||
{ | ||
source: '# |Don’t touch this one\n## Touch this one\nMake this one h|eader', | ||
headerLevel: 1, | ||
expected: '# Don’t touch this one\n# Touch this one\n# Make this one header', | ||
}, | ||
{ | ||
source: '```\nSome code\nHead|er in code block\nMore code\n```', | ||
headerLevel: 1, | ||
expected: '```\nSome code\n# Header in code block\nMore code\n```', | ||
}, | ||
{ | ||
source: 'Some paragraph\n```\nSome code\n# Head|er in code block\nMore code\n```', | ||
headerLevel: 2, | ||
expected: 'Some paragraph\n```\nSome code\n## Header in code block\nMore code\n```', | ||
}, | ||
{ | ||
source: '> This is a quote\nHeader| in quote', | ||
headerLevel: 1, | ||
expected: '> This is a quote\n# Header in quote', | ||
}, | ||
{ | ||
source: '1. This is a list item\n2. This is| a future header', | ||
headerLevel: 1, | ||
expected: '1. This is a list item\n# 2. This is a future header', | ||
}, | ||
] | ||
|
||
test.each(headerTestCases)('markdown headers $source', ({ source, headerLevel, expected }) => { | ||
const view = setupEditor(source) | ||
toggleHeader(view, headerLevel) | ||
expect(view.state.doc.toString()).toEqual(expected) | ||
}) | ||
|
||
const quotesTestCases: TestCase[] = [ | ||
{ | ||
desc: 'Create simple quote', | ||
source: 'This| is a quote', | ||
expected: '> This is a quote', | ||
}, | ||
{ | ||
desc: 'Multiline quote', | ||
source: 'This |is a quote\nThis is anoth|er quote', | ||
expected: '> This is a quote\nThis is another quote', | ||
}, | ||
{ | ||
desc: 'Disable quote', | ||
source: '> This |is a quote', | ||
expected: 'This is a quote', | ||
}, | ||
{ | ||
desc: 'Disable multiline quote', | ||
source: '> This is| a quote\nThis is |another quote\n\nThis is a new paragraph', | ||
expected: 'This is a quote\nThis is another quote\n\nThis is a new paragraph', | ||
}, | ||
{ | ||
desc: 'Enable quote in code block', | ||
source: '```\nSome code\nThis i|s a quote\nMore code\n```', | ||
expected: '```\nSome code\n> This is a quote\nMore code\n```', | ||
}, | ||
{ | ||
desc: 'Enable multiline quote in code block', | ||
source: '```\nSome code\nThis i|s a quote\nAlso |a quote\nMore code\n```', | ||
expected: '```\nSome code\n> This is a quote\nAlso a quote\nMore code\n```', | ||
}, | ||
{ | ||
desc: 'Disable quote in code block', | ||
source: '```\nSome code\n> This i|s a quote\nMore code\n```', | ||
expected: '```\nSome code\nThis is a quote\nMore code\n```', | ||
}, | ||
{ | ||
desc: 'Disable multiline quote in code block', | ||
source: '```\nSome code\n> This i|s a quote\nAlso a q|uote\n\nMore code\n```', | ||
expected: '```\nSome code\nThis is a quote\nAlso a quote\n\nMore code\n```', | ||
}, | ||
] | ||
|
||
test.each(quotesTestCases)('markdown quotes $desc', ({ source, expected }) => { | ||
const view = setupEditor(source) | ||
toggleQuote(view) | ||
expect(view.state.doc.toString()).toEqual(expected) | ||
}) | ||
|
||
const unorderedListTestCases: TestCase[] = [ | ||
{ | ||
desc: 'Create unordered list from empty line', | ||
source: '|', | ||
expected: '- ', | ||
}, | ||
{ | ||
desc: 'Create simple unordered list', | ||
source: '|List item\nList item\nList |item', | ||
expected: '- List item\n- List item\n- List item', | ||
}, | ||
{ | ||
desc: 'Disable unordered list', | ||
source: '- Li|st item\n- List item\n- Lis|t item', | ||
expected: 'List item\nList item\nList item', | ||
}, | ||
{ | ||
desc: 'Change ordered list to unordered list', | ||
source: '1. List| item\n2. List item\n3. Lis|t item', | ||
expected: '- List item\n- List item\n- List item', | ||
}, | ||
{ | ||
desc: 'Disable unordered list in code block', | ||
source: '```\nSome code\n- Lis|t item\nMore code\n```', | ||
expected: '```\nSome code\nList item\nMore code\n```', | ||
}, | ||
{ | ||
desc: 'Create unordered list in code block', | ||
source: '```\nSome code\nLis|t item\nAnother |list item\n```', | ||
expected: '```\nSome code\n- List item\n- Another list item\n```', | ||
}, | ||
{ | ||
desc: 'Change ordered list to unordered list in code block', | ||
source: '```\nSome code\n1. List| item\n2. List item\n3. Lis|t item\nSome paragraph\n```', | ||
expected: '```\nSome code\n- List item\n- List item\n- List item\nSome paragraph\n```', | ||
}, | ||
{ | ||
desc: 'Disable unordered list in code block', | ||
source: '```\nSome code\n- List| item\n- List item\n- Lis|t item\nSome paragraph\n```', | ||
expected: '```\nSome code\nList item\nList item\nList item\nSome paragraph\n```', | ||
}, | ||
] | ||
|
||
test.each(unorderedListTestCases)('markdown unordered list $desc', ({ source, expected }) => { | ||
const view = setupEditor(source) | ||
toggleList(view, 'unordered') | ||
expect(view.state.doc.toString()).toEqual(expected) | ||
}) | ||
|
||
const orderedListTestCases: TestCase[] = [ | ||
{ | ||
desc: 'Create unordered list from empty line', | ||
source: '|', | ||
expected: '1. ', | ||
}, | ||
{ | ||
desc: 'Create simple ordered list', | ||
source: 'Li|st item\nList item\nLis|t item', | ||
expected: '1. List item\n2. List item\n3. List item', | ||
}, | ||
{ | ||
desc: 'Disable ordered list', | ||
source: '1. Li|st item\n2. List item\n3. Lis|t item', | ||
expected: 'List item\nList item\nList item', | ||
}, | ||
{ | ||
desc: 'Change unordered list to ordered list', | ||
source: '- List| item\n- List item\n- Lis|t item', | ||
expected: '1. List item\n2. List item\n3. List item', | ||
}, | ||
{ | ||
desc: 'Create ordered list in code block', | ||
source: '```\nSome code\nLis|t item\nAnother |list item\n```', | ||
expected: '```\nSome code\n1. List item\n2. Another list item\n```', | ||
}, | ||
{ | ||
desc: 'Change unordered list to ordered list in code block', | ||
source: '```\nSome code\n- List| item\n- List item\n- Lis|t item\nSome paragraph\n```', | ||
expected: '```\nSome code\n1. List item\n2. List item\n3. List item\nSome paragraph\n```', | ||
}, | ||
{ | ||
desc: 'Disable ordered list in code block', | ||
source: '```\nSome code\n1. List| item\n2. List item\n3. Lis|t item\nSome paragraph\n```', | ||
expected: '```\nSome code\nList item\nList item\nList item\nSome paragraph\n```', | ||
}, | ||
] | ||
|
||
test.each(orderedListTestCases)('markdown ordered list $desc', ({ source, expected }) => { | ||
const view = setupEditor(source) | ||
toggleList(view, 'ordered') | ||
expect(view.state.doc.toString()).toEqual(expected) | ||
}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see any tests for checking what the "current" block type is identified as when different text is selected--that's complex and I think important to cover. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed, not needed. We might want to implement it in the future, but I don’t see much point given instant visual feedback in the editor, highlighting the block markers. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The block type should probably be a dropdown, top-level toggle buttons for everything would get unwieldy. We should be able to use the same menu code we used for Lexical:
https://github.com/enso-org/enso/pull/11469/files#diff-c9082ea1842a4e8a415bc841239cb0866d6d8ef678c957fe94b033e8cc8ef6d9
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done