working code editor but not yet adding text/code in place

code-editor
Fabien Benetou 1 year ago
parent ebd1fb19ea
commit b0dbc6579d
  1. 410
      index.html

@ -13,6 +13,10 @@
<!--<script type="module" id=immersbundle src="https://cdn.jsdelivr.net/npm/immers-client/dist/destination.bundle.js?role=modFull"></script>-->
<script src="https://threejs.org/examples/js/exporters/GLTFExporter.js"></script>
<script src="https://unpkg.com/shiki"></script>
<!-- for input sharing -->
<script src='dependencies/peerjs.min.js'></script>
<!-- for content sharing, using NAF -->
@ -1221,6 +1225,7 @@ function parseKeys(status, key){
}
}
var keyboardInputTarget = 'hud'
AFRAME.registerComponent('hud', {
init: function(){
var feedbackHUDel= document.createElement("a-troika-text")
@ -1237,9 +1242,11 @@ AFRAME.registerComponent('hud', {
this.el.appendChild( typingHUDel )
hudTextEl = typingHUDel // should rely on the id based selector now
document.addEventListener('keyup', function(event) {
if (keyboardInputTarget != 'hud') return
parseKeys('keyup', event.key)
});
document.addEventListener('keydown', function(event) {
if (keyboardInputTarget != 'hud') return
parseKeys('keydown', event.key)
});
}
@ -2169,13 +2176,403 @@ function addAllPrimitives(){
.map( (i,j) => addPrimitive( i, ""+ j/7 + " 1.4 -0.5" ) )
}
var highlighter
shiki.getHighlighter({ theme: 'monokai', langs: ['javascript' ] }).then(h => highlighter = h )
// see https://github.com/shikijs/shiki/blob/main/docs/languages.md
function highlight(code = `console.log("Here is your code."); var x = 5;`, language='javascript'){
if (!highlighter) return {} // should also warn
const tokens = highlighter.codeToThemedTokens(code, language)
let pos=0
let colorRange={}
tokens.map( line => {
line.map( (t,i) => {
colorRange[pos] = t.color/*.replace("#","0x")*/
pos+=t.content.length
})
pos++
})
return colorRange
}
function startExperience(){
//addCodeEditor(" \n\nqwe qwe qwe qwe qwe qwe qwe qweqweqwe\n\n\na", "")
//addCodeEditor()
fetch("https://fabien.benetou.fr/pub/home/future_of_text_demo/engine/colorme.js").then(r=>r.text()).then( page => { addCodeEditor( page ) })
//fetch("https://fabien.benetou.fr/Tools/Docker?action=source").then(r=>r.text()).then( page => { addCodeEditor( page, "" ) })
if (AFRAME.utils.device.checkHeadsetConnected())
AFRAME.scenes[0].enterVR();
document.querySelector("#snapping-sound").components.sound.playSound();
//document.querySelector("#snapping-sound").components.sound.playSound();
document.querySelector("#mainbutton").style.display = "none"
}
// should consider multiple instances instead
let codeEditor = {
element: null,
line: 0,
page: null,
startWindowRange: 0,
lengthWindowRange: 20,
scrollInterval: null,
currentlyDisplayedText: "",
caret: null
}
function nextLineCodeEditor(lines=1){ // can be negative to scroll up
if (codeEditor.line+lines < 0) return
codeEditor.line+=lines
let content=codeEditor.page.split("\n").slice(codeEditor.line,codeEditor.line+codeEditor.lengthWindowRange).join("\n");
codeEditor.currentlyDisplayedText=content
codeEditor.element.setAttribute("troika-text", {value: content})
codeEditor.element.setAttribute("troika-text", {colorRanges: highlight(content, language='javascript')})
// should respect language set, can be empty
let gutterEl = document.getElementById("leftgutter")
if (gutterEl){
let lineNumbers = "\n"
for (let i=codeEditor.line+1;i<=codeEditor.line+codeEditor.lengthWindowRange;i++){
for (let pad=0;pad<String(getNumberOfLinesFromCodeEditor()).length-String(i).length; pad++)
lineNumbers+="_"
lineNumbers+=i+"\n"
}
gutterEl.setAttribute("troika-text", {value: lineNumbers})
}
let rightGutterEl = document.getElementById("rightgutter")
if (rightGutterEl){
b = rightGutterEl.parentElement.object3D.children[0]._textRenderInfo.blockBounds
w = b[2]-b[0]
h = b[3]-b[1]
let scrollBarHeight = codeEditor.lengthWindowRange/codeEditor.page.match(/\n/g).length * h
let scrollBarVerticalOffset = codeEditor.line/codeEditor.page.match(/\n/g).length * h
if (scrollBarHeight < .1) scrollBarHeight = .1
rightGutterEl.object3D.position.y= h/2-scrollBarHeight/2 - scrollBarVerticalOffset
}
}
function nextPageCodeEditor(){
nextLineCodeEditor(codeEditor.lengthWindowRange)
}
function previousPageCodeEditor(){
nextLineCodeEditor(-codeEditor.lengthWindowRange)
}
function stopScrollCodeEditor(){
codeEditor.scrollInterval = clearInterval( codeEditor.scrollInterval )
}
function startScrollCodeEditor(){
if (!codeEditor.scrollInterval) codeEditor.scrollInterval = setInterval( _ => nextLineCodeEditor(), 100)
}
function highlightAllOccurences(keyword="function"){
let indices = []
let lastfound = codeEditor.currentlyDisplayedText.indexOf(keyword,0)
while (lastfound>-1) {
indices.push(lastfound)
lastfound = codeEditor.currentlyDisplayedText.indexOf(keyword,lastfound+keyword.length)
}
indices.map( pos => {
let offset = (codeEditor.currentlyDisplayedText.slice(0,pos).match(/[\n\t ]/g)||[]).length
pos-=offset
highlightString(pos, keyword.length)
})
}
function hightlightNextKeyword(keyword="function"){
let pos = codeEditor.currentlyDisplayedText.indexOf(keyword)
// invisible characters... some still left
let offset = (codeEditor.currentlyDisplayedText.slice(0,pos).match(/[\n\t ]/g)||[]).length
pos-=offset
highlightString(pos, keyword.length)
}
function highlightString(pos, length){
for (let c=pos;c<pos+length;c++) highlightChar( c )
}
// WARNING this is limited to visible characters, i.e not " " or "\t" or "\n"
// should instead allow to highlight " " and "\t" both looking the same
function highlightChar(pos=0, name=null){ // could have multiple selection
let b = Array.from( codeEditor.element.object3D.children[0].geometry.attributes.aTroikaGlyphBounds.array ).slice(pos*4,pos*4+4)
let w = b[2]-b[0]
let h = b[3]-b[1] // could be used to check for same line, if so could make a single block from beginning to end
let g = new THREE.BoxGeometry( w, h, .01 );
let m = new THREE.MeshBasicMaterial( {color: 0xffffff, opacity: 0.8, transparent: true} );
let c = new THREE.Mesh( g, m )
if (name) c.name = name
codeEditor.element.object3D.add( c );
c.position.x= b[0]+w/2
c.position.y= b[1]+h/2
c.position.z= .01
}
function highlightUnderChar(pos=0, name=null){
let b = Array.from( codeEditor.element.object3D.children[0].geometry.attributes.aTroikaGlyphBounds.array ).slice(pos*4,pos*4+4)
// note that this skips invisible char and thus desync codeEditor.caret from actual position
// but, for now at least, " " and "\t" seems to be of equal value and "\n" does not shift on the current line
// currently we could count the invisible moving ones on this line and negative offset horizontally
let currentLineNumber = (codeEditor.currentlyDisplayedText.slice(0, pos ).match(/\n/g)||[]).length
let currentPositionOnLine = codeEditor.currentlyDisplayedText.slice(0, pos ).length
- codeEditor.currentlyDisplayedText.split("\n").slice(0,currentLineNumber).join("\n").length
console.log(currentPositionOnLine)
// might need a special case for the beginning of the line... or rather maybe the position here is different, it's the number of the glyph
// e.g " x" x is not 3 but 0
// we could also count then from the string but we don't have that, just pos as parameter. Some extra information might be needed.
let currentLineContent = codeEditor.currentlyDisplayedText.split("\n")[currentLineNumber].slice(0, currentPositionOnLine)
let invisibleOnCurrentLine = (currentLineContent.slice(0, currentPositionOnLine).match(/[\t ]/g)||[]).length
const spaceSize = .046 // hardcoded but changes per front so should be measured instead
// can be done via temptroikaobject.element.object3D.children[0].geometry.attributes.aTroikaGlyphBounds.array[0] for value " _"
// as a kind of calibration
let w = b[2]-b[0]
let h = b[3]-b[1] // could be used to check for same line, if so could make a single block from beginning to end
//console.log(invisibleOnCurrentLine, currentLineContent)
let g = new THREE.BoxGeometry( w, .01, .01 );
let m = new THREE.MeshBasicMaterial( {color: 0xffffff, opacity: 0.8, transparent: true} );
let c = new THREE.Mesh( g, m )
if (name) c.name = name
codeEditor.element.object3D.add( c );
c.position.x= b[0]+w/2-invisibleOnCurrentLine*spaceSize
c.position.y= b[1]-.01
c.position.z= .01
}
function moveCaretToNextVisibleChar(){
addCaretToCodeEditor( ++codeEditor.caret )
// might be able to reach non visible one by remove an offset
}
function addCaretToCodeEditor(pos=0){
if (codeEditor.caret) removeCaretFromCodeEditor()
highlightUnderChar(pos, "caret")
codeEditor.caret = pos
}
function removeCaretFromCodeEditor(){
codeEditor.element.object3D.getObjectByName("caret").removeFromParent()
}
function clearCodeEditorContent(){
updateCodeEditorWithContent( "" )
}
// should support a range, note the entire document (or window?)
function searchAndReplaceInCodeEditor(before, after){
updateCodeEditorWithContent( codeEditor.currentlyDisplayedText.replaceAll(before, after))
// note that it desyncs from page so page should only be seen as the initial source
// this though would break scrolling which is based on page
// consequently page should be modified
}
function updateCodeEditorWithContent(content){
if (!codeEditor.element) return
codeEditor.currentlyDisplayedText=content
codeEditor.element.setAttribute("troika-text", {value: content})
codeEditor.element.setAttribute("troika-text", {colorRanges: highlight(content, language='javascript')})
// should respect language set, can be empty
}
function addBackdropToTroikaElement( el ){
el.addEventListener("object3dset", e => {
el.object3D.children[0].addEventListener("synccomplete", e => {
// this can be used for resizing but without add the element
if (document.getElementById("leftgutter")) return // already added, should unregister
b = el.object3D.children[0]._textRenderInfo.blockBounds
w = b[2]-b[0]
h = b[3]-b[1]
g = new THREE.BoxGeometry( w, h, .01 );
m = new THREE.MeshBasicMaterial( {color: 0, opacity: 0.9, transparent: true} );
c = new THREE.Mesh( g, m );
el.object3D.add( c );
c.name = "backdrop"
c.position.z=-.01
c.position.x= w/2
})
})
}
function addGuttersToTroikaElement( el ){
el.addEventListener("object3dset", e => {
el.object3D.children[0].addEventListener("synccomplete", e => {
if (document.getElementById("leftgutter")) return
// already added, should unregister, can be removed to allow dynamic resizing BUT should skip adding element
b = el.object3D.children[0]._textRenderInfo.blockBounds
w = b[2]-b[0]
h = b[3]-b[1]
gutterWidth = .2 * String(getNumberOfLinesFromCodeEditor()).length
//should adjust width based on number of lines in total first
g = new THREE.BoxGeometry( gutterWidth, h, .01 );
m = new THREE.MeshBasicMaterial( {color: 0x333333, opacity: 0.9, transparent: true} );
c = new THREE.Mesh( g, m );
el.object3D.add( c );
c.name = "leftgutter"
c.position.z=-.01
c.position.x= -gutterWidth/2
//c.rotation.y= .2 // looks nice but have to consider text on top first, could apply rotation to text too
var leftGutter = document.createElement("a-troika-text")
leftGutter.setAttribute("anchor", "left" )
leftGutter.setAttribute("outline-width", "5%" )
leftGutter.setAttribute("outline-color", "black" )
let lineNumbers = "\n"
for (let i=codeEditor.line+1;i<=codeEditor.line+codeEditor.lengthWindowRange;i++){
for (let pad=0;pad<String(getNumberOfLinesFromCodeEditor()).length-String(i).length; pad++)
lineNumbers+="_" // not using a fixed width font now so " " is smaller
lineNumbers+=i+"\n"
}
lineNumbers.slice(0,-1)
leftGutter.setAttribute("troika-text", {value: lineNumbers})
leftGutter.setAttribute("troika-text", {textIndent: -.5})
leftGutter.id = "leftgutter"
codeEditor.element.appendChild( leftGutter )
// should be updated when scrolling
gutterWidth = .1
g = new THREE.BoxGeometry( gutterWidth, h, .01 );
m = new THREE.MeshBasicMaterial( {color: 0x333333, opacity: 0.9, transparent: true} );
c = new THREE.Mesh( g, m );
el.object3D.add( c );
c.name = "rightgutter"
c.position.z=-.01
c.position.x= w+gutterWidth/2
//c.rotation.y= -.2 // looks nice but have to consider text on top first
var rightGutter = document.createElement("a-cylinder")
// height proportional to the visible content to the terminal size
let scrollBarHeight = codeEditor.lengthWindowRange/getNumberOfLinesFromCodeEditor() * h
let scrollBarVerticalOffset = codeEditor.line/getNumberOfLinesFromCodeEditor() * h
if (scrollBarHeight < .1) scrollBarHeight = .1
rightGutter.setAttribute("height", scrollBarHeight )
rightGutter.setAttribute("radius", .01 )
rightGutter.id = "rightgutter"
// should become a constrained target (moving only on y axis and clamped)
codeEditor.element.appendChild( rightGutter )
// so... rightgutter vs rightGutter ... somehow changing to the "correct" one breaks the editor itself (?!)
rightgutter.object3D.position.x= w+gutterWidth/2
rightgutter.object3D.position.y= h/2-scrollBarHeight/2 - scrollBarVerticalOffset
// offset by line position proportional also then updated when scrolling
gutterHeight = .3
g = new THREE.BoxGeometry( w, gutterHeight, .01 );
m = new THREE.MeshBasicMaterial( {color: 0x333333, opacity: 0.9, transparent: true} );
c = new THREE.Mesh( g, m );
el.object3D.add( c );
c.name = "middlegutter"
c.position.z=-.01
c.position.y= -h/2-gutterHeight/2
c.position.x= w/2
//c.rotation.x= -.2 // looks nice but have to consider text on top first
// should add the commands here
var middleGutter = document.createElement("a-troika-text")
middleGutter.setAttribute("anchor", "left" )
middleGutter.setAttribute("outline-width", "5%" )
middleGutter.setAttribute("outline-color", "black" )
middleGutter.setAttribute("troika-text", {value: ":(will add commands here)"})
//middleGutter.setAttribute("troika-text", {textIndent: -.3})
middleGutter.id = "middlegutter"
codeEditor.element.appendChild( middleGutter )
middleGutter.object3D.position.y= -h/2-gutterHeight/2
// should disable the overlay first, see parseKeys
// see listeners in 'hud'
let enteringCommand = false
document.addEventListener('keydown', function(event) {
if (keyboardInputTarget != 'codeeditor') return
if (event.key == ":"){
enteringCommand = true
//let middlegutter = document.getElementById("middlegutter")
middleGutter.setAttribute("troika-text", {value: ":(started typing command)"})
// should add text here until esc or enter is pressed
} else if (enteringCommand) {
if (event.key == "Escape"){
enteringCommand = false
middleGutter.setAttribute("troika-text", {value: "(cancel, ready to receive new command)"})
} else if (event.key == "Enter"){
enteringCommand = false
middleGutter.setAttribute("troika-text", {value: "(executed, ready to receive new command)"})
// could rely only on searchAndReplaceInCodeEditor(before, after) for now
// which BTW should support a range, note the entire document (or window?)
} else {
middleGutter.setAttribute("troika-text", {value:
middleGutter.getAttribute("troika-text").value
+ event.key})
}
}
});
})
})
}
function getNumberOfLinesFromCodeEditor(){
let newLines = codeEditor.page.match(/\n/g)
if (!newLines) return 1 // undefined or 0
return newLines.length
}
// add jxr command on top of the editor e.g "jxr focusCodeEditor()" which would replace keyboard input
// switching keyboardInputTarget to 'codeeditor' then to 'hud' when done
// should also support clipboard or even a more direct way to have impact
// could save remotely (e.g wiki) or locally in localStorage
function addCodeEditor(page="jxr console.log('hello world')", language="javascript", position="-.5 1.6 -.7", name="codeditor" ){
// could also add empty but with column and row for sizing
// for now supporting only 1 code editor, despite the name parameter
codeEditor.page = page
codeEditor.line = codeEditor.startWindowRange
let numberOfLines = getNumberOfLinesFromCodeEditor()
if (numberOfLines<codeEditor.lengthWindowRange) codeEditor.lengthWindowRange = numberOfLines
content=codeEditor.page.split("\n").slice(codeEditor.line,codeEditor.line+codeEditor.lengthWindowRange).join("\n");
codeEditor.currentlyDisplayedText=content
if (!codeEditor.element) codeEditor.element = addNewNote(content, position, "0.1 0.1 0.1", name, "tool")
codeEditor.element.setAttribute("troika-text", {value: content})
codeEditor.element.setAttribute("troika-text", {depthOffset: .1})
if (language?.length > 0 && language != "none") codeEditor.element.setAttribute("troika-text", {colorRanges: highlight(content, language)})
// shouldn't set colorRange if the result is {} which is the case when shiki highlighter isn't ready or available
addBackdropToTroikaElement( codeEditor.element )
addGuttersToTroikaElement( codeEditor.element )
let scrollbarPicked = false
let previousPosition
let p = document.querySelector('[pinchprimary]')
let target = new THREE.Vector3(); // create once an reuse it
p.addEventListener('pinchended', pinchPrimaryScrollbarEnded );
function pinchPrimaryScrollbarEnded(event){
//p.removeEventListener('pinchended', pinchPrimaryScrollbarEnded)
//p.removeEventListener('pinchmoved', pinchPrimaryScrollbarMoved)
//p.removeEventListener('pinchstarted', pinchPrimaryScrollbarStarted)
if (!scrollbarPicked) return
scrollbarPicked = false
}
p.addEventListener('pinchmoved', pinchPrimaryScrollbarMoved );
function pinchPrimaryScrollbarMoved(event){
if (!scrollbarPicked) return
if (previousPosition.y>event.detail.position.y)
nextLineCodeEditor(1)
else
nextLineCodeEditor(-1)
previousPosition = event.detail.position.clone()
}
p.addEventListener('pinchstarted', pinchPrimaryScrollbarStarted );
function pinchPrimaryScrollbarStarted(event){
let rightGutterEl = document.getElementById("rightgutter")
previousPosition = event.detail.position.clone()
rightGutterEl.object3D.getWorldPosition( target );
if (previousPosition.distanceTo(target)<0.1) scrollbarPicked = true
}
return codeEditor.element
}
// could change model opacity based on hand position, fading out when within a (very small here) safe space
function removeOutlineFromEntity( el ){
@ -2501,6 +2898,7 @@ function makeAnchorsVisibleOnTargets(){
}
function startMesher(){
// consider preview triangle from primary moved
let meshPoints = []
let meshTriangles = []
let offset
@ -2512,7 +2910,8 @@ function startMesher(){
elSecondary.addEventListener('pinchended', endedSecondary );
function endedSecondary(){
targets.push(meshEl)
meshEl.setAttribute('dynamic-body', "")
meshEl.setAttribute('dynamic-body', "shape:hull")
// using 'dynamic-unless-picked' crashes the browser
//makeAnchorsVisibleOnTargets() // too large here
applyToClass("meshvertex", (e, val ) => e.setAttribute("visible", val), "false")
el.removeEventListener('pinchended', end)
@ -2587,7 +2986,7 @@ AFRAME.registerComponent('startfunctions', {
//doublePinchToScale()
//emptyPinchToMove()
//makeAnchorsVisibleOnTargets()
startMesher()
//startMesher()
}
})
@ -2668,4 +3067,9 @@ AFRAME.registerComponent('startfunctions', {
</a-scene>
</body>
<script>
setTimeout( _=>startExperience(), 1000)
// should wait for shiki instead but faster to test on desktop like that
// should actually display without highlight first and add as callback when ready
</script>
</html>

Loading…
Cancel
Save