ScriptEditor.hx 9.7 KB

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