diff --git a/web_src/js/features/comp/EditorMarkdown.test.ts b/web_src/js/features/comp/EditorMarkdown.test.ts
index 7b4b44e83c..9f34d77348 100644
--- a/web_src/js/features/comp/EditorMarkdown.test.ts
+++ b/web_src/js/features/comp/EditorMarkdown.test.ts
@@ -1,4 +1,166 @@
-import {initTextareaMarkdown} from './EditorMarkdown.ts';
+import {initTextareaMarkdown, markdownHandleIndention, textareaSplitLines} from './EditorMarkdown.ts';
+
+test('textareaSplitLines', () => {
+  let ret = textareaSplitLines('a\nbc\nd', 0);
+  expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 0, posLineIndex: 0, inlinePos: 0});
+
+  ret = textareaSplitLines('a\nbc\nd', 1);
+  expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 0, posLineIndex: 0, inlinePos: 1});
+
+  ret = textareaSplitLines('a\nbc\nd', 2);
+  expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 2, posLineIndex: 1, inlinePos: 0});
+
+  ret = textareaSplitLines('a\nbc\nd', 3);
+  expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 2, posLineIndex: 1, inlinePos: 1});
+
+  ret = textareaSplitLines('a\nbc\nd', 4);
+  expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 2, posLineIndex: 1, inlinePos: 2});
+
+  ret = textareaSplitLines('a\nbc\nd', 5);
+  expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 5, posLineIndex: 2, inlinePos: 0});
+
+  ret = textareaSplitLines('a\nbc\nd', 6);
+  expect(ret).toEqual({lines: ['a', 'bc', 'd'], lengthBeforePosLine: 5, posLineIndex: 2, inlinePos: 1});
+});
+
+test('markdownHandleIndention', () => {
+  const testInput = (input: string, expected?: string) => {
+    const inputPos = input.indexOf('|');
+    input = input.replace('|', '');
+    const ret = markdownHandleIndention({value: input, selStart: inputPos, selEnd: inputPos});
+    if (expected === null) {
+      expect(ret).toEqual({handled: false});
+    } else {
+      const expectedPos = expected.indexOf('|');
+      expected = expected.replace('|', '');
+      expect(ret).toEqual({
+        handled: true,
+        valueSelection: {value: expected, selStart: expectedPos, selEnd: expectedPos},
+      });
+    }
+  };
+
+  testInput(`
+  a|b
+`, `
+  a
+  |b
+`);
+
+  testInput(`
+1. a
+2. |
+`, `
+1. a
+|
+`);
+
+  testInput(`
+|1. a
+`, null); // let browser handle it
+
+  testInput(`
+1. a
+1. b|c
+`, `
+1. a
+2. b
+3. |c
+`);
+
+  testInput(`
+2. a
+2. b|
+
+1. x
+1. y
+`, `
+1. a
+2. b
+3. |
+
+1. x
+1. y
+`);
+
+  testInput(`
+2. a
+2. b
+
+1. x|
+1. y
+`, `
+2. a
+2. b
+
+1. x
+2. |
+3. y
+`);
+
+  testInput(`
+1. a
+2. b|
+3. c
+`, `
+1. a
+2. b
+3. |
+4. c
+`);
+
+  testInput(`
+1. a
+  1. b
+  2. b
+  3. b
+  4. b
+1. c|
+`, `
+1. a
+  1. b
+  2. b
+  3. b
+  4. b
+2. c
+3. |
+`);
+
+  testInput(`
+1. a
+2. a
+3. a
+4. a
+5. a
+6. a
+7. a
+8. a
+9. b|c
+`, `
+1. a
+2. a
+3. a
+4. a
+5. a
+6. a
+7. a
+8. a
+9. b
+10. |c
+`);
+
+  // this is a special case, it's difficult to re-format the parent level at the moment, so leave it to the future
+  testInput(`
+1. a
+  2. b|
+3. c
+`, `
+1. a
+  1. b
+  2. |
+3. c
+`);
+});
 
 test('EditorMarkdown', () => {
   const textarea = document.createElement('textarea');
@@ -32,10 +194,10 @@ test('EditorMarkdown', () => {
   testInput({value: '1. \n2. ', pos: 3}, {value: '\n2. ', pos: 0});
 
   testInput('- x', '- x\n- ');
-  testInput('1. foo', '1. foo\n1. ');
-  testInput({value: '1. a\n2. b\n3. c', pos: 4}, {value: '1. a\n1. \n2. b\n3. c', pos: 8});
+  testInput('1. foo', '1. foo\n2. ');
+  testInput({value: '1. a\n2. b\n3. c', pos: 4}, {value: '1. a\n2. \n3. b\n4. c', pos: 8});
   testInput('- [ ]', '- [ ]\n- ');
   testInput('- [ ] foo', '- [ ] foo\n- [ ] ');
   testInput('* [x] foo', '* [x] foo\n* [ ] ');
-  testInput('1. [x] foo', '1. [x] foo\n1. [ ] ');
+  testInput('1. [x] foo', '1. [x] foo\n2. [ ] ');
 });
diff --git a/web_src/js/features/comp/EditorMarkdown.ts b/web_src/js/features/comp/EditorMarkdown.ts
index 5e2ef121f5..d3ed492396 100644
--- a/web_src/js/features/comp/EditorMarkdown.ts
+++ b/web_src/js/features/comp/EditorMarkdown.ts
@@ -14,7 +14,13 @@ export function textareaInsertText(textarea, value) {
   triggerEditorContentChanged(textarea);
 }
 
-function handleIndentSelection(textarea, e) {
+type TextareaValueSelection = {
+  value: string;
+  selStart: number;
+  selEnd: number;
+}
+
+function handleIndentSelection(textarea: HTMLTextAreaElement, e) {
   const selStart = textarea.selectionStart;
   const selEnd = textarea.selectionEnd;
   if (selEnd === selStart) return; // do not process when no selection
@@ -56,53 +62,125 @@ function handleIndentSelection(textarea, e) {
   triggerEditorContentChanged(textarea);
 }
 
-function handleNewline(textarea: HTMLTextAreaElement, e: Event) {
-  const selStart = textarea.selectionStart;
-  const selEnd = textarea.selectionEnd;
-  if (selEnd !== selStart) return; // do not process when there is a selection
+type MarkdownHandleIndentionResult = {
+  handled: boolean;
+  valueSelection?: TextareaValueSelection;
+}
 
-  const value = textarea.value;
+type TextLinesBuffer = {
+  lines: string[];
+  lengthBeforePosLine: number;
+  posLineIndex: number;
+  inlinePos: number
+}
 
-  // find the current line
-  // * if selStart is 0, lastIndexOf(..., -1) is the same as lastIndexOf(..., 0)
-  // * if lastIndexOf reruns -1, lineStart is 0 and it is still correct.
-  const lineStart = value.lastIndexOf('\n', selStart - 1) + 1;
-  let lineEnd = value.indexOf('\n', selStart);
-  lineEnd = lineEnd < 0 ? value.length : lineEnd;
-  let line = value.slice(lineStart, lineEnd);
-  if (!line) return; // if the line is empty, do nothing, let the browser handle it
+export function textareaSplitLines(value: string, pos: number): TextLinesBuffer {
+  const lines = value.split('\n');
+  let lengthBeforePosLine = 0, inlinePos = 0, posLineIndex = 0;
+  for (; posLineIndex < lines.length; posLineIndex++) {
+    const lineLength = lines[posLineIndex].length + 1;
+    if (lengthBeforePosLine + lineLength > pos) {
+      inlinePos = pos - lengthBeforePosLine;
+      break;
+    }
+    lengthBeforePosLine += lineLength;
+  }
+  return {lines, lengthBeforePosLine, posLineIndex, inlinePos};
+}
+
+function markdownReformatListNumbers(linesBuf: TextLinesBuffer, indention: string) {
+  const reDeeperIndention = new RegExp(`^${indention}\\s+`);
+  const reSameLevel = new RegExp(`^${indention}([0-9]+)\\.`);
+  let firstLineIdx: number;
+  for (firstLineIdx = linesBuf.posLineIndex - 1; firstLineIdx >= 0; firstLineIdx--) {
+    const line = linesBuf.lines[firstLineIdx];
+    if (!reDeeperIndention.test(line) && !reSameLevel.test(line)) break;
+  }
+  firstLineIdx++;
+  let num = 1;
+  for (let i = firstLineIdx; i < linesBuf.lines.length; i++) {
+    const oldLine = linesBuf.lines[i];
+    const sameLevel = reSameLevel.test(oldLine);
+    if (!sameLevel && !reDeeperIndention.test(oldLine)) break;
+    if (sameLevel) {
+      const newLine = `${indention}${num}.${oldLine.replace(reSameLevel, '')}`;
+      linesBuf.lines[i] = newLine;
+      num++;
+      if (linesBuf.posLineIndex === i) {
+        // need to correct the cursor inline position if the line length changes
+        linesBuf.inlinePos += newLine.length - oldLine.length;
+        linesBuf.inlinePos = Math.max(0, linesBuf.inlinePos);
+        linesBuf.inlinePos = Math.min(newLine.length, linesBuf.inlinePos);
+      }
+    }
+  }
+  recalculateLengthBeforeLine(linesBuf);
+}
+
+function recalculateLengthBeforeLine(linesBuf: TextLinesBuffer) {
+  linesBuf.lengthBeforePosLine = 0;
+  for (let i = 0; i < linesBuf.posLineIndex; i++) {
+    linesBuf.lengthBeforePosLine += linesBuf.lines[i].length + 1;
+  }
+}
+
+export function markdownHandleIndention(tvs: TextareaValueSelection): MarkdownHandleIndentionResult {
+  const unhandled: MarkdownHandleIndentionResult = {handled: false};
+  if (tvs.selEnd !== tvs.selStart) return unhandled; // do not process when there is a selection
+
+  const linesBuf = textareaSplitLines(tvs.value, tvs.selStart);
+  const line = linesBuf.lines[linesBuf.posLineIndex] ?? '';
+  if (!line) return unhandled; // if the line is empty, do nothing, let the browser handle it
 
   // parse the indention
-  const indention = /^\s*/.exec(line)[0];
-  line = line.slice(indention.length);
+  let lineContent = line;
+  const indention = /^\s*/.exec(lineContent)[0];
+  lineContent = lineContent.slice(indention.length);
+  if (linesBuf.inlinePos <= indention.length) return unhandled; // if cursor is at the indention, do nothing, let the browser handle it
 
   // parse the prefixes: "1. ", "- ", "* ", there could also be " [ ] " or " [x] " for task lists
   // there must be a space after the prefix because none of "1.foo" / "-foo" is a list item
-  const prefixMatch = /^([0-9]+\.|[-*])(\s\[([ x])\])?\s/.exec(line);
+  const prefixMatch = /^([0-9]+\.|[-*])(\s\[([ x])\])?\s/.exec(lineContent);
   let prefix = '';
   if (prefixMatch) {
     prefix = prefixMatch[0];
-    if (lineStart + prefix.length > selStart) prefix = ''; // do not add new line if cursor is at prefix
+    if (prefix.length > linesBuf.inlinePos) prefix = ''; // do not add new line if cursor is at prefix
   }
 
-  line = line.slice(prefix.length);
-  if (!indention && !prefix) return; // if no indention and no prefix, do nothing, let the browser handle it
+  lineContent = lineContent.slice(prefix.length);
+  if (!indention && !prefix) return unhandled; // if no indention and no prefix, do nothing, let the browser handle it
 
-  e.preventDefault();
-  if (!line) {
+  if (!lineContent) {
     // clear current line if we only have i.e. '1. ' and the user presses enter again to finish creating a list
-    textarea.value = value.slice(0, lineStart) + value.slice(lineEnd);
-    textarea.setSelectionRange(selStart - prefix.length, selStart - prefix.length);
+    linesBuf.lines[linesBuf.posLineIndex] = '';
+    linesBuf.inlinePos = 0;
   } else {
-    // start a new line with the same indention and prefix
+    // start a new line with the same indention
     let newPrefix = prefix;
-    // a simple approach, otherwise it needs to parse the lines after the current line
     if (/^\d+\./.test(prefix)) newPrefix = `1. ${newPrefix.slice(newPrefix.indexOf('.') + 2)}`;
     newPrefix = newPrefix.replace('[x]', '[ ]');
-    const newLine = `\n${indention}${newPrefix}`;
-    textarea.value = value.slice(0, selStart) + newLine + value.slice(selEnd);
-    textarea.setSelectionRange(selStart + newLine.length, selStart + newLine.length);
+
+    const inlinePos = linesBuf.inlinePos;
+    linesBuf.lines[linesBuf.posLineIndex] = line.substring(0, inlinePos);
+    const newLineLeft = `${indention}${newPrefix}`;
+    const newLine = `${newLineLeft}${line.substring(inlinePos)}`;
+    linesBuf.lines.splice(linesBuf.posLineIndex + 1, 0, newLine);
+    linesBuf.posLineIndex++;
+    linesBuf.inlinePos = newLineLeft.length;
+    recalculateLengthBeforeLine(linesBuf);
   }
+
+  markdownReformatListNumbers(linesBuf, indention);
+  const newPos = linesBuf.lengthBeforePosLine + linesBuf.inlinePos;
+  return {handled: true, valueSelection: {value: linesBuf.lines.join('\n'), selStart: newPos, selEnd: newPos}};
+}
+
+function handleNewline(textarea: HTMLTextAreaElement, e: Event) {
+  const ret = markdownHandleIndention({value: textarea.value, selStart: textarea.selectionStart, selEnd: textarea.selectionEnd});
+  if (!ret.handled) return;
+  e.preventDefault();
+  textarea.value = ret.valueSelection.value;
+  textarea.setSelectionRange(ret.valueSelection.selStart, ret.valueSelection.selEnd);
   triggerEditorContentChanged(textarea);
 }