Преглед изворни кода

Editor: Rework auto-completion in script editor. (#33711)

Michael Herzog пре 1 недеља
родитељ
комит
274f9712ce
4 измењених фајлова са 264 додато и 271 уклоњено
  1. 0 1
      editor/index.html
  2. 31 2
      editor/js/Script.js
  3. 233 0
      editor/js/libs/tern-threejs/build-defs.js
  4. 0 268
      editor/js/libs/tern-threejs/threejs.js

+ 0 - 1
editor/index.html

@@ -54,7 +54,6 @@
 		<script src="js/libs/ternjs/comment.js"></script>
 		<script src="js/libs/ternjs/infer.js"></script>
 		<script src="js/libs/ternjs/doc_comment.js"></script>
-		<script src="js/libs/tern-threejs/threejs.js"></script>
 		<script src="js/libs/signals.min.js"></script>
 
 		<script type="module">

+ 31 - 2
editor/js/Script.js

@@ -3,6 +3,8 @@ import { UIElement, UIPanel, UIText } from './libs/ui.js';
 import { SetScriptValueCommand } from './commands/SetScriptValueCommand.js';
 import { SetMaterialValueCommand } from './commands/SetMaterialValueCommand.js';
 
+import buildThreeDefs from './libs/tern-threejs/build-defs.js';
+
 function Script( editor ) {
 
 	const signals = editor.signals;
@@ -291,10 +293,35 @@ function Script( editor ) {
 	// tern js autocomplete
 
 	const server = new CodeMirror.TernServer( {
-		caseInsensitive: true,
-		plugins: { threejs: null }
+		caseInsensitive: true
 	} );
 
+	// The three.js API definitions are built lazily from the JSDoc in the library
+	// build the first time the script editor is opened.
+
+	let threeDefsRequested = false;
+
+	async function loadThreeDefs() {
+
+		if ( threeDefsRequested ) return;
+		threeDefsRequested = true;
+
+		try {
+
+			const url = new URL( '../build/three.core.js', document.baseURI ).href;
+			const source = await ( await fetch( url ) ).text();
+
+			server.server.defs.push( buildThreeDefs( source ) );
+			server.server.reset();
+
+		} catch ( error ) {
+
+			console.warn( 'Script: Failed to build three.js autocomplete defs.', error );
+
+		}
+
+	}
+
 	codemirror.setOption( 'extraKeys', {
 		'Ctrl-Space': function ( cm ) {
 
@@ -448,6 +475,8 @@ function Script( editor ) {
 		currentScript = script;
 		currentObject = object;
 
+		if ( mode === 'javascript' ) loadThreeDefs();
+
 		container.setDisplay( '' );
 		codemirror.setValue( source );
 		codemirror.clearHistory();

+ 233 - 0
editor/js/libs/tern-threejs/build-defs.js

@@ -0,0 +1,233 @@
+// Builds Tern definitions for the three.js API by scanning the JSDoc comments
+// in a three.js build file (e.g. build/three.core.js).
+//
+// The build output is machine-formatted (one declaration per line, tab
+// indentation, every documented symbol preceded by a `/** ... */` block), so a
+// lightweight line scanner is enough — no full JS parser is required.
+//
+// Usage:  const defs = buildDefs( sourceText );
+//
+// `defs` is a Tern definitions object of the shape consumed by the editor's
+// TernServer:  { "!name": "threejs", "THREE": { Box3: { prototype: { ... } } } }
+
+// --- JSDoc type -> Tern type ------------------------------------------------
+
+function mapType( raw, classes ) {
+
+	if ( ! raw ) return null;
+
+	let type = raw.trim();
+
+	// strip a single leading/trailing brace pair if present
+	type = type.replace( /^\{/, '' ).replace( /\}$/, '' ).trim();
+
+	// unions: Tern has no union type, take the first member
+	if ( type.includes( '|' ) ) type = type.split( '|' )[ 0 ].trim();
+
+	// nullable / non-nullable / rest markers
+	type = type.replace( /^[?!]/, '' ).replace( /^\.\.\./, '' ).trim();
+
+	// Array forms: Array<X>, Array.<X>, X[]
+	let m = type.match( /^Array\.?<(.+)>$/ );
+	if ( m ) return '[' + ( mapType( m[ 1 ], classes ) || '?' ) + ']';
+	m = type.match( /^(.+)\[\]$/ );
+	if ( m ) return '[' + ( mapType( m[ 1 ], classes ) || '?' ) + ']';
+
+	switch ( type ) {
+
+		case 'number': case 'string': case 'boolean': return type;
+		case 'function': case 'Function': return 'fn()';
+		case '*': case 'any': case 'Object': case 'object':
+		case 'undefined': case 'null': case 'void': return '?';
+
+	}
+
+	if ( classes.has( type ) ) return '+THREE.' + type;
+
+	return '?'; // unknown (TypedArray, external types, generics, …)
+
+}
+
+// --- JSDoc block parser -----------------------------------------------------
+
+function parseDoc( lines ) {
+
+	const params = [];
+	let returns = null, atType = null, readonly = false;
+	const desc = [];
+
+	for ( let raw of lines ) {
+
+		const line = raw.replace( /^\s*\*?\s?/, '' ); // strip ` * `
+
+		const pm = line.match( /^@param\s+(\{[^}]*\})?\s*\[?([\w.]+)/ );
+		if ( pm ) { params.push( { type: pm[ 1 ] || null, name: pm[ 2 ].split( '.' )[ 0 ] } ); continue; }
+
+		const rm = line.match( /^@returns?\s+(\{[^}]*\})/ );
+		if ( rm ) { returns = rm[ 1 ]; continue; }
+
+		const tm = line.match( /^@type\s+(\{[^}]*\})/ );
+		if ( tm ) { atType = tm[ 1 ]; continue; }
+
+		if ( /^@readonly/.test( line ) ) { readonly = true; continue; }
+		if ( /^@/.test( line ) ) continue; // other tags ignored
+
+		if ( line.trim() ) desc.push( line.trim() );
+
+	}
+
+	// de-duplicate rest params that share a base name
+	const seen = new Set(), uniqueParams = [];
+	for ( const p of params ) if ( ! seen.has( p.name ) ) { seen.add( p.name ); uniqueParams.push( p ); }
+
+	return { params: uniqueParams, returns, atType, readonly, doc: desc.join( ' ' ) };
+
+}
+
+function fnType( doc, classes ) {
+
+	const args = doc.params.map( p => p.name + ': ' + ( mapType( p.type, classes ) || '?' ) ).join( ', ' );
+	let t = 'fn(' + args + ')';
+	const ret = mapType( doc.returns, classes );
+	if ( ret ) t += ' -> ' + ret;
+	return t;
+
+}
+
+function entry( type, doc ) {
+
+	const e = { '!type': type };
+	if ( doc.doc ) e[ '!doc' ] = doc.doc;
+	return e;
+
+}
+
+// --- main scan --------------------------------------------------------------
+
+export default function buildDefs( source ) {
+
+	const lines = source.split( '\n' );
+
+	// pass 1: collect class names (so types can resolve to +THREE.X)
+	const classes = new Set();
+	for ( const line of lines ) {
+
+		const m = line.match( /^class\s+(\w+)/ );
+		if ( m ) classes.add( m[ 1 ] );
+
+	}
+
+	const THREE = {};
+	let cur = null;        // current class def object
+	let curName = null;    // current class name
+	let pending = null;    // most recent parsed JSDoc block
+
+	for ( let i = 0; i < lines.length; i ++ ) {
+
+		const line = lines[ i ];
+
+		// JSDoc block: collect then resolve against the next code line below
+		const t = line.trim();
+		if ( t.startsWith( '/**' ) ) {
+
+			const block = [];
+			if ( ! t.endsWith( '*/' ) ) {
+
+				i ++;
+				for ( ; i < lines.length; i ++ ) {
+
+					if ( lines[ i ].trim().endsWith( '*/' ) ) break;
+					block.push( lines[ i ] );
+
+				}
+
+			}
+
+			pending = parseDoc( block );
+			continue;
+
+		}
+
+		// class declaration
+		const cm = line.match( /^class\s+(\w+)(?:\s+extends\s+(\w+))?/ );
+		if ( cm ) {
+
+			curName = cm[ 1 ];
+			cur = THREE[ curName ] || ( THREE[ curName ] = {} );
+			// Declaring the class with a function `!type` makes Tern treat it as a
+			// constructor, so `new THREE.X()` resolves to X.prototype. The signature
+			// is refined once the constructor's JSDoc is read.
+			cur[ '!type' ] = 'fn()';
+			cur.prototype = cur.prototype || {};
+			if ( cm[ 2 ] && classes.has( cm[ 2 ] ) ) cur.prototype[ '!proto' ] = 'THREE.' + cm[ 2 ] + '.prototype';
+			if ( pending && pending.doc ) cur[ '!doc' ] = pending.doc;
+			pending = null;
+			continue;
+
+		}
+
+		// end of a top-level class
+		if ( line === '}' ) { cur = null; curName = null; pending = null; continue; }
+
+		if ( ! cur || ! pending ) { if ( t === '' ) continue; pending = null; continue; }
+
+		// inside a class, the line right after a JSDoc block:
+
+		// instance field:  \t\tthis.name =
+		let m = line.match( /^\t\tthis\.(\w+)\s*=/ );
+		if ( m ) {
+
+			const ty = mapType( pending.atType, classes ) || '?';
+			cur.prototype[ m[ 1 ] ] = entry( ty, pending );
+			pending = null;
+			continue;
+
+		}
+
+		// static method:  \tstatic name(
+		m = line.match( /^\tstatic\s+(\w+)\s*\(/ );
+		if ( m ) {
+
+			cur[ m[ 1 ] ] = entry( fnType( pending, classes ), pending );
+			pending = null;
+			continue;
+
+		}
+
+		// accessor:  \tget name()  /  \tset name(
+		m = line.match( /^\t(?:get|set)\s+(\w+)\s*\(/ );
+		if ( m ) {
+
+			const ty = mapType( pending.atType || pending.returns, classes ) || '?';
+			if ( ! cur.prototype[ m[ 1 ] ] ) cur.prototype[ m[ 1 ] ] = entry( ty, pending );
+			pending = null;
+			continue;
+
+		}
+
+		// constructor:  \tconstructor(   -> refine the class signature
+		if ( /^\tconstructor\s*\(/.test( line ) ) {
+
+			cur[ '!type' ] = 'fn(' + pending.params.map( p => p.name + ': ' + ( mapType( p.type, classes ) || '?' ) ).join( ', ' ) + ')';
+			pending = null;
+			continue;
+
+		}
+
+		// instance method:  \t[async ]name(
+		m = line.match( /^\t(?:async\s+)?(\w+)\s*\(/ );
+		if ( m ) {
+
+			cur.prototype[ m[ 1 ] ] = entry( fnType( pending, classes ), pending );
+			pending = null;
+			continue;
+
+		}
+
+		pending = null;
+
+	}
+
+	return { '!name': 'threejs', THREE };
+
+}

Разлика између датотеке није приказан због своје велике величине
+ 0 - 268
editor/js/libs/tern-threejs/threejs.js


Неке датотеке нису приказане због велике количине промена

粤ICP备19079148号