Commit 87a9ae81 authored by Brett Walker's avatar Brett Walker Committed by Miguel Rincon

Auto increment markdown ordered lists

When adding a new list item to the end of a list,
we will increment the number, rather than repeating it.
parent ee8cdca2
...@@ -9,7 +9,7 @@ const LINK_TAG_PATTERN = '[{text}](url)'; ...@@ -9,7 +9,7 @@ const LINK_TAG_PATTERN = '[{text}](url)';
// a bullet point character (*+-) and an optional checkbox ([ ] [x]) // a bullet point character (*+-) and an optional checkbox ([ ] [x])
// OR a number with a . after it and an optional checkbox ([ ] [x]) // OR a number with a . after it and an optional checkbox ([ ] [x])
// followed by one or more whitespace characters // followed by one or more whitespace characters
const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isOl>[*+-])|(?<isUl>\d+\.))( \[([x ])\])?\s)(?<content>.)?/; const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isUl>[*+-])|(?<isOl>\d+\.))( \[([x ])\])?\s)(?<content>.)?/;
function selectedText(text, textarea) { function selectedText(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd); return text.substring(textarea.selectionStart, textarea.selectionEnd);
...@@ -31,8 +31,19 @@ function lineBefore(text, textarea, trimNewlines = true) { ...@@ -31,8 +31,19 @@ function lineBefore(text, textarea, trimNewlines = true) {
return split[split.length - 1]; return split[split.length - 1];
} }
function lineAfter(text, textarea) { function lineAfter(text, textarea, trimNewlines = true) {
return text.substring(textarea.selectionEnd).trim().split('\n')[0]; let split = text.substring(textarea.selectionEnd);
if (trimNewlines) {
split = split.trim();
} else {
// remove possible leading newline to get at the real line
split = split.replace(/^\n/, '');
}
split = split.split('\n');
return split[0];
} }
function convertMonacoSelectionToAceFormat(sel) { function convertMonacoSelectionToAceFormat(sel) {
...@@ -329,6 +340,25 @@ function handleSurroundSelectedText(e, textArea) { ...@@ -329,6 +340,25 @@ function handleSurroundSelectedText(e, textArea) {
} }
/* eslint-enable @gitlab/require-i18n-strings */ /* eslint-enable @gitlab/require-i18n-strings */
/**
* Returns the content for a new line following a list item.
*
* @param {Object} result - regex match of the current line
* @param {Object?} nextLineResult - regex match of the next line
* @returns string with the new list item
*/
function continueOlText(result, nextLineResult) {
const { indent, leader } = result.groups;
const { indent: nextIndent, isOl: nextIsOl } = nextLineResult?.groups ?? {};
const [numStr, postfix = ''] = leader.split('.');
const incrementBy = nextIsOl && nextIndent === indent ? 0 : 1;
const num = parseInt(numStr, 10) + incrementBy;
return `${indent}${num}.${postfix}`;
}
function handleContinueList(e, textArea) { function handleContinueList(e, textArea) {
if (!gon.features?.markdownContinueLists) return; if (!gon.features?.markdownContinueLists) return;
if (!(e.key === 'Enter')) return; if (!(e.key === 'Enter')) return;
...@@ -339,7 +369,7 @@ function handleContinueList(e, textArea) { ...@@ -339,7 +369,7 @@ function handleContinueList(e, textArea) {
const result = currentLine.match(LIST_LINE_HEAD_PATTERN); const result = currentLine.match(LIST_LINE_HEAD_PATTERN);
if (result) { if (result) {
const { indent, content, leader } = result.groups; const { leader, indent, content, isOl } = result.groups;
const prevLineEmpty = !content; const prevLineEmpty = !content;
if (prevLineEmpty) { if (prevLineEmpty) {
...@@ -349,12 +379,22 @@ function handleContinueList(e, textArea) { ...@@ -349,12 +379,22 @@ function handleContinueList(e, textArea) {
return; return;
} }
const itemInsert = `${indent}${leader}`; let itemToInsert;
if (isOl) {
const nextLine = lineAfter(textArea.value, textArea, false);
const nextLineResult = nextLine.match(LIST_LINE_HEAD_PATTERN);
itemToInsert = continueOlText(result, nextLineResult);
} else {
// isUl
itemToInsert = `${indent}${leader}`;
}
e.preventDefault(); e.preventDefault();
updateText({ updateText({
tag: itemInsert, tag: itemToInsert,
textArea, textArea,
blockTag: '', blockTag: '',
wrap: false, wrap: false,
......
...@@ -181,12 +181,13 @@ describe('init markdown', () => { ...@@ -181,12 +181,13 @@ describe('init markdown', () => {
${'- [ ] item'} | ${'- [ ] item\n- [ ] '} ${'- [ ] item'} | ${'- [ ] item\n- [ ] '}
${'- [x] item'} | ${'- [x] item\n- [x] '} ${'- [x] item'} | ${'- [x] item\n- [x] '}
${'- item\n - second'} | ${'- item\n - second\n - '} ${'- item\n - second'} | ${'- item\n - second\n - '}
${'1. item'} | ${'1. item\n1. '} ${'1. item'} | ${'1. item\n2. '}
${'1. [ ] item'} | ${'1. [ ] item\n1. [ ] '} ${'1. [ ] item'} | ${'1. [ ] item\n2. [ ] '}
${'1. [x] item'} | ${'1. [x] item\n1. [x] '} ${'1. [x] item'} | ${'1. [x] item\n2. [x] '}
${'108. item'} | ${'108. item\n108. '} ${'108. item'} | ${'108. item\n109. '}
${'108. item\n - second'} | ${'108. item\n - second\n - '} ${'108. item\n - second'} | ${'108. item\n - second\n - '}
${'108. item\n 1. second'} | ${'108. item\n 1. second\n 1. '} ${'108. item\n 1. second'} | ${'108. item\n 1. second\n 2. '}
${'non-item, will not change'} | ${'non-item, will not change'}
`('adds correct list continuation characters', ({ text, expected }) => { `('adds correct list continuation characters', ({ text, expected }) => {
textArea.value = text; textArea.value = text;
textArea.setSelectionRange(text.length, text.length); textArea.setSelectionRange(text.length, text.length);
...@@ -207,10 +208,10 @@ describe('init markdown', () => { ...@@ -207,10 +208,10 @@ describe('init markdown', () => {
${'- [ ] item\n- [ ] '} | ${'- [ ] item\n'} ${'- [ ] item\n- [ ] '} | ${'- [ ] item\n'}
${'- [x] item\n- [x] '} | ${'- [x] item\n'} ${'- [x] item\n- [x] '} | ${'- [x] item\n'}
${'- item\n - second\n - '} | ${'- item\n - second\n'} ${'- item\n - second\n - '} | ${'- item\n - second\n'}
${'1. item\n1. '} | ${'1. item\n'} ${'1. item\n2. '} | ${'1. item\n'}
${'1. [ ] item\n1. [ ] '} | ${'1. [ ] item\n'} ${'1. [ ] item\n2. [ ] '} | ${'1. [ ] item\n'}
${'1. [x] item\n1. [x] '} | ${'1. [x] item\n'} ${'1. [x] item\n2. [x] '} | ${'1. [x] item\n'}
${'108. item\n108. '} | ${'108. item\n'} ${'108. item\n109. '} | ${'108. item\n'}
${'108. item\n - second\n - '} | ${'108. item\n - second\n'} ${'108. item\n - second\n - '} | ${'108. item\n - second\n'}
${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'} ${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'}
`('adds correct list continuation characters', ({ text, expected }) => { `('adds correct list continuation characters', ({ text, expected }) => {
...@@ -243,6 +244,23 @@ describe('init markdown', () => { ...@@ -243,6 +244,23 @@ describe('init markdown', () => {
expect(textArea.value).toEqual(expected); expect(textArea.value).toEqual(expected);
}); });
it.each`
text | add_at | expected
${'1. one\n2. two\n3. three'} | ${13} | ${'1. one\n2. two\n2. \n3. three'}
${'108. item\n 5. second\n 6. six\n 7. seven'} | ${36} | ${'108. item\n 5. second\n 6. six\n 6. \n 7. seven'}
`(
'adds correct numbered continuation characters when in middle of list',
({ text, add_at, expected }) => {
textArea.value = text;
textArea.setSelectionRange(add_at, add_at);
textArea.addEventListener('keydown', keypressNoteText);
textArea.dispatchEvent(enterEvent);
expect(textArea.value).toEqual(expected);
},
);
it('does nothing if feature flag disabled', () => { it('does nothing if feature flag disabled', () => {
gon.features = { markdownContinueLists: false }; gon.features = { markdownContinueLists: false };
...@@ -262,8 +280,8 @@ describe('init markdown', () => { ...@@ -262,8 +280,8 @@ describe('init markdown', () => {
}); });
describe('with selection', () => { describe('with selection', () => {
const text = 'initial selected value'; let text = 'initial selected value';
const selected = 'selected'; let selected = 'selected';
let selectedIndex; let selectedIndex;
beforeEach(() => { beforeEach(() => {
...@@ -409,6 +427,46 @@ describe('init markdown', () => { ...@@ -409,6 +427,46 @@ describe('init markdown', () => {
expectedText.indexOf(expectedSelectionText, 1) + expectedSelectionText.length, expectedText.indexOf(expectedSelectionText, 1) + expectedSelectionText.length,
); );
}); });
it('adds block tags on line above and below selection', () => {
selected = 'this text\nis multiple\nlines';
text = `before \n${selected}\nafter `;
textArea.value = text;
selectedIndex = text.indexOf(selected);
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
insertMarkdownText({
textArea,
text,
tag: '',
blockTag: '***',
selected,
wrap: true,
});
expect(textArea.value).toEqual(`before \n***\n${selected}\n***\nafter `);
});
it('removes block tags on line above and below selection', () => {
selected = 'this text\nis multiple\nlines';
text = `before \n***\n${selected}\n***\nafter `;
textArea.value = text;
selectedIndex = text.indexOf(selected);
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
insertMarkdownText({
textArea,
text,
tag: '',
blockTag: '***',
selected,
wrap: true,
});
expect(textArea.value).toEqual(`before \n${selected}\nafter `);
});
}); });
}); });
}); });
...@@ -460,7 +518,31 @@ describe('init markdown', () => { ...@@ -460,7 +518,31 @@ describe('init markdown', () => {
expect(editor.replaceSelectedText).toHaveBeenCalledWith(`***\n${selected}\n***\n`, undefined); expect(editor.replaceSelectedText).toHaveBeenCalledWith(`***\n${selected}\n***\n`, undefined);
}); });
it('uses ace editor to navigate back tag length when nothing is selected', () => { it('removes block tags on line above and below selection', () => {
const selected = 'this text\nis multiple\nlines';
const text = `before\n***\n${selected}\n***\nafter`;
editor.getSelection = jest.fn().mockReturnValue({
startLineNumber: 2,
startColumn: 1,
endLineNumber: 4,
endColumn: 2,
setSelectionRange: jest.fn(),
});
insertMarkdownText({
text,
tag: '',
blockTag: '***',
selected,
wrap: true,
editor,
});
expect(editor.replaceSelectedText).toHaveBeenCalledWith(`${selected}\n`, undefined);
});
it('uses editor to navigate back tag length when nothing is selected', () => {
editor.getSelection = jest.fn().mockReturnValue({ editor.getSelection = jest.fn().mockReturnValue({
startLineNumber: 1, startLineNumber: 1,
startColumn: 1, startColumn: 1,
...@@ -480,7 +562,7 @@ describe('init markdown', () => { ...@@ -480,7 +562,7 @@ describe('init markdown', () => {
expect(editor.moveCursor).toHaveBeenCalledWith(-1); expect(editor.moveCursor).toHaveBeenCalledWith(-1);
}); });
it('ace editor does not navigate back when there is selected text', () => { it('editor does not navigate back when there is selected text', () => {
insertMarkdownText({ insertMarkdownText({
text: editor.getValue, text: editor.getValue,
tag: '*', tag: '*',
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment