Durchgaengiger Live-Editor fuer Code-Projekte: - App: selbstenthaltener Highlight-Editor in einer WebView (editorHtml.ts, Textarea + Regex-Highlight-Layer, voll offline). CodeEditorTile mit Datei-Tabs, verdrahtet mit dem codeFile-Spiegel; Bridge-Protokoll setContent/applyPatch/ setReadOnly ↔ onEditFromUser. - Proxy-Hook (routes.js): faengt ARIAs Write/Edit/MultiEdit unter /shared/projects/<pid>/ ab und postet den Volltext bei Erfolg an /internal/code-file. tool_use_id→file_path-Korrelation, liest die Datei aus dem gemounteten /shared. - Bridge: /internal/code-file relayt als RVS code_file an die App; eingehende code_file_edit schreiben den Volltext pfad-sicher nach /shared/projects/<pid>/ (_write_project_file, kein Ausbruch via ..). Arbeitsverzeichnis fuer Code-Projekte = /shared/projects/<projectId>/ (in proxy/ bridge/brain gemountet, kein SSH noetig). tsc/py/js clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
168 lines
6.5 KiB
TypeScript
168 lines
6.5 KiB
TypeScript
/**
|
|
* editorHtml — selbstenthaltener Live-Code-Editor fuer die WebView (offline,
|
|
* kein CDN/Bundler). Eine transparente <textarea> ueber einer <pre>-Highlight-
|
|
* Ebene: man sieht Syntax-Highlighting UND kann tippen. Bewusst leichtgewichtig
|
|
* (Regex-Highlighter fuer C-artige/JS/Python/Shell), damit es ohne Build-Schritt
|
|
* inline passt.
|
|
*
|
|
* Bridge-Protokoll:
|
|
* RN -> WebView window.ariaBridge.onMessage(jsonString):
|
|
* {cmd:'setContent', content, language, version}
|
|
* {cmd:'applyPatch', from, to, insert, version}
|
|
* {cmd:'setLanguage', language}
|
|
* {cmd:'setReadOnly', value}
|
|
* WebView -> RN window.ReactNativeWebView.postMessage(jsonString):
|
|
* {event:'ready'}
|
|
* {event:'onEditFromUser', from, to, insert, fullText, version}
|
|
*/
|
|
|
|
export const EDITOR_HTML = `<!doctype html><html><head><meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
html, body { height: 100%; background: #0D0D1A; }
|
|
#wrap { position: relative; height: 100%; width: 100%; }
|
|
#hl, #ed {
|
|
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
|
margin: 0; border: 0; padding: 10px 12px;
|
|
font-family: 'Courier New', monospace; font-size: 13px; line-height: 1.45;
|
|
white-space: pre; word-wrap: normal; overflow: auto; tab-size: 2;
|
|
}
|
|
#hl { color: #C8C8E0; z-index: 1; pointer-events: none; }
|
|
#ed {
|
|
z-index: 2; color: transparent; background: transparent; caret-color: #0096FF;
|
|
resize: none; outline: none;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
#ed::selection { background: rgba(0,150,255,0.3); }
|
|
.tok-cmt { color: #6A7A6A; font-style: italic; }
|
|
.tok-str { color: #C6A972; }
|
|
.tok-num { color: #B58BE0; }
|
|
.tok-kw { color: #4F9CE8; font-weight: bold; }
|
|
</style></head><body>
|
|
<div id="wrap">
|
|
<pre id="hl"></pre>
|
|
<textarea id="ed" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
|
</div>
|
|
<script>
|
|
(function(){
|
|
var ed = document.getElementById('ed');
|
|
var hl = document.getElementById('hl');
|
|
var lang = 'text';
|
|
var version = 0;
|
|
var lastValue = '';
|
|
var applyingProgrammatic = false;
|
|
|
|
var KW = {
|
|
common: ['if','else','for','while','do','return','break','continue','switch','case','default','function','var','let','const','class','new','this','import','from','export','try','catch','finally','throw','typeof','instanceof','void','delete','in','of','yield','async','await','def','elif','end','then','fi','esac','local','echo','extends','implements','interface','public','private','protected','static','struct','enum','include','define','null','true','false','undefined','None','True','False','print','with','as','pass','lambda','not','and','or','is']
|
|
};
|
|
|
|
function esc(s){ return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
|
|
function highlight(code){
|
|
// Token-Scan: Kommentare, Strings, Zahlen, Keywords. Bewusst simpel.
|
|
var out = '';
|
|
var i = 0, n = code.length;
|
|
var kwRe = /[A-Za-z_][A-Za-z0-9_]*/;
|
|
while(i < n){
|
|
var c = code[i];
|
|
var two = code.substr(i,2);
|
|
// Zeilenkommentar // oder #
|
|
if(two === '//' || (c === '#')){
|
|
var j = code.indexOf('\\n', i); if(j<0) j=n;
|
|
out += '<span class="tok-cmt">'+esc(code.slice(i,j))+'</span>'; i=j; continue;
|
|
}
|
|
// Blockkommentar
|
|
if(two === '/*'){
|
|
var k = code.indexOf('*/', i+2); k = (k<0)? n : k+2;
|
|
out += '<span class="tok-cmt">'+esc(code.slice(i,k))+'</span>'; i=k; continue;
|
|
}
|
|
// Strings
|
|
if(c === '"' || c === "'" || c === '\`'){
|
|
var q=c, m=i+1;
|
|
while(m<n){ if(code[m]==='\\\\'){m+=2;continue;} if(code[m]===q){m++;break;} m++; }
|
|
out += '<span class="tok-str">'+esc(code.slice(i,m))+'</span>'; i=m; continue;
|
|
}
|
|
// Zahl
|
|
if(c>='0' && c<='9'){
|
|
var p=i+1; while(p<n && /[0-9a-fA-F.xX_]/.test(code[p])) p++;
|
|
out += '<span class="tok-num">'+esc(code.slice(i,p))+'</span>'; i=p; continue;
|
|
}
|
|
// Wort / Keyword
|
|
if(/[A-Za-z_]/.test(c)){
|
|
var rest = code.slice(i);
|
|
var mm = rest.match(kwRe);
|
|
var w = mm[0];
|
|
if(KW.common.indexOf(w) >= 0){ out += '<span class="tok-kw">'+esc(w)+'</span>'; }
|
|
else { out += esc(w); }
|
|
i += w.length; continue;
|
|
}
|
|
out += esc(c); i++;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function render(){
|
|
hl.innerHTML = highlight(ed.value) + '\\n';
|
|
hl.scrollTop = ed.scrollTop; hl.scrollLeft = ed.scrollLeft;
|
|
}
|
|
|
|
function post(obj){ if(window.ReactNativeWebView) window.ReactNativeWebView.postMessage(JSON.stringify(obj)); }
|
|
|
|
// Minimalen Diff (gemeinsamer Prefix/Suffix) zwischen alt und neu.
|
|
function diff(a, b){
|
|
var s = 0; var maxS = Math.min(a.length, b.length);
|
|
while(s < maxS && a[s] === b[s]) s++;
|
|
var e = 0;
|
|
while(e < (maxS - s) && a[a.length-1-e] === b[b.length-1-e]) e++;
|
|
return { from: s, to: a.length - e, insert: b.slice(s, b.length - e) };
|
|
}
|
|
|
|
var editTimer = null;
|
|
ed.addEventListener('input', function(){
|
|
render();
|
|
if(applyingProgrammatic) return;
|
|
if(editTimer) clearTimeout(editTimer);
|
|
editTimer = setTimeout(function(){
|
|
var nv = ed.value;
|
|
var d = diff(lastValue, nv);
|
|
lastValue = nv; version++;
|
|
post({ event:'onEditFromUser', from:d.from, to:d.to, insert:d.insert, fullText:nv, version:version });
|
|
}, 160);
|
|
});
|
|
ed.addEventListener('scroll', function(){ hl.scrollTop=ed.scrollTop; hl.scrollLeft=ed.scrollLeft; });
|
|
|
|
window.ariaBridge = {
|
|
onMessage: function(json){
|
|
var m; try { m = JSON.parse(json); } catch(e){ return; }
|
|
if(m.cmd === 'setContent'){
|
|
applyingProgrammatic = true;
|
|
ed.value = m.content || '';
|
|
lastValue = ed.value;
|
|
if(typeof m.version === 'number') version = m.version;
|
|
if(m.language) lang = m.language;
|
|
render();
|
|
applyingProgrammatic = false;
|
|
} else if(m.cmd === 'applyPatch'){
|
|
applyingProgrammatic = true;
|
|
var v = ed.value;
|
|
var from = Math.max(0, Math.min(m.from, v.length));
|
|
var to = Math.max(from, Math.min(m.to, v.length));
|
|
ed.value = v.slice(0, from) + (m.insert||'') + v.slice(to);
|
|
lastValue = ed.value;
|
|
if(typeof m.version === 'number') version = m.version;
|
|
render();
|
|
applyingProgrammatic = false;
|
|
} else if(m.cmd === 'setLanguage'){
|
|
lang = m.language || 'text'; render();
|
|
} else if(m.cmd === 'setReadOnly'){
|
|
ed.readOnly = !!m.value;
|
|
}
|
|
}
|
|
};
|
|
|
|
render();
|
|
post({ event:'ready' });
|
|
})();
|
|
</script></body></html>`;
|