overtype 1.2.2 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/overtype.cjs +243 -1
- package/dist/overtype.cjs.map +2 -2
- package/dist/overtype.esm.js +243 -1
- package/dist/overtype.esm.js.map +2 -2
- package/dist/overtype.js +243 -1
- package/dist/overtype.js.map +2 -2
- package/dist/overtype.min.js +56 -49
- package/package.json +1 -1
- package/src/overtype.js +145 -1
- package/src/parser.js +162 -0
package/src/overtype.js
CHANGED
|
@@ -164,7 +164,8 @@ class OverType {
|
|
|
164
164
|
showActiveLineRaw: false,
|
|
165
165
|
showStats: false,
|
|
166
166
|
toolbar: false,
|
|
167
|
-
statsFormatter: null
|
|
167
|
+
statsFormatter: null,
|
|
168
|
+
smartLists: true // Enable smart list continuation
|
|
168
169
|
};
|
|
169
170
|
|
|
170
171
|
// Remove theme and colors from options - these are now global
|
|
@@ -583,6 +584,14 @@ class OverType {
|
|
|
583
584
|
return;
|
|
584
585
|
}
|
|
585
586
|
|
|
587
|
+
// Handle Enter key for smart list continuation
|
|
588
|
+
if (event.key === 'Enter' && !event.shiftKey && !event.metaKey && !event.ctrlKey && this.options.smartLists) {
|
|
589
|
+
if (this.handleSmartListContinuation()) {
|
|
590
|
+
event.preventDefault();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
586
595
|
// Let shortcuts manager handle other keys
|
|
587
596
|
const handled = this.shortcuts.handleKeydown(event);
|
|
588
597
|
|
|
@@ -592,6 +601,141 @@ class OverType {
|
|
|
592
601
|
}
|
|
593
602
|
}
|
|
594
603
|
|
|
604
|
+
/**
|
|
605
|
+
* Handle smart list continuation
|
|
606
|
+
* @returns {boolean} Whether the event was handled
|
|
607
|
+
*/
|
|
608
|
+
handleSmartListContinuation() {
|
|
609
|
+
const textarea = this.textarea;
|
|
610
|
+
const cursorPos = textarea.selectionStart;
|
|
611
|
+
const context = MarkdownParser.getListContext(textarea.value, cursorPos);
|
|
612
|
+
|
|
613
|
+
if (!context || !context.inList) return false;
|
|
614
|
+
|
|
615
|
+
// Handle empty list item (exit list)
|
|
616
|
+
if (context.content.trim() === '' && cursorPos >= context.markerEndPos) {
|
|
617
|
+
this.deleteListMarker(context);
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Handle text splitting if cursor is in middle of content
|
|
622
|
+
if (cursorPos > context.markerEndPos && cursorPos < context.lineEnd) {
|
|
623
|
+
this.splitListItem(context, cursorPos);
|
|
624
|
+
} else {
|
|
625
|
+
// Just add new item after current line
|
|
626
|
+
this.insertNewListItem(context);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Handle numbered list renumbering
|
|
630
|
+
if (context.listType === 'numbered') {
|
|
631
|
+
this.scheduleNumberedListUpdate();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Delete list marker and exit list
|
|
639
|
+
* @private
|
|
640
|
+
*/
|
|
641
|
+
deleteListMarker(context) {
|
|
642
|
+
// Select from line start to marker end
|
|
643
|
+
this.textarea.setSelectionRange(context.lineStart, context.markerEndPos);
|
|
644
|
+
document.execCommand('delete');
|
|
645
|
+
|
|
646
|
+
// Trigger input event
|
|
647
|
+
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Insert new list item
|
|
652
|
+
* @private
|
|
653
|
+
*/
|
|
654
|
+
insertNewListItem(context) {
|
|
655
|
+
const newItem = MarkdownParser.createNewListItem(context);
|
|
656
|
+
document.execCommand('insertText', false, '\n' + newItem);
|
|
657
|
+
|
|
658
|
+
// Trigger input event
|
|
659
|
+
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Split list item at cursor position
|
|
664
|
+
* @private
|
|
665
|
+
*/
|
|
666
|
+
splitListItem(context, cursorPos) {
|
|
667
|
+
// Get text after cursor
|
|
668
|
+
const textAfterCursor = context.content.substring(cursorPos - context.markerEndPos);
|
|
669
|
+
|
|
670
|
+
// Delete text after cursor
|
|
671
|
+
this.textarea.setSelectionRange(cursorPos, context.lineEnd);
|
|
672
|
+
document.execCommand('delete');
|
|
673
|
+
|
|
674
|
+
// Insert new list item with remaining text
|
|
675
|
+
const newItem = MarkdownParser.createNewListItem(context);
|
|
676
|
+
document.execCommand('insertText', false, '\n' + newItem + textAfterCursor);
|
|
677
|
+
|
|
678
|
+
// Position cursor after new list marker
|
|
679
|
+
const newCursorPos = this.textarea.selectionStart - textAfterCursor.length;
|
|
680
|
+
this.textarea.setSelectionRange(newCursorPos, newCursorPos);
|
|
681
|
+
|
|
682
|
+
// Trigger input event
|
|
683
|
+
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Schedule numbered list renumbering
|
|
688
|
+
* @private
|
|
689
|
+
*/
|
|
690
|
+
scheduleNumberedListUpdate() {
|
|
691
|
+
// Clear any pending update
|
|
692
|
+
if (this.numberUpdateTimeout) {
|
|
693
|
+
clearTimeout(this.numberUpdateTimeout);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Schedule update after current input cycle
|
|
697
|
+
this.numberUpdateTimeout = setTimeout(() => {
|
|
698
|
+
this.updateNumberedLists();
|
|
699
|
+
}, 10);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Update/renumber all numbered lists
|
|
704
|
+
* @private
|
|
705
|
+
*/
|
|
706
|
+
updateNumberedLists() {
|
|
707
|
+
const value = this.textarea.value;
|
|
708
|
+
const cursorPos = this.textarea.selectionStart;
|
|
709
|
+
|
|
710
|
+
const newValue = MarkdownParser.renumberLists(value);
|
|
711
|
+
|
|
712
|
+
if (newValue !== value) {
|
|
713
|
+
// Calculate cursor offset
|
|
714
|
+
let offset = 0;
|
|
715
|
+
const oldLines = value.split('\n');
|
|
716
|
+
const newLines = newValue.split('\n');
|
|
717
|
+
let charCount = 0;
|
|
718
|
+
|
|
719
|
+
for (let i = 0; i < oldLines.length && charCount < cursorPos; i++) {
|
|
720
|
+
if (oldLines[i] !== newLines[i]) {
|
|
721
|
+
const diff = newLines[i].length - oldLines[i].length;
|
|
722
|
+
if (charCount + oldLines[i].length < cursorPos) {
|
|
723
|
+
offset += diff;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
charCount += oldLines[i].length + 1; // +1 for newline
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Update textarea
|
|
730
|
+
this.textarea.value = newValue;
|
|
731
|
+
const newCursorPos = cursorPos + offset;
|
|
732
|
+
this.textarea.setSelectionRange(newCursorPos, newCursorPos);
|
|
733
|
+
|
|
734
|
+
// Trigger update
|
|
735
|
+
this.textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
595
739
|
/**
|
|
596
740
|
* Handle scroll events
|
|
597
741
|
* @private
|
package/src/parser.js
CHANGED
|
@@ -517,4 +517,166 @@ export class MarkdownParser {
|
|
|
517
517
|
|
|
518
518
|
return processed;
|
|
519
519
|
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* List pattern definitions
|
|
523
|
+
*/
|
|
524
|
+
static LIST_PATTERNS = {
|
|
525
|
+
bullet: /^(\s*)([-*+])\s+(.*)$/,
|
|
526
|
+
numbered: /^(\s*)(\d+)\.\s+(.*)$/,
|
|
527
|
+
checkbox: /^(\s*)-\s+\[([ x])\]\s+(.*)$/
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Get list context at cursor position
|
|
532
|
+
* @param {string} text - Full text content
|
|
533
|
+
* @param {number} cursorPosition - Current cursor position
|
|
534
|
+
* @returns {Object} List context information
|
|
535
|
+
*/
|
|
536
|
+
static getListContext(text, cursorPosition) {
|
|
537
|
+
// Find the line containing the cursor
|
|
538
|
+
const lines = text.split('\n');
|
|
539
|
+
let currentPos = 0;
|
|
540
|
+
let lineIndex = 0;
|
|
541
|
+
let lineStart = 0;
|
|
542
|
+
|
|
543
|
+
for (let i = 0; i < lines.length; i++) {
|
|
544
|
+
const lineLength = lines[i].length;
|
|
545
|
+
if (currentPos + lineLength >= cursorPosition) {
|
|
546
|
+
lineIndex = i;
|
|
547
|
+
lineStart = currentPos;
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
currentPos += lineLength + 1; // +1 for newline
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const currentLine = lines[lineIndex];
|
|
554
|
+
const lineEnd = lineStart + currentLine.length;
|
|
555
|
+
|
|
556
|
+
// Check for checkbox first (most specific)
|
|
557
|
+
const checkboxMatch = currentLine.match(this.LIST_PATTERNS.checkbox);
|
|
558
|
+
if (checkboxMatch) {
|
|
559
|
+
return {
|
|
560
|
+
inList: true,
|
|
561
|
+
listType: 'checkbox',
|
|
562
|
+
indent: checkboxMatch[1],
|
|
563
|
+
marker: '-',
|
|
564
|
+
checked: checkboxMatch[2] === 'x',
|
|
565
|
+
content: checkboxMatch[3],
|
|
566
|
+
lineStart,
|
|
567
|
+
lineEnd,
|
|
568
|
+
markerEndPos: lineStart + checkboxMatch[1].length + checkboxMatch[2].length + 5 // indent + "- [ ] "
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Check for bullet list
|
|
573
|
+
const bulletMatch = currentLine.match(this.LIST_PATTERNS.bullet);
|
|
574
|
+
if (bulletMatch) {
|
|
575
|
+
return {
|
|
576
|
+
inList: true,
|
|
577
|
+
listType: 'bullet',
|
|
578
|
+
indent: bulletMatch[1],
|
|
579
|
+
marker: bulletMatch[2],
|
|
580
|
+
content: bulletMatch[3],
|
|
581
|
+
lineStart,
|
|
582
|
+
lineEnd,
|
|
583
|
+
markerEndPos: lineStart + bulletMatch[1].length + bulletMatch[2].length + 1 // indent + marker + space
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Check for numbered list
|
|
588
|
+
const numberedMatch = currentLine.match(this.LIST_PATTERNS.numbered);
|
|
589
|
+
if (numberedMatch) {
|
|
590
|
+
return {
|
|
591
|
+
inList: true,
|
|
592
|
+
listType: 'numbered',
|
|
593
|
+
indent: numberedMatch[1],
|
|
594
|
+
marker: parseInt(numberedMatch[2]),
|
|
595
|
+
content: numberedMatch[3],
|
|
596
|
+
lineStart,
|
|
597
|
+
lineEnd,
|
|
598
|
+
markerEndPos: lineStart + numberedMatch[1].length + numberedMatch[2].length + 2 // indent + number + ". "
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Not in a list
|
|
603
|
+
return {
|
|
604
|
+
inList: false,
|
|
605
|
+
listType: null,
|
|
606
|
+
indent: '',
|
|
607
|
+
marker: null,
|
|
608
|
+
content: currentLine,
|
|
609
|
+
lineStart,
|
|
610
|
+
lineEnd,
|
|
611
|
+
markerEndPos: lineStart
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Create a new list item based on context
|
|
617
|
+
* @param {Object} context - List context from getListContext
|
|
618
|
+
* @returns {string} New list item text
|
|
619
|
+
*/
|
|
620
|
+
static createNewListItem(context) {
|
|
621
|
+
switch (context.listType) {
|
|
622
|
+
case 'bullet':
|
|
623
|
+
return `${context.indent}${context.marker} `;
|
|
624
|
+
case 'numbered':
|
|
625
|
+
return `${context.indent}${context.marker + 1}. `;
|
|
626
|
+
case 'checkbox':
|
|
627
|
+
return `${context.indent}- [ ] `;
|
|
628
|
+
default:
|
|
629
|
+
return '';
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Renumber all numbered lists in text
|
|
635
|
+
* @param {string} text - Text containing numbered lists
|
|
636
|
+
* @returns {string} Text with renumbered lists
|
|
637
|
+
*/
|
|
638
|
+
static renumberLists(text) {
|
|
639
|
+
const lines = text.split('\n');
|
|
640
|
+
const numbersByIndent = new Map();
|
|
641
|
+
let inList = false;
|
|
642
|
+
|
|
643
|
+
const result = lines.map(line => {
|
|
644
|
+
const match = line.match(this.LIST_PATTERNS.numbered);
|
|
645
|
+
|
|
646
|
+
if (match) {
|
|
647
|
+
const indent = match[1];
|
|
648
|
+
const indentLevel = indent.length;
|
|
649
|
+
const content = match[3];
|
|
650
|
+
|
|
651
|
+
// If we weren't in a list or indent changed, reset lower levels
|
|
652
|
+
if (!inList) {
|
|
653
|
+
numbersByIndent.clear();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Get the next number for this indent level
|
|
657
|
+
const currentNumber = (numbersByIndent.get(indentLevel) || 0) + 1;
|
|
658
|
+
numbersByIndent.set(indentLevel, currentNumber);
|
|
659
|
+
|
|
660
|
+
// Clear deeper indent levels
|
|
661
|
+
for (const [level] of numbersByIndent) {
|
|
662
|
+
if (level > indentLevel) {
|
|
663
|
+
numbersByIndent.delete(level);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
inList = true;
|
|
668
|
+
return `${indent}${currentNumber}. ${content}`;
|
|
669
|
+
} else {
|
|
670
|
+
// Not a numbered list item
|
|
671
|
+
if (line.trim() === '' || !line.match(/^\s/)) {
|
|
672
|
+
// Empty line or non-indented line breaks the list
|
|
673
|
+
inList = false;
|
|
674
|
+
numbersByIndent.clear();
|
|
675
|
+
}
|
|
676
|
+
return line;
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
return result.join('\n');
|
|
681
|
+
}
|
|
520
682
|
}
|