aboutsummaryrefslogtreecommitdiff
path: root/modules/ext/editor.js
blob: 07205b97d3bda3af54d1cac1f6aaf5bacce0c204 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
class EditorModule {
    constructor() {
        this.commands = {};
        this.terminal = null;
        this._vfsPath = null;
    }

    requirements() {
        return { met: true, missing: [] };
    }

    getHelpInfo() {
        return {
            'edit': 'Open the code editor for a VFS file (usage: edit <path>)',
        };
    }
    
    registerCommands() {
        this.commands['edit'] = (args) => this.edit(args);
    }
    edit(args) {
        if (!args[0]) return this.terminal.print('Usage: edit <path>', 'warning');
        const path = args.join(' ');
        const modal = document.getElementById('code-editor');
        const title = document.getElementById('code-editor-title');
        const textarea = document.getElementById('code-editor-textarea');
        const gutter = document.getElementById('code-editor-gutter');
        const statusEl = document.getElementById('code-editor-status');
        if (!modal || !textarea) return this.terminal.print('Editor modal not found', 'error');
        // Read existing file content or use empty for new files
        const content = this.terminal.fs.readFile(path);
        const fileContent = (content !== null && content !== false) ? content : '';
        // Set up modal
        this._vfsPath = path;
        title.textContent = '📝 Editing: ' + path;
        textarea.value = fileContent;
        modal.style.display = 'flex';
        textarea.focus();
        // Editor enhancements: line numbers, Tab, Ctrl+S
        const updateGutter = () => {
            const lines = textarea.value.split('\n').length;
            let html = '';
            for (let i = 1; i <= lines; i++) html += i + '\n';
            gutter.textContent = '';
            gutter.textContent = html;
        };
        const syncScroll = () => { gutter.scrollTop = textarea.scrollTop; };
        const updateStatus = () => {
            const lines = textarea.value.split('\n').length;
            const chars = textarea.value.length;
            statusEl.textContent = `${lines} lines · ${chars} chars`;
        };
        updateGutter();
        updateStatus();
        textarea.onscroll = syncScroll;
        textarea.oninput = () => { updateGutter(); updateStatus(); };
        textarea.onkeydown = (e) => {
            if (e.key === 'Tab') { e.preventDefault(); insertTab(textarea, e.shiftKey); updateGutter(); }
            if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); saveBtn.click(); }
        };
        function insertTab(el, shift) {
            const s = el.selectionStart, e2 = el.selectionEnd;
            if (shift) {
                const before = el.value.substring(0, s);
                const lineStart = before.lastIndexOf('\n') + 1;
                if (lineStart < s && el.value.substring(lineStart, lineStart + 1) === '\t') {
                    el.value = el.value.substring(0, lineStart) + el.value.substring(lineStart + 1);
                    el.selectionStart = el.selectionEnd = s - 1;
                }
            } else {
                el.value = el.value.substring(0, s) + '\t' + el.value.substring(e2);
                el.selectionStart = el.selectionEnd = s + 1;
            }
        }
        // Fullscreen toggle
        window._editorToggleFullscreen = () => {
            const content = modal.querySelector('.modal-content');
            if (content.style.maxWidth === '100vw') {
                content.style.maxWidth = '90vw'; content.style.maxHeight = '90vh'; content.style.width = '900px'; content.style.height = '80vh';
            } else {
                content.style.maxWidth = '100vw'; content.style.maxHeight = '100vh'; content.style.width = '100vw'; content.style.height = '100vh';
            }
        };
        // Override save button to write back to VFS
        const saveBtn = modal.querySelector('.modal-footer .btn:last-child');
        const cancelBtn = modal.querySelector('.modal-footer .btn-danger');
        if (saveBtn) {
            saveBtn._origOnclick = saveBtn.onclick;
            saveBtn.onclick = async () => {
                const code = textarea.value;
                const ok = this.terminal.fs.writeFile(this._vfsPath, code);
                if (ok) {
                    this.terminal.print('✅ Saved to ' + this._vfsPath, 'success');
                    // Mirror to _mods/ for test_module compatibility
                    const modMatch = this._vfsPath.match(/(?:^|\/)(\w+)\.js$/);
                    if (modMatch) {
                        const modName = modMatch[1];
                        this.terminal.fs.mkdirRecursive('_mods');
                        this.terminal.fs.writeFile('_mods/' + modName + '.js', code);
                        if (!this.terminal.fs.readFile('_mods/' + modName + '.orig.js')) {
                            this.terminal.fs.writeFile('_mods/' + modName + '.orig.js', 'placeholder');
                        }
                        this.terminal.print('📋 Mirrored to _mods/' + modName + '.js for live testing', 'info');
                    }
                    // Notify tutorial module for quest 4 (edit + save)
                    if (this.terminal.tutorialModule) {
                        this.terminal.tutorialModule.onFileSaved(this._vfsPath);
                    }
                } else {
                    this.terminal.print('❌ Failed to save ' + this._vfsPath, 'error');
                }
                modal.style.display = 'none';
                this._vfsPath = null;
                // Restore original onclick handler
                if (saveBtn._origOnclick) saveBtn.onclick = saveBtn._origOnclick;
                delete saveBtn._origOnclick;
            };
        }
        if (cancelBtn) {
            cancelBtn._origOnclick = cancelBtn.onclick;
            cancelBtn.onclick = () => {
                modal.style.display = 'none';
                this._vfsPath = null;
                if (cancelBtn._origOnclick) cancelBtn.onclick = cancelBtn._origOnclick;
                delete cancelBtn._origOnclick;
            };
        }
    }
}
if (typeof window.webx11 !== 'undefined' && window.webx11) {
    webx11.moduleManager.registerModule(new EditorModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/editor.js');
}