2
0

ScriptEditor.hx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. package hide.comp;
  2. typedef GlobalsDef = haxe.DynamicAccess<{
  3. var globals : haxe.DynamicAccess<String>;
  4. var context : String;
  5. var contexts : Array<String>;
  6. var events : String;
  7. var evalTo : String;
  8. var allowGlobalsDefine : Bool;
  9. var cdbEnums : Array<String>;
  10. }>;
  11. class ScriptCache {
  12. var content : Map<String,Bool> = [];
  13. var configSign : String;
  14. public var signature : String;
  15. public function new(configSign:String) {
  16. this.configSign = configSign;
  17. var key = js.Browser.window.localStorage.getItem("script_cache_key");
  18. if( key == configSign ) {
  19. var values = js.Browser.window.localStorage.getItem("script_cache_val").split(";");
  20. for( v in values )
  21. content.set(v,true);
  22. }
  23. }
  24. public function get( hash : String ) {
  25. return content.get(hash);
  26. }
  27. public function set( hash : String, b : Bool ) {
  28. if( content.get(hash) == b ) return;
  29. content.set(hash, b);
  30. var all = [];
  31. for( key => b in content ) {
  32. if( b )
  33. all.push(key);
  34. }
  35. js.Browser.window.localStorage.setItem("script_cache_key", configSign);
  36. js.Browser.window.localStorage.setItem("script_cache_val", all.join(";"));
  37. }
  38. }
  39. class ScriptChecker {
  40. static var TYPES_SAVE = new Map();
  41. static var ERROR_SAVE = new Map();
  42. static var CONFIG_HASH = null;
  43. static var CHECK_CACHE : ScriptCache;
  44. static var TYPE_CHECK_HOOKS : Array<ScriptChecker->Void> = [];
  45. var ide : hide.Ide;
  46. var apiFiles : Array<String>;
  47. var checkEvents : Bool;
  48. public var config : hide.Config;
  49. public var documentName : String;
  50. public var constants : Map<String,Dynamic>;
  51. public var evalTo : String;
  52. public var checker(default,null) : hscript.Checker;
  53. var initDone = false;
  54. public function new( config : hide.Config, documentName : String, ?constants : Map<String,Dynamic> ) {
  55. this.config = config;
  56. this.documentName = documentName;
  57. this.constants = constants == null ? new Map() : constants;
  58. ide = hide.Ide.inst;
  59. apiFiles = config.get("script.api.files");
  60. reload();
  61. }
  62. function hashString( str : String ) {
  63. return haxe.crypto.Md5.encode(str);
  64. }
  65. public function reload() {
  66. checker = new hscript.Checker();
  67. checker.allowAsync = true;
  68. initDone = false;
  69. var hashes = [];
  70. if( apiFiles != null && apiFiles.length >= 0 ) {
  71. var types = TYPES_SAVE.get(apiFiles.join(";"));
  72. if( types == null ) {
  73. types = new hscript.Checker.CheckerTypes();
  74. for( f in apiFiles ) {
  75. var path = ide.getPath(f);
  76. var content = try sys.io.File.getContent(path) catch( e : Dynamic ) { error(""+e); continue; };
  77. hashes.push(path+":"+sys.FileSystem.stat(path).mtime.getTime());
  78. types.addXmlApi(Xml.parse(content).firstElement());
  79. ide.fileWatcher.register(f, reloadApi);
  80. }
  81. TYPES_SAVE.set(apiFiles.join(";"), types);
  82. }
  83. checker.types = types;
  84. }
  85. if( CONFIG_HASH == null ) {
  86. hashes.push(haxe.Json.stringify(config.get("script.api")));
  87. CONFIG_HASH = hashString(hashes.join(","));
  88. }
  89. }
  90. function init() {
  91. if( initDone ) return;
  92. initDone = true;
  93. var parts = documentName.split(".");
  94. var apis = [];
  95. while( parts.length > 0 ) {
  96. var path = parts.join(".");
  97. parts.pop();
  98. var config = config.get("script.api");
  99. if( config == null ) continue;
  100. var api = (config : GlobalsDef).get(path);
  101. if( api == null ) {
  102. path = ~/\[group=[^\]]+?\]/g.replace(path,"");
  103. api = (config : GlobalsDef).get(path);
  104. }
  105. if( api != null )
  106. apis.unshift(api);
  107. }
  108. var cdbPack : String = config.get("script.cdbPackage");
  109. var contexts = [];
  110. var allowGlobalsDefine = false;
  111. checkEvents = false;
  112. for( api in apis ) {
  113. for( f in api.globals.keys() ) {
  114. var tname = api.globals.get(f);
  115. if( tname == null ) {
  116. checker.removeGlobal(f);
  117. continue;
  118. }
  119. var isClass = tname.charCodeAt(0) == '#'.code;
  120. if( isClass ) tname = tname.substr(1);
  121. var t = checker.types.resolve(tname);
  122. if( t == null ) {
  123. var path = tname.split(".");
  124. var fields = [];
  125. while( path.length > 0 ) {
  126. var name = path.join(".");
  127. if( constants.exists(name) ) {
  128. var value : Dynamic = constants.get(name);
  129. for( f in fields )
  130. value = Reflect.field(value, f);
  131. t = typeFromValue(value);
  132. if( t == null ) t = TAnon([]);
  133. }
  134. fields.unshift(path.pop());
  135. }
  136. }
  137. if( t == null ) {
  138. error('Global type $tname not found in $apiFiles ($f)');
  139. continue;
  140. }
  141. if( isClass ) {
  142. switch( t ) {
  143. case TEnum(e,_):
  144. t = TAnon([for( c in e.constructors ) { name : c.name, opt : false, t : c.args == null ? t : TFun(c.args,t) }]);
  145. default:
  146. error('Cannot process class type $tname');
  147. }
  148. }
  149. checker.setGlobal(f, t);
  150. }
  151. if( api.context != null )
  152. contexts = [api.context];
  153. if( api.contexts != null )
  154. contexts = api.contexts;
  155. if( api.allowGlobalsDefine != null )
  156. allowGlobalsDefine = api.allowGlobalsDefine;
  157. if( api.events != null ) {
  158. for( f in getFields(api.events) )
  159. checker.setEvent(f.name, f.t);
  160. checkEvents = true;
  161. }
  162. if( api.cdbEnums != null ) {
  163. for( c in api.cdbEnums )
  164. addCDBEnum(c, cdbPack);
  165. }
  166. if( api.evalTo != null )
  167. this.evalTo = api.evalTo;
  168. }
  169. for( context in contexts ) {
  170. var ctx = checker.types.resolve(context);
  171. if( ctx == null )
  172. error(context+" is not defined");
  173. else {
  174. switch( ctx ) {
  175. case TInst(c,_):
  176. var cc = c;
  177. while( true ) {
  178. for( f in cc.fields ) if( f.t.match(TFun(_)) ) f.isPublic = true; // allow access to private methods
  179. if( cc.superClass == null ) break;
  180. cc = switch( cc.superClass ) {
  181. case TInst(c,_): c;
  182. default: throw "assert";
  183. }
  184. }
  185. checker.setGlobals(c);
  186. default: error(context+" is not a class");
  187. }
  188. }
  189. }
  190. checker.allowUntypedMeta = true;
  191. checker.allowGlobalsDefine = allowGlobalsDefine;
  192. for( c in TYPE_CHECK_HOOKS )
  193. c(this);
  194. }
  195. function getFields( tpath : String ) {
  196. var t = checker.types.resolve(tpath);
  197. if( t == null )
  198. error("Missing type "+tpath);
  199. var fl = checker.getFields(t);
  200. if( fl == null )
  201. error(tpath+" is not a class");
  202. return fl;
  203. }
  204. function error( msg : String ) {
  205. if( !ERROR_SAVE.exists(msg) ) {
  206. ERROR_SAVE.set(msg,true);
  207. ide.error(msg);
  208. }
  209. }
  210. public function addCDBEnum( name : String, ?cdbPack : String ) {
  211. var path = name.split(".");
  212. var sname = path.join("@");
  213. var objPath = null;
  214. if( path.length > 1 ) { // might be a scoped id
  215. var objID = this.constants.get("cdb.objID");
  216. objPath = objID == null ? [] : objID.split(":");
  217. }
  218. for( s in ide.database.sheets ) {
  219. if( s.name != sname ) continue;
  220. var name = path[path.length - 1];
  221. name = name.charAt(0).toUpperCase() + name.substr(1);
  222. var kname = path.join("_")+"Kind";
  223. kname = kname.charAt(0).toUpperCase() + kname.substr(1);
  224. if( cdbPack != "" ) kname = cdbPack + "." + kname;
  225. var kind = checker.types.resolve(kname);
  226. if( kind == null )
  227. kind = TEnum({ name : kname, params : [], constructors : [] },[]);
  228. var cl : hscript.Checker.CClass = {
  229. name : name,
  230. params : [],
  231. fields : new Map(),
  232. statics : new Map()
  233. };
  234. var refPath = s.idCol.scope == null ? null : objPath.slice(0, s.idCol.scope).join(":")+":";
  235. for( o in s.all ) {
  236. var id = o.id;
  237. if( id == null || id == "" ) continue;
  238. if( refPath != null ) {
  239. if( !StringTools.startsWith(id, refPath) ) continue;
  240. id = id.substr(refPath.length);
  241. }
  242. cl.fields.set(id, { name : id, params : [], canWrite : false, t : kind, isPublic: true, complete : true });
  243. }
  244. checker.setGlobal(name, TInst(cl,[]));
  245. return kind;
  246. }
  247. return null;
  248. }
  249. function typeFromValue( value : Dynamic ) : hscript.Checker.TType {
  250. switch( std.Type.typeof(value) ) {
  251. case TNull:
  252. return null;
  253. case TInt:
  254. return TInt;
  255. case TFloat:
  256. return TFloat;
  257. case TBool:
  258. return TBool;
  259. case TObject:
  260. var fields = [];
  261. for( f in Reflect.fields(value) ) {
  262. var t = typeFromValue(Reflect.field(value,f));
  263. if( t == null ) continue;
  264. fields.push({ name : f, t : t, opt : false });
  265. }
  266. return TAnon(fields);
  267. case TClass(c):
  268. return checker.types.resolve(Type.getClassName(c),[]);
  269. case TEnum(e):
  270. return checker.types.resolve(Type.getEnumName(e),[]);
  271. case TFunction, TUnknown:
  272. }
  273. return null;
  274. }
  275. public function makeParser() {
  276. var parser = new hscript.Parser();
  277. parser.allowMetadata = true;
  278. parser.allowTypes = true;
  279. parser.allowJSON = true;
  280. return parser;
  281. }
  282. public function getCompletion( script : String ) {
  283. var parser = makeParser();
  284. parser.resumeErrors = true;
  285. var expr = parser.parseString(script,""); // should not error
  286. try {
  287. init();
  288. var et = checker.check(expr,null,true);
  289. return null;
  290. } catch( e : hscript.Checker.Completion ) {
  291. // ignore
  292. return e.t;
  293. }
  294. }
  295. public function getCache( code : String ) {
  296. if( CHECK_CACHE == null )
  297. CHECK_CACHE = new ScriptCache(CONFIG_HASH);
  298. CHECK_CACHE.signature = hashString(code+":"+documentName+":"+haxe.Json.stringify(constants));
  299. return CHECK_CACHE;
  300. }
  301. public function check( script : String, checkTypes = true ) {
  302. var parser = makeParser();
  303. try {
  304. var expr = parser.parseString(script, "");
  305. if( checkEvents ) {
  306. function checkRec(e:hscript.Expr) {
  307. switch( e.e ) {
  308. case EFunction(_,_,name,_):
  309. if( name != null && StringTools.startsWith(name,"on") && name.charCodeAt(2) > 'A'.code && name.charCodeAt(2) < 'Z'.code && @:privateAccess !checker.events.exists(name) )
  310. @:privateAccess checker.error('Unknown event $name', e);
  311. default:
  312. hscript.Tools.iter(e, checkRec);
  313. }
  314. }
  315. checkRec(expr);
  316. }
  317. if( checkTypes ) {
  318. init();
  319. var et = checker.check(expr);
  320. if( evalTo != null ) {
  321. var t = checker.types.resolve(evalTo);
  322. if( t == null ) {
  323. error('EvalTo type $evalTo not found');
  324. return null;
  325. }
  326. checker.unify(et, t, expr);
  327. }
  328. }
  329. return null;
  330. } catch( e : hscript.Expr.Error ) {
  331. return e;
  332. }
  333. }
  334. public static function reloadApi() {
  335. TYPES_SAVE = new Map();
  336. CONFIG_HASH = null;
  337. CHECK_CACHE = null;
  338. }
  339. }
  340. class ScriptEditor extends CodeEditor {
  341. static var INIT_DONE = false;
  342. var checker : ScriptChecker;
  343. var checkTypes : Bool;
  344. public function new( script : String, ?checker : ScriptChecker, ?parent : Element, ?root : Element, ?lang ) {
  345. if( !INIT_DONE ) {
  346. INIT_DONE = true;
  347. (monaco.Languages : Dynamic).typescript.javascriptDefaults.setCompilerOptions({ noLib: true, allowNonTsExtensions: true }); // disable js stdlib completion
  348. monaco.Languages.registerCompletionItemProvider("javascript", {
  349. triggerCharacters : ["."],
  350. provideCompletionItems : function(model,position,_,_) {
  351. var comp : ScriptEditor = (model : Dynamic).__comp__;
  352. var code = model.getValueInRange({startLineNumber: 1, startColumn: 1, endLineNumber: position.lineNumber, endColumn: position.column});
  353. return comp.getCompletion(code.length);
  354. }
  355. });
  356. }
  357. super(script, lang, parent,root);
  358. if( checker == null ) {
  359. checker = new ScriptChecker(new hide.Config(),"");
  360. checkTypes = false;
  361. } else {
  362. var files = @:privateAccess checker.apiFiles;
  363. if( files != null ) {
  364. for( f in files )
  365. ide.fileWatcher.register(f, function() {
  366. ScriptChecker.reloadApi();
  367. haxe.Timer.delay(function() { try checker.reload() catch( e : Dynamic ) {}; doCheckScript(); }, 100);
  368. }, root);
  369. }
  370. }
  371. this.checker = checker;
  372. onChanged = doCheckScript;
  373. haxe.Timer.delay(function() doCheckScript(), 0);
  374. }
  375. function getCompletion( position : Int ) : Array<monaco.Languages.CompletionItem> {
  376. var script = code.substr(0,position);
  377. var vars = checker.checker.getGlobals();
  378. if( script.charCodeAt(script.length-1) == ".".code ) {
  379. vars = [];
  380. var t = checker.getCompletion(script);
  381. if( t != null ) {
  382. var fields = checker.checker.getFields(t);
  383. for( f in fields )
  384. vars.set(f.name, f.t);
  385. }
  386. }
  387. var checker = checker.checker;
  388. return [for( k in vars.keys() ) {
  389. var t = vars.get(k);
  390. if( StringTools.startsWith(k,"a_") ) {
  391. var t2 = checker.unasync(t);
  392. if( t2 != null ) {
  393. t = t2;
  394. k = k.substr(2);
  395. }
  396. }
  397. var isFun = checker.follow(t).match(TFun(_));
  398. if( isFun ) {
  399. {
  400. kind : Method,
  401. label : k,
  402. detail : hscript.Checker.typeStr(t),
  403. commitCharacters: ["("],
  404. }
  405. } else {
  406. {
  407. kind : Field,
  408. label : k,
  409. detail : hscript.Checker.typeStr(t),
  410. }
  411. }
  412. }];
  413. }
  414. public function doCheckScript() {
  415. var script = code;
  416. var error = checker.check(script, checkTypes);
  417. if( error == null )
  418. clearError();
  419. else
  420. setError(hscript.Printer.errorToString(error), error.line, error.pmin, error.pmax);
  421. }
  422. public static function register( cl : Class<Dynamic> ) : Bool {
  423. @:privateAccess ScriptChecker.TYPE_CHECK_HOOKS.push(function(checker) {
  424. Type.createInstance(cl,[checker]);
  425. });
  426. return true;
  427. }
  428. }