New editor.

This commit is contained in:
retoor 2025-08-06 15:33:13 +02:00
parent e62da0aef1
commit 89d639e44e

View File

@ -1,226 +1,494 @@
import { NjetComponent} from "/njet.js"
import { NjetComponent } from "/njet.js"
class NjetEditor extends NjetComponent {
constructor() {
super();
this.attachShadow({ mode: 'open' });
class NjetEditor extends NjetComponent {
constructor() {
super();
this.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
#editor {
padding: 1rem;
outline: none;
white-space: pre-wrap;
line-height: 1.5;
height: 100%;
overflow-y: auto;
background: #1e1e1e;
color: #d4d4d4;
}
#command-line {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 0.2rem 1rem;
background: #333;
color: #0f0;
display: none;
font-family: monospace;
}
`;
const style = document.createElement('style');
style.textContent = `
:host {
display: block;
position: relative;
height: 100%;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
#editor {
padding: 1rem;
outline: none;
white-space: pre-wrap;
line-height: 1.5;
height: calc(100% - 30px);
overflow-y: auto;
background: #1e1e1e;
color: #d4d4d4;
font-size: 14px;
caret-color: #fff;
}
#editor.insert-mode {
caret-color: #4ec9b0;
}
#editor.visual-mode {
caret-color: #c586c0;
}
#editor::selection {
background: #264f78;
}
#status-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 30px;
background: #007acc;
color: #fff;
display: flex;
align-items: center;
padding: 0 1rem;
font-size: 12px;
font-weight: 500;
}
#mode-indicator {
text-transform: uppercase;
margin-right: 20px;
font-weight: bold;
}
#command-line {
position: absolute;
bottom: 30px;
left: 0;
width: 100%;
padding: 0.3rem 1rem;
background: #2d2d2d;
color: #d4d4d4;
display: none;
font-family: inherit;
border-top: 1px solid #3e3e3e;
font-size: 14px;
}
#command-input {
background: transparent;
border: none;
color: inherit;
outline: none;
font-family: inherit;
font-size: inherit;
width: calc(100% - 20px);
}
.visual-selection {
background: #264f78 !important;
}
`;
this.editor = document.createElement('div');
this.editor.id = 'editor';
this.editor.contentEditable = true;
this.editor.innerText = `Welcome to VimEditor Component
this.editor = document.createElement('div');
this.editor.id = 'editor';
this.editor.contentEditable = true;
this.editor.spellcheck = false;
this.editor.innerText = `Welcome to VimEditor Component
Line 2 here
Another line
Try i, Esc, v, :, yy, dd, 0, $, gg, G, and p`;
this.cmdLine = document.createElement('div');
this.cmdLine.id = 'command-line';
this.shadowRoot.append(style, this.editor, this.cmdLine);
this.cmdLine = document.createElement('div');
this.cmdLine.id = 'command-line';
const cmdPrompt = document.createElement('span');
cmdPrompt.textContent = ':';
this.cmdInput = document.createElement('input');
this.cmdInput.id = 'command-input';
this.cmdInput.type = 'text';
this.cmdLine.append(cmdPrompt, this.cmdInput);
this.mode = 'normal'; // normal | insert | visual | command
this.keyBuffer = '';
this.lastDeletedLine = '';
this.yankedLine = '';
this.statusBar = document.createElement('div');
this.statusBar.id = 'status-bar';
this.modeIndicator = document.createElement('span');
this.modeIndicator.id = 'mode-indicator';
this.modeIndicator.textContent = 'NORMAL';
this.statusBar.appendChild(this.modeIndicator);
this.editor.addEventListener('keydown', this.handleKeydown.bind(this));
}
this.shadowRoot.append(style, this.editor, this.cmdLine, this.statusBar);
connectedCallback() {
this.mode = 'normal';
this.keyBuffer = '';
this.lastDeletedLine = '';
this.yankedLine = '';
this.visualStartOffset = null;
this.visualEndOffset = null;
// Bind event handlers
this.handleKeydown = this.handleKeydown.bind(this);
this.handleCmdKeydown = this.handleCmdKeydown.bind(this);
this.updateVisualSelection = this.updateVisualSelection.bind(this);
this.editor.addEventListener('keydown', this.handleKeydown);
this.cmdInput.addEventListener('keydown', this.handleCmdKeydown);
this.editor.addEventListener('beforeinput', this.handleBeforeInput.bind(this));
}
connectedCallback() {
this.editor.focus();
}
setMode(mode) {
this.mode = mode;
this.modeIndicator.textContent = mode.toUpperCase();
// Update editor classes
this.editor.classList.remove('insert-mode', 'visual-mode', 'normal-mode');
this.editor.classList.add(`${mode}-mode`);
if (mode === 'visual') {
this.visualStartOffset = this.getCaretOffset();
this.editor.addEventListener('selectionchange', this.updateVisualSelection);
} else {
this.clearVisualSelection();
this.editor.removeEventListener('selectionchange', this.updateVisualSelection);
}
if (mode === 'command') {
this.cmdLine.style.display = 'block';
this.cmdInput.value = '';
this.cmdInput.focus();
} else {
this.cmdLine.style.display = 'none';
if (mode !== 'insert') {
// Keep focus on editor for all non-insert modes
this.editor.focus();
}
}
}
getCaretOffset() {
let caretOffset = 0;
const sel = this.shadowRoot.getSelection();
if (!sel || sel.rangeCount === 0) return 0;
updateVisualSelection() {
if (this.mode !== 'visual') return;
this.visualEndOffset = this.getCaretOffset();
}
const range = sel.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(this.editor);
preCaretRange.setEnd(range.endContainer, range.endOffset);
caretOffset = preCaretRange.toString().length;
return caretOffset;
clearVisualSelection() {
const sel = this.shadowRoot.getSelection();
if (sel) sel.removeAllRanges();
this.visualStartOffset = null;
this.visualEndOffset = null;
}
getCaretOffset() {
const sel = this.shadowRoot.getSelection();
if (!sel || sel.rangeCount === 0) return 0;
const range = sel.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(this.editor);
preCaretRange.setEnd(range.endContainer, range.endOffset);
return preCaretRange.toString().length;
}
setCaretOffset(offset) {
const textContent = this.editor.innerText;
offset = Math.max(0, Math.min(offset, textContent.length));
const range = document.createRange();
const sel = this.shadowRoot.getSelection();
const walker = document.createTreeWalker(
this.editor,
NodeFilter.SHOW_TEXT,
null,
false
);
let currentOffset = 0;
let node;
while ((node = walker.nextNode())) {
const nodeLength = node.textContent.length;
if (currentOffset + nodeLength >= offset) {
range.setStart(node, offset - currentOffset);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
return;
}
currentOffset += nodeLength;
}
// If we couldn't find the position, set to end
if (this.editor.lastChild) {
range.selectNodeContents(this.editor.lastChild);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
}
}
setCaretOffset(offset) {
const range = document.createRange();
const sel = this.shadowRoot.getSelection();
const walker = document.createTreeWalker(this.editor, NodeFilter.SHOW_TEXT, null, false);
handleBeforeInput(e) {
if (this.mode !== 'insert') {
e.preventDefault();
}
}
let currentOffset = 0;
let node;
while ((node = walker.nextNode())) {
if (currentOffset + node.length >= offset) {
range.setStart(node, offset - currentOffset);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
return;
}
currentOffset += node.length;
handleCmdKeydown(e) {
if (e.key === 'Enter') {
e.preventDefault();
this.executeCommand(this.cmdInput.value);
this.setMode('normal');
} else if (e.key === 'Escape') {
e.preventDefault();
this.setMode('normal');
}
}
executeCommand(cmd) {
const trimmedCmd = cmd.trim();
// Handle basic vim commands
if (trimmedCmd === 'w' || trimmedCmd === 'write') {
console.log('Save command (not implemented)');
} else if (trimmedCmd === 'q' || trimmedCmd === 'quit') {
console.log('Quit command (not implemented)');
} else if (trimmedCmd === 'wq' || trimmedCmd === 'x') {
console.log('Save and quit command (not implemented)');
} else if (/^\d+$/.test(trimmedCmd)) {
// Go to line number
const lineNum = parseInt(trimmedCmd, 10) - 1;
this.goToLine(lineNum);
}
}
goToLine(lineNum) {
const lines = this.editor.innerText.split('\n');
if (lineNum < 0 || lineNum >= lines.length) return;
let offset = 0;
for (let i = 0; i < lineNum; i++) {
offset += lines[i].length + 1;
}
this.setCaretOffset(offset);
}
getCurrentLineInfo() {
const text = this.editor.innerText;
const caretPos = this.getCaretOffset();
const lines = text.split('\n');
let charCount = 0;
for (let i = 0; i < lines.length; i++) {
if (caretPos <= charCount + lines[i].length) {
return {
lineIndex: i,
lines: lines,
lineStartOffset: charCount,
positionInLine: caretPos - charCount
};
}
charCount += lines[i].length + 1;
}
return {
lineIndex: lines.length - 1,
lines: lines,
lineStartOffset: charCount - lines[lines.length - 1].length - 1,
positionInLine: 0
};
}
handleKeydown(e) {
if (this.mode === 'insert') {
if (e.key === 'Escape') {
e.preventDefault();
this.setMode('normal');
// Move cursor one position left (vim behavior)
const offset = this.getCaretOffset();
if (offset > 0) {
this.setCaretOffset(offset - 1);
}
}
return;
}
handleKeydown(e) {
const key = e.key;
if (this.mode === 'command') {
return; // Command mode input is handled by cmdInput
}
if (this.mode === 'insert') {
if (key === 'Escape') {
e.preventDefault();
this.mode = 'normal';
this.editor.blur();
this.editor.focus();
}
return;
if (this.mode === 'visual') {
if (e.key === 'Escape') {
e.preventDefault();
this.setMode('normal');
return;
}
// Allow movement in visual mode
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
return; // Let default behavior handle selection
}
if (e.key === 'y') {
e.preventDefault();
// Yank selected text
const sel = this.shadowRoot.getSelection();
if (sel && sel.rangeCount > 0) {
this.yankedLine = sel.toString();
}
if (this.mode === 'command') {
if (key === 'Enter' || key === 'Escape') {
e.preventDefault();
this.cmdLine.style.display = 'none';
this.mode = 'normal';
this.keyBuffer = '';
}
return;
}
if (this.mode === 'visual') {
if (key === 'Escape') {
e.preventDefault();
this.mode = 'normal';
}
return;
}
// Handle normal mode
this.keyBuffer += key;
const text = this.editor.innerText;
const caretPos = this.getCaretOffset();
const lines = text.split('\n');
let charCount = 0, lineIdx = 0;
for (let i = 0; i < lines.length; i++) {
if (caretPos <= charCount + lines[i].length) {
lineIdx = i;
break;
}
charCount += lines[i].length + 1;
}
const offsetToLine = idx =>
text.split('\n').slice(0, idx).reduce((acc, l) => acc + l.length + 1, 0);
switch (this.keyBuffer) {
case 'i':
e.preventDefault();
this.mode = 'insert';
this.keyBuffer = '';
break;
case 'v':
e.preventDefault();
this.mode = 'visual';
this.keyBuffer = '';
break;
case ':':
e.preventDefault();
this.mode = 'command';
this.cmdLine.style.display = 'block';
this.cmdLine.textContent = ':';
this.keyBuffer = '';
break;
case 'yy':
e.preventDefault();
this.yankedLine = lines[lineIdx];
this.keyBuffer = '';
break;
case 'dd':
e.preventDefault();
this.lastDeletedLine = lines[lineIdx];
lines.splice(lineIdx, 1);
this.editor.innerText = lines.join('\n');
this.setCaretOffset(offsetToLine(lineIdx));
this.keyBuffer = '';
break;
case 'p':
e.preventDefault();
const lineToPaste = this.yankedLine || this.lastDeletedLine;
if (lineToPaste) {
lines.splice(lineIdx + 1, 0, lineToPaste);
this.editor.innerText = lines.join('\n');
this.setCaretOffset(offsetToLine(lineIdx + 1));
}
this.keyBuffer = '';
break;
case '0':
e.preventDefault();
this.setCaretOffset(offsetToLine(lineIdx));
this.keyBuffer = '';
break;
case '$':
e.preventDefault();
this.setCaretOffset(offsetToLine(lineIdx) + lines[lineIdx].length);
this.keyBuffer = '';
break;
case 'gg':
e.preventDefault();
this.setCaretOffset(0);
this.keyBuffer = '';
break;
case 'G':
e.preventDefault();
this.setCaretOffset(text.length);
this.keyBuffer = '';
break;
case 'Escape':
e.preventDefault();
this.mode = 'normal';
this.keyBuffer = '';
this.cmdLine.style.display = 'none';
break;
default:
// allow up to 2 chars for combos
if (this.keyBuffer.length > 2) this.keyBuffer = '';
break;
this.setMode('normal');
return;
}
if (e.key === 'd' || e.key === 'x') {
e.preventDefault();
// Delete selected text
const sel = this.shadowRoot.getSelection();
if (sel && sel.rangeCount > 0) {
this.lastDeletedLine = sel.toString();
document.execCommand('delete');
}
this.setMode('normal');
return;
}
}
customElements.define('njet-editor', NjetEditor);
export {NjetEditor}
// Normal mode handling
e.preventDefault();
// Special keys that should be handled immediately
if (e.key === 'Escape') {
this.keyBuffer = '';
this.setMode('normal');
return;
}
// Build key buffer for commands
this.keyBuffer += e.key;
const lineInfo = this.getCurrentLineInfo();
const { lineIndex, lines, lineStartOffset, positionInLine } = lineInfo;
// Process commands
switch (this.keyBuffer) {
case 'i':
this.keyBuffer = '';
this.setMode('insert');
break;
case 'a':
this.keyBuffer = '';
this.setCaretOffset(this.getCaretOffset() + 1);
this.setMode('insert');
break;
case 'v':
this.keyBuffer = '';
this.setMode('visual');
break;
case ':':
this.keyBuffer = '';
this.setMode('command');
break;
case 'yy':
this.keyBuffer = '';
this.yankedLine = lines[lineIndex];
break;
case 'dd':
this.keyBuffer = '';
this.lastDeletedLine = lines[lineIndex];
lines.splice(lineIndex, 1);
if (lines.length === 0) lines.push('');
this.editor.innerText = lines.join('\n');
this.setCaretOffset(lineStartOffset);
break;
case 'p':
this.keyBuffer = '';
const lineToPaste = this.yankedLine || this.lastDeletedLine;
if (lineToPaste) {
lines.splice(lineIndex + 1, 0, lineToPaste);
this.editor.innerText = lines.join('\n');
this.setCaretOffset(lineStartOffset + lines[lineIndex].length + 1);
}
break;
case '0':
this.keyBuffer = '';
this.setCaretOffset(lineStartOffset);
break;
case '$':
this.keyBuffer = '';
this.setCaretOffset(lineStartOffset + lines[lineIndex].length);
break;
case 'gg':
this.keyBuffer = '';
this.setCaretOffset(0);
break;
case 'G':
this.keyBuffer = '';
this.setCaretOffset(this.editor.innerText.length);
break;
case 'h':
case 'ArrowLeft':
this.keyBuffer = '';
const currentOffset = this.getCaretOffset();
if (currentOffset > 0) {
this.setCaretOffset(currentOffset - 1);
}
break;
case 'l':
case 'ArrowRight':
this.keyBuffer = '';
this.setCaretOffset(this.getCaretOffset() + 1);
break;
case 'j':
case 'ArrowDown':
this.keyBuffer = '';
if (lineIndex < lines.length - 1) {
const nextLineStart = lineStartOffset + lines[lineIndex].length + 1;
const nextLineLength = lines[lineIndex + 1].length;
const newPosition = Math.min(positionInLine, nextLineLength);
this.setCaretOffset(nextLineStart + newPosition);
}
break;
case 'k':
case 'ArrowUp':
this.keyBuffer = '';
if (lineIndex > 0) {
let prevLineStart = 0;
for (let i = 0; i < lineIndex - 1; i++) {
prevLineStart += lines[i].length + 1;
}
const prevLineLength = lines[lineIndex - 1].length;
const newPosition = Math.min(positionInLine, prevLineLength);
this.setCaretOffset(prevLineStart + newPosition);
}
break;
default:
// Clear buffer if it gets too long or contains invalid sequences
if (this.keyBuffer.length > 2 ||
(this.keyBuffer.length === 2 && !['dd', 'yy', 'gg'].includes(this.keyBuffer))) {
this.keyBuffer = '';
}
break;
}
}
}
customElements.define('njet-editor', NjetEditor);
export { NjetEditor }