ScriptEditor.hx 11 KB

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