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)';
// a bullet point character (*+-) and an optional checkbox ([ ] [x])
// OR a number with a . after it and an optional checkbox ([ ] [x])
// 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) {
return text.substring(textarea.selectionStart, textarea.selectionEnd);
......@@ -31,8 +31,19 @@ function lineBefore(text, textarea, trimNewlines = true) {
return split[split.length - 1];
}
function lineAfter(text, textarea) {
return text.substring(textarea.selectionEnd).trim().split('\n')[0];
function lineAfter(text, textarea, trimNewlines = true) {
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) {
......@@ -329,6 +340,25 @@ function handleSurroundSelectedText(e, textArea) {
}
/* 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) {
if (!gon.features?.markdownContinueLists) return;
if (!(e.key === 'Enter')) return;
......@@ -339,7 +369,7 @@ function handleContinueList(e, textArea) {
const result = currentLine.match(LIST_LINE_HEAD_PATTERN);
if (result) {
const { indent, content, leader } = result.groups;
const { leader, indent, content, isOl } = result.groups;
const prevLineEmpty = !content;
if (prevLineEmpty) {
......@@ -349,12 +379,22 @@ function handleContinueList(e, textArea) {
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();
updateText({
tag: itemInsert,
tag: itemToInsert,
textArea,
blockTag: '',
wrap: false,
......
......@@ -181,12 +181,13 @@ describe('init markdown', () => {
${'- [ ] item'} | ${'- [ ] item\n- [ ] '}
${'- [x] item'} | ${'- [x] item\n- [x] '}
${'- item\n - second'} | ${'- item\n - second\n - '}
${'1. item'} | ${'1. item\n1. '}
${'1. [ ] item'} | ${'1. [ ] item\n1. [ ] '}
${'1. [x] item'} | ${'1. [x] item\n1. [x] '}
${'108. item'} | ${'108. item\n108. '}
${'1. item'} | ${'1. item\n2. '}
${'1. [ ] item'} | ${'1. [ ] item\n2. [ ] '}
${'1. [x] item'} | ${'1. [x] item\n2. [x] '}
${'108. item'} | ${'108. item\n109. '}
${'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 }) => {
textArea.value = text;
textArea.setSelectionRange(text.length, text.length);
......@@ -207,10 +208,10 @@ describe('init markdown', () => {
${'- [ ] item\n- [ ] '} | ${'- [ ] item\n'}
${'- [x] item\n- [x] '} | ${'- [x] item\n'}
${'- item\n - second\n - '} | ${'- item\n - second\n'}
${'1. item\n1. '} | ${'1. item\n'}
${'1. [ ] item\n1. [ ] '} | ${'1. [ ] item\n'}
${'1. [x] item\n1. [x] '} | ${'1. [x] item\n'}
${'108. item\n108. '} | ${'108. item\n'}
${'1. item\n2. '} | ${'1. item\n'}
${'1. [ ] item\n2. [ ] '} | ${'1. [ ] item\n'}
${'1. [x] item\n2. [x] '} | ${'1. [x] item\n'}
${'108. item\n109. '} | ${'108. item\n'}
${'108. item\n - second\n - '} | ${'108. item\n - second\n'}
${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'}
`('adds correct list continuation characters', ({ text, expected }) => {
......@@ -243,6 +244,23 @@ describe('init markdown', () => {
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', () => {
gon.features = { markdownContinueLists: false };
......@@ -262,8 +280,8 @@ describe('init markdown', () => {
});
describe('with selection', () => {
const text = 'initial selected value';
const selected = 'selected';
let text = 'initial selected value';
let selected = 'selected';
let selectedIndex;
beforeEach(() => {
......@@ -409,6 +427,46 @@ describe('init markdown', () => {
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', () => {
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({
startLineNumber: 1,
startColumn: 1,
......@@ -480,7 +562,7 @@ describe('init markdown', () => {
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({
text: editor.getValue,
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