initial
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
README.html
|
||||||
117
README.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Leporello.js
|
||||||
|
|
||||||
|
Leporello.js is live coding IDE for pure functional subset of javascript. It provides novel debugging experience
|
||||||
|
|
||||||
|
## **[Try online](https://leporello-js.github.io/leporello-js/)**
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Mutating values is not allowed
|
||||||
|

|
||||||
|
|
||||||
|
- All values are immutable. You create new values by applying change to old values
|
||||||
|

|
||||||
|
|
||||||
|
- Functional programs are trees of expressions that map values to other values,
|
||||||
|
rather than a sequence of imperative statements which update the running
|
||||||
|
state of the program. Because data is never mutated, you can jump to any
|
||||||
|
point in execution of your program
|
||||||
|

|
||||||
|
|
||||||
|
- and inspect any intermediate values
|
||||||
|

|
||||||
|
|
||||||
|
- Expressions that were evaluated have blue background. And that were not reached
|
||||||
|
have white background.
|
||||||
|

|
||||||
|
|
||||||
|
- Expressions that throw errors are red
|
||||||
|

|
||||||
|
|
||||||
|
- When you put cursor inside function, the first call of this function is found
|
||||||
|

|
||||||
|
|
||||||
|
- You can edit this function and immediately see result
|
||||||
|

|
||||||
|
|
||||||
|
- Leporello is (mostly) self-hosted, i.e. built in itself
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## Supported javascript subset
|
||||||
|
|
||||||
|
Variables are declared by `const` declaration. `var` is not supported. `let` variables can be declared to be assigned later, for cases when value depends on condition. Example:
|
||||||
|
```
|
||||||
|
let result
|
||||||
|
if (n == 0 || n == 1) {
|
||||||
|
result = n
|
||||||
|
} else {
|
||||||
|
result = fib(n - 1) + fib(n - 2)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Currenlty only one declaration for single `const` statement is supported (TODO).
|
||||||
|
|
||||||
|
Any kind of loops are not supported. Use recursion or array functions instead.
|
||||||
|
|
||||||
|
`if` / `else` can only contain blocks, not single statements (TODO).
|
||||||
|
|
||||||
|
Functions can be declared only by arrow function syntax. `function` keyword and method definitions (like `const foo = { bar() { /* body */ } }` may be supported in future. Both concise and block body are supported.
|
||||||
|
|
||||||
|
Classes are not supported. Some sort of immutable classes may be supported in future. `this` keyword is not currently supported. `new` operator is supported for instantiating builtin classes.
|
||||||
|
|
||||||
|
`switch` statements will be supported in future.
|
||||||
|
|
||||||
|
`try`, `catch` and `finally` will be supported in future. `throw` is currently supported.
|
||||||
|
|
||||||
|
ES6 modules are suppoted. Default exports are not currently supported, only named exports. Circular module dependencies are not supported (currently they crash IDE (TODO)). Import/export aliases are not supported. Exporting `let` variables is not supported. `import.meta` is not supported.
|
||||||
|
|
||||||
|
Generators are not supported.
|
||||||
|
|
||||||
|
Async/await will be supported in future.
|
||||||
|
|
||||||
|
Destructuring is mostly supported.
|
||||||
|
|
||||||
|
Some operators are not currently supported:
|
||||||
|
- Unary negation, unary plus
|
||||||
|
- Bitwise operators
|
||||||
|
- `in`, `instanceof`
|
||||||
|
- `void`
|
||||||
|
- comma operator
|
||||||
|
|
||||||
|
Operators that are not supported by design (not pure functional):
|
||||||
|
- increment, decrement
|
||||||
|
- `delete`
|
||||||
|
|
||||||
|
## Hotkeys
|
||||||
|
|
||||||
|
See built-in Help
|
||||||
|
|
||||||
|
## Editing local files
|
||||||
|
|
||||||
|
Editing local files is possible via [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API). Click "Allow access to local project folder" to grant access to local directory.
|
||||||
|
|
||||||
|
|
||||||
|
## Run Leporello locally
|
||||||
|
To run it locally, you need to clone repo to local folder and serve it via HTTPS protocol (HTTPS is required by [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API)). See [How to use HTTPS for local development](https://web.dev/how-to-use-local-https/)
|
||||||
|
|
||||||
|
## Running test suite
|
||||||
|
run tests in node.js:
|
||||||
|
|
||||||
|
```
|
||||||
|
node test/run.js
|
||||||
|
```
|
||||||
|
|
||||||
|
run tests in leporello itself:
|
||||||
|

|
||||||
|
|
||||||
|
- grant local folder access
|
||||||
|
- select `test/run.js` as entrypoint
|
||||||
|
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
* Support async/await and calling impure (performing IO) functions
|
||||||
|
* Use production level JS parser, probably typescript parser (so it will be
|
||||||
|
possible to program in pure functional subset of typescript)
|
||||||
|
* Implement VSCode plugin
|
||||||
21816
ace/ace.js
Normal file
8
ace/ext-searchbox.js
Normal file
5895
ace/keybinding-vim.js
Normal file
797
ace/mode-javascript.js
Normal file
@@ -0,0 +1,797 @@
|
|||||||
|
define("ace/mode/doc_comment_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var oop = require("../lib/oop");
|
||||||
|
var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
|
||||||
|
|
||||||
|
var DocCommentHighlightRules = function() {
|
||||||
|
this.$rules = {
|
||||||
|
"start" : [ {
|
||||||
|
token : "comment.doc.tag",
|
||||||
|
regex : "@[\\w\\d_]+" // TODO: fix email addresses
|
||||||
|
},
|
||||||
|
DocCommentHighlightRules.getTagRule(),
|
||||||
|
{
|
||||||
|
defaultToken : "comment.doc",
|
||||||
|
caseInsensitive: true
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
oop.inherits(DocCommentHighlightRules, TextHighlightRules);
|
||||||
|
|
||||||
|
DocCommentHighlightRules.getTagRule = function(start) {
|
||||||
|
return {
|
||||||
|
token : "comment.doc.tag.storage.type",
|
||||||
|
regex : "\\b(?:TODO|FIXME|XXX|HACK)\\b"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
DocCommentHighlightRules.getStartRule = function(start) {
|
||||||
|
return {
|
||||||
|
token : "comment.doc", // doc comment
|
||||||
|
regex : "\\/\\*(?=\\*)",
|
||||||
|
next : start
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
DocCommentHighlightRules.getEndRule = function (start) {
|
||||||
|
return {
|
||||||
|
token : "comment.doc", // closing comment
|
||||||
|
regex : "\\*\\/",
|
||||||
|
next : start
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
exports.DocCommentHighlightRules = DocCommentHighlightRules;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
define("ace/mode/javascript_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"], function(require, exports, module) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var oop = require("../lib/oop");
|
||||||
|
var DocCommentHighlightRules = require("./doc_comment_highlight_rules").DocCommentHighlightRules;
|
||||||
|
var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
|
||||||
|
var identifierRe = "[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*";
|
||||||
|
|
||||||
|
var JavaScriptHighlightRules = function(options) {
|
||||||
|
var keywordMapper = this.createKeywordMapper({
|
||||||
|
"variable.language":
|
||||||
|
"Array|Boolean|Date|Function|Iterator|Number|Object|RegExp|String|Proxy|" + // Constructors
|
||||||
|
"Namespace|QName|XML|XMLList|" + // E4X
|
||||||
|
"ArrayBuffer|Float32Array|Float64Array|Int16Array|Int32Array|Int8Array|" +
|
||||||
|
"Uint16Array|Uint32Array|Uint8Array|Uint8ClampedArray|" +
|
||||||
|
"Error|EvalError|InternalError|RangeError|ReferenceError|StopIteration|" + // Errors
|
||||||
|
"SyntaxError|TypeError|URIError|" +
|
||||||
|
"decodeURI|decodeURIComponent|encodeURI|encodeURIComponent|eval|isFinite|" + // Non-constructor functions
|
||||||
|
"isNaN|parseFloat|parseInt|" +
|
||||||
|
"JSON|Math|" + // Other
|
||||||
|
"this|arguments|prototype|window|document" , // Pseudo
|
||||||
|
"keyword":
|
||||||
|
"const|yield|import|get|set|async|await|" +
|
||||||
|
"break|case|catch|continue|default|delete|do|else|finally|for|function|" +
|
||||||
|
"if|in|of|instanceof|new|return|switch|throw|try|typeof|let|var|while|with|debugger|" +
|
||||||
|
"__parent__|__count__|escape|unescape|with|__proto__|" +
|
||||||
|
"class|enum|extends|super|export|implements|private|public|interface|package|protected|static",
|
||||||
|
"storage.type":
|
||||||
|
"const|let|var|function",
|
||||||
|
"constant.language":
|
||||||
|
"null|Infinity|NaN|undefined",
|
||||||
|
"support.function":
|
||||||
|
"alert",
|
||||||
|
"constant.language.boolean": "true|false"
|
||||||
|
}, "identifier");
|
||||||
|
var kwBeforeRe = "case|do|else|finally|in|instanceof|return|throw|try|typeof|yield|void";
|
||||||
|
|
||||||
|
var escapedRe = "\\\\(?:x[0-9a-fA-F]{2}|" + // hex
|
||||||
|
"u[0-9a-fA-F]{4}|" + // unicode
|
||||||
|
"u{[0-9a-fA-F]{1,6}}|" + // es6 unicode
|
||||||
|
"[0-2][0-7]{0,2}|" + // oct
|
||||||
|
"3[0-7][0-7]?|" + // oct
|
||||||
|
"[4-7][0-7]?|" + //oct
|
||||||
|
".)";
|
||||||
|
|
||||||
|
this.$rules = {
|
||||||
|
"no_regex" : [
|
||||||
|
DocCommentHighlightRules.getStartRule("doc-start"),
|
||||||
|
comments("no_regex"),
|
||||||
|
{
|
||||||
|
token : "string",
|
||||||
|
regex : "'(?=.)",
|
||||||
|
next : "qstring"
|
||||||
|
}, {
|
||||||
|
token : "string",
|
||||||
|
regex : '"(?=.)',
|
||||||
|
next : "qqstring"
|
||||||
|
}, {
|
||||||
|
token : "constant.numeric", // hexadecimal, octal and binary
|
||||||
|
regex : /0(?:[xX][0-9a-fA-F]+|[oO][0-7]+|[bB][01]+)\b/
|
||||||
|
}, {
|
||||||
|
token : "constant.numeric", // decimal integers and floats
|
||||||
|
regex : /(?:\d\d*(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+\b)?/
|
||||||
|
}, {
|
||||||
|
token : [
|
||||||
|
"storage.type", "punctuation.operator", "support.function",
|
||||||
|
"punctuation.operator", "entity.name.function", "text","keyword.operator"
|
||||||
|
],
|
||||||
|
regex : "(" + identifierRe + ")(\\.)(prototype)(\\.)(" + identifierRe +")(\\s*)(=)",
|
||||||
|
next: "function_arguments"
|
||||||
|
}, {
|
||||||
|
token : [
|
||||||
|
"storage.type", "punctuation.operator", "entity.name.function", "text",
|
||||||
|
"keyword.operator", "text", "storage.type", "text", "paren.lparen"
|
||||||
|
],
|
||||||
|
regex : "(" + identifierRe + ")(\\.)(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s*)(\\()",
|
||||||
|
next: "function_arguments"
|
||||||
|
}, {
|
||||||
|
token : [
|
||||||
|
"entity.name.function", "text", "keyword.operator", "text", "storage.type",
|
||||||
|
"text", "paren.lparen"
|
||||||
|
],
|
||||||
|
regex : "(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s*)(\\()",
|
||||||
|
next: "function_arguments"
|
||||||
|
}, {
|
||||||
|
token : [
|
||||||
|
"storage.type", "punctuation.operator", "entity.name.function", "text",
|
||||||
|
"keyword.operator", "text",
|
||||||
|
"storage.type", "text", "entity.name.function", "text", "paren.lparen"
|
||||||
|
],
|
||||||
|
regex : "(" + identifierRe + ")(\\.)(" + identifierRe +")(\\s*)(=)(\\s*)(function)(\\s+)(\\w+)(\\s*)(\\()",
|
||||||
|
next: "function_arguments"
|
||||||
|
}, {
|
||||||
|
token : [
|
||||||
|
"storage.type", "text", "entity.name.function", "text", "paren.lparen"
|
||||||
|
],
|
||||||
|
regex : "(function)(\\s+)(" + identifierRe + ")(\\s*)(\\()",
|
||||||
|
next: "function_arguments"
|
||||||
|
}, {
|
||||||
|
token : [
|
||||||
|
"entity.name.function", "text", "punctuation.operator",
|
||||||
|
"text", "storage.type", "text", "paren.lparen"
|
||||||
|
],
|
||||||
|
regex : "(" + identifierRe + ")(\\s*)(:)(\\s*)(function)(\\s*)(\\()",
|
||||||
|
next: "function_arguments"
|
||||||
|
}, {
|
||||||
|
token : [
|
||||||
|
"text", "text", "storage.type", "text", "paren.lparen"
|
||||||
|
],
|
||||||
|
regex : "(:)(\\s*)(function)(\\s*)(\\()",
|
||||||
|
next: "function_arguments"
|
||||||
|
}, {
|
||||||
|
token : "keyword",
|
||||||
|
regex : "from(?=\\s*('|\"))"
|
||||||
|
}, {
|
||||||
|
token : "keyword",
|
||||||
|
regex : "(?:" + kwBeforeRe + ")\\b",
|
||||||
|
next : "start"
|
||||||
|
}, {
|
||||||
|
token : ["support.constant"],
|
||||||
|
regex : /that\b/
|
||||||
|
}, {
|
||||||
|
token : ["storage.type", "punctuation.operator", "support.function.firebug"],
|
||||||
|
regex : /(console)(\.)(warn|info|log|error|time|trace|timeEnd|assert)\b/
|
||||||
|
}, {
|
||||||
|
token : keywordMapper,
|
||||||
|
regex : identifierRe
|
||||||
|
}, {
|
||||||
|
token : "punctuation.operator",
|
||||||
|
regex : /[.](?![.])/,
|
||||||
|
next : "property"
|
||||||
|
}, {
|
||||||
|
token : "storage.type",
|
||||||
|
regex : /=>/,
|
||||||
|
next : "start"
|
||||||
|
}, {
|
||||||
|
token : "keyword.operator",
|
||||||
|
regex : /--|\+\+|\.{3}|===|==|=|!=|!==|<+=?|>+=?|!|&&|\|\||\?:|[!$%&*+\-~\/^]=?/,
|
||||||
|
next : "start"
|
||||||
|
}, {
|
||||||
|
token : "punctuation.operator",
|
||||||
|
regex : /[?:,;.]/,
|
||||||
|
next : "start"
|
||||||
|
}, {
|
||||||
|
token : "paren.lparen",
|
||||||
|
regex : /[\[({]/,
|
||||||
|
next : "start"
|
||||||
|
}, {
|
||||||
|
token : "paren.rparen",
|
||||||
|
regex : /[\])}]/
|
||||||
|
}, {
|
||||||
|
token: "comment",
|
||||||
|
regex: /^#!.*$/
|
||||||
|
}
|
||||||
|
],
|
||||||
|
property: [{
|
||||||
|
token : "text",
|
||||||
|
regex : "\\s+"
|
||||||
|
}, {
|
||||||
|
token : [
|
||||||
|
"storage.type", "punctuation.operator", "entity.name.function", "text",
|
||||||
|
"keyword.operator", "text",
|
||||||
|
"storage.type", "text", "entity.name.function", "text", "paren.lparen"
|
||||||
|
],
|
||||||
|
regex : "(" + identifierRe + ")(\\.)(" + identifierRe +")(\\s*)(=)(\\s*)(function)(?:(\\s+)(\\w+))?(\\s*)(\\()",
|
||||||
|
next: "function_arguments"
|
||||||
|
}, {
|
||||||
|
token : "punctuation.operator",
|
||||||
|
regex : /[.](?![.])/
|
||||||
|
}, {
|
||||||
|
token : "support.function",
|
||||||
|
regex : /(s(?:h(?:ift|ow(?:Mod(?:elessDialog|alDialog)|Help))|croll(?:X|By(?:Pages|Lines)?|Y|To)?|t(?:op|rike)|i(?:n|zeToContent|debar|gnText)|ort|u(?:p|b(?:str(?:ing)?)?)|pli(?:ce|t)|e(?:nd|t(?:Re(?:sizable|questHeader)|M(?:i(?:nutes|lliseconds)|onth)|Seconds|Ho(?:tKeys|urs)|Year|Cursor|Time(?:out)?|Interval|ZOptions|Date|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Date|FullYear)|FullYear|Active)|arch)|qrt|lice|avePreferences|mall)|h(?:ome|andleEvent)|navigate|c(?:har(?:CodeAt|At)|o(?:s|n(?:cat|textual|firm)|mpile)|eil|lear(?:Timeout|Interval)?|a(?:ptureEvents|ll)|reate(?:StyleSheet|Popup|EventObject))|t(?:o(?:GMTString|S(?:tring|ource)|U(?:TCString|pperCase)|Lo(?:caleString|werCase))|est|a(?:n|int(?:Enabled)?))|i(?:s(?:NaN|Finite)|ndexOf|talics)|d(?:isableExternalCapture|ump|etachEvent)|u(?:n(?:shift|taint|escape|watch)|pdateCommands)|j(?:oin|avaEnabled)|p(?:o(?:p|w)|ush|lugins.refresh|a(?:ddings|rse(?:Int|Float)?)|r(?:int|ompt|eference))|e(?:scape|nableExternalCapture|val|lementFromPoint|x(?:p|ec(?:Script|Command)?))|valueOf|UTC|queryCommand(?:State|Indeterm|Enabled|Value)|f(?:i(?:nd|le(?:ModifiedDate|Size|CreatedDate|UpdatedDate)|xed)|o(?:nt(?:size|color)|rward)|loor|romCharCode)|watch|l(?:ink|o(?:ad|g)|astIndexOf)|a(?:sin|nchor|cos|t(?:tachEvent|ob|an(?:2)?)|pply|lert|b(?:s|ort))|r(?:ou(?:nd|teEvents)|e(?:size(?:By|To)|calc|turnValue|place|verse|l(?:oad|ease(?:Capture|Events)))|andom)|g(?:o|et(?:ResponseHeader|M(?:i(?:nutes|lliseconds)|onth)|Se(?:conds|lection)|Hours|Year|Time(?:zoneOffset)?|Da(?:y|te)|UTC(?:M(?:i(?:nutes|lliseconds)|onth)|Seconds|Hours|Da(?:y|te)|FullYear)|FullYear|A(?:ttention|llResponseHeaders)))|m(?:in|ove(?:B(?:y|elow)|To(?:Absolute)?|Above)|ergeAttributes|a(?:tch|rgins|x))|b(?:toa|ig|o(?:ld|rderWidths)|link|ack))\b(?=\()/
|
||||||
|
}, {
|
||||||
|
token : "support.function.dom",
|
||||||
|
regex : /(s(?:ub(?:stringData|mit)|plitText|e(?:t(?:NamedItem|Attribute(?:Node)?)|lect))|has(?:ChildNodes|Feature)|namedItem|c(?:l(?:ick|o(?:se|neNode))|reate(?:C(?:omment|DATASection|aption)|T(?:Head|extNode|Foot)|DocumentFragment|ProcessingInstruction|E(?:ntityReference|lement)|Attribute))|tabIndex|i(?:nsert(?:Row|Before|Cell|Data)|tem)|open|delete(?:Row|C(?:ell|aption)|T(?:Head|Foot)|Data)|focus|write(?:ln)?|a(?:dd|ppend(?:Child|Data))|re(?:set|place(?:Child|Data)|move(?:NamedItem|Child|Attribute(?:Node)?)?)|get(?:NamedItem|Element(?:sBy(?:Name|TagName|ClassName)|ById)|Attribute(?:Node)?)|blur)\b(?=\()/
|
||||||
|
}, {
|
||||||
|
token : "support.constant",
|
||||||
|
regex : /(s(?:ystemLanguage|cr(?:ipts|ollbars|een(?:X|Y|Top|Left))|t(?:yle(?:Sheets)?|atus(?:Text|bar)?)|ibling(?:Below|Above)|ource|uffixes|e(?:curity(?:Policy)?|l(?:ection|f)))|h(?:istory|ost(?:name)?|as(?:h|Focus))|y|X(?:MLDocument|SLDocument)|n(?:ext|ame(?:space(?:s|URI)|Prop))|M(?:IN_VALUE|AX_VALUE)|c(?:haracterSet|o(?:n(?:structor|trollers)|okieEnabled|lorDepth|mp(?:onents|lete))|urrent|puClass|l(?:i(?:p(?:boardData)?|entInformation)|osed|asses)|alle(?:e|r)|rypto)|t(?:o(?:olbar|p)|ext(?:Transform|Indent|Decoration|Align)|ags)|SQRT(?:1_2|2)|i(?:n(?:ner(?:Height|Width)|put)|ds|gnoreCase)|zIndex|o(?:scpu|n(?:readystatechange|Line)|uter(?:Height|Width)|p(?:sProfile|ener)|ffscreenBuffering)|NEGATIVE_INFINITY|d(?:i(?:splay|alog(?:Height|Top|Width|Left|Arguments)|rectories)|e(?:scription|fault(?:Status|Ch(?:ecked|arset)|View)))|u(?:ser(?:Profile|Language|Agent)|n(?:iqueID|defined)|pdateInterval)|_content|p(?:ixelDepth|ort|ersonalbar|kcs11|l(?:ugins|atform)|a(?:thname|dding(?:Right|Bottom|Top|Left)|rent(?:Window|Layer)?|ge(?:X(?:Offset)?|Y(?:Offset)?))|r(?:o(?:to(?:col|type)|duct(?:Sub)?|mpter)|e(?:vious|fix)))|e(?:n(?:coding|abledPlugin)|x(?:ternal|pando)|mbeds)|v(?:isibility|endor(?:Sub)?|Linkcolor)|URLUnencoded|P(?:I|OSITIVE_INFINITY)|f(?:ilename|o(?:nt(?:Size|Family|Weight)|rmName)|rame(?:s|Element)|gColor)|E|whiteSpace|l(?:i(?:stStyleType|n(?:eHeight|kColor))|o(?:ca(?:tion(?:bar)?|lName)|wsrc)|e(?:ngth|ft(?:Context)?)|a(?:st(?:M(?:odified|atch)|Index|Paren)|yer(?:s|X)|nguage))|a(?:pp(?:MinorVersion|Name|Co(?:deName|re)|Version)|vail(?:Height|Top|Width|Left)|ll|r(?:ity|guments)|Linkcolor|bove)|r(?:ight(?:Context)?|e(?:sponse(?:XML|Text)|adyState))|global|x|m(?:imeTypes|ultiline|enubar|argin(?:Right|Bottom|Top|Left))|L(?:N(?:10|2)|OG(?:10E|2E))|b(?:o(?:ttom|rder(?:Width|RightWidth|BottomWidth|Style|Color|TopWidth|LeftWidth))|ufferDepth|elow|ackground(?:Color|Image)))\b/
|
||||||
|
}, {
|
||||||
|
token : "identifier",
|
||||||
|
regex : identifierRe
|
||||||
|
}, {
|
||||||
|
regex: "",
|
||||||
|
token: "empty",
|
||||||
|
next: "no_regex"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start": [
|
||||||
|
DocCommentHighlightRules.getStartRule("doc-start"),
|
||||||
|
comments("start"),
|
||||||
|
{
|
||||||
|
token: "string.regexp",
|
||||||
|
regex: "\\/",
|
||||||
|
next: "regex"
|
||||||
|
}, {
|
||||||
|
token : "text",
|
||||||
|
regex : "\\s+|^$",
|
||||||
|
next : "start"
|
||||||
|
}, {
|
||||||
|
token: "empty",
|
||||||
|
regex: "",
|
||||||
|
next: "no_regex"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"regex": [
|
||||||
|
{
|
||||||
|
token: "regexp.keyword.operator",
|
||||||
|
regex: "\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)"
|
||||||
|
}, {
|
||||||
|
token: "string.regexp",
|
||||||
|
regex: "/[sxngimy]*",
|
||||||
|
next: "no_regex"
|
||||||
|
}, {
|
||||||
|
token : "invalid",
|
||||||
|
regex: /\{\d+\b,?\d*\}[+*]|[+*$^?][+*]|[$^][?]|\?{3,}/
|
||||||
|
}, {
|
||||||
|
token : "constant.language.escape",
|
||||||
|
regex: /\(\?[:=!]|\)|\{\d+\b,?\d*\}|[+*]\?|[()$^+*?.]/
|
||||||
|
}, {
|
||||||
|
token : "constant.language.delimiter",
|
||||||
|
regex: /\|/
|
||||||
|
}, {
|
||||||
|
token: "constant.language.escape",
|
||||||
|
regex: /\[\^?/,
|
||||||
|
next: "regex_character_class"
|
||||||
|
}, {
|
||||||
|
token: "empty",
|
||||||
|
regex: "$",
|
||||||
|
next: "no_regex"
|
||||||
|
}, {
|
||||||
|
defaultToken: "string.regexp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"regex_character_class": [
|
||||||
|
{
|
||||||
|
token: "regexp.charclass.keyword.operator",
|
||||||
|
regex: "\\\\(?:u[\\da-fA-F]{4}|x[\\da-fA-F]{2}|.)"
|
||||||
|
}, {
|
||||||
|
token: "constant.language.escape",
|
||||||
|
regex: "]",
|
||||||
|
next: "regex"
|
||||||
|
}, {
|
||||||
|
token: "constant.language.escape",
|
||||||
|
regex: "-"
|
||||||
|
}, {
|
||||||
|
token: "empty",
|
||||||
|
regex: "$",
|
||||||
|
next: "no_regex"
|
||||||
|
}, {
|
||||||
|
defaultToken: "string.regexp.charachterclass"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"function_arguments": [
|
||||||
|
{
|
||||||
|
token: "variable.parameter",
|
||||||
|
regex: identifierRe
|
||||||
|
}, {
|
||||||
|
token: "punctuation.operator",
|
||||||
|
regex: "[, ]+"
|
||||||
|
}, {
|
||||||
|
token: "punctuation.operator",
|
||||||
|
regex: "$"
|
||||||
|
}, {
|
||||||
|
token: "empty",
|
||||||
|
regex: "",
|
||||||
|
next: "no_regex"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"qqstring" : [
|
||||||
|
{
|
||||||
|
token : "constant.language.escape",
|
||||||
|
regex : escapedRe
|
||||||
|
}, {
|
||||||
|
token : "string",
|
||||||
|
regex : "\\\\$",
|
||||||
|
consumeLineEnd : true
|
||||||
|
}, {
|
||||||
|
token : "string",
|
||||||
|
regex : '"|$',
|
||||||
|
next : "no_regex"
|
||||||
|
}, {
|
||||||
|
defaultToken: "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"qstring" : [
|
||||||
|
{
|
||||||
|
token : "constant.language.escape",
|
||||||
|
regex : escapedRe
|
||||||
|
}, {
|
||||||
|
token : "string",
|
||||||
|
regex : "\\\\$",
|
||||||
|
consumeLineEnd : true
|
||||||
|
}, {
|
||||||
|
token : "string",
|
||||||
|
regex : "'|$",
|
||||||
|
next : "no_regex"
|
||||||
|
}, {
|
||||||
|
defaultToken: "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (!options || !options.noES6) {
|
||||||
|
this.$rules.no_regex.unshift({
|
||||||
|
regex: "[{}]", onMatch: function(val, state, stack) {
|
||||||
|
this.next = val == "{" ? this.nextState : "";
|
||||||
|
if (val == "{" && stack.length) {
|
||||||
|
stack.unshift("start", state);
|
||||||
|
}
|
||||||
|
else if (val == "}" && stack.length) {
|
||||||
|
stack.shift();
|
||||||
|
this.next = stack.shift();
|
||||||
|
if (this.next.indexOf("string") != -1 || this.next.indexOf("jsx") != -1)
|
||||||
|
return "paren.quasi.end";
|
||||||
|
}
|
||||||
|
return val == "{" ? "paren.lparen" : "paren.rparen";
|
||||||
|
},
|
||||||
|
nextState: "start"
|
||||||
|
}, {
|
||||||
|
token : "string.quasi.start",
|
||||||
|
regex : /`/,
|
||||||
|
push : [{
|
||||||
|
token : "constant.language.escape",
|
||||||
|
regex : escapedRe
|
||||||
|
}, {
|
||||||
|
token : "paren.quasi.start",
|
||||||
|
regex : /\${/,
|
||||||
|
push : "start"
|
||||||
|
}, {
|
||||||
|
token : "string.quasi.end",
|
||||||
|
regex : /`/,
|
||||||
|
next : "pop"
|
||||||
|
}, {
|
||||||
|
defaultToken: "string.quasi"
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!options || options.jsx != false)
|
||||||
|
JSX.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.embedRules(DocCommentHighlightRules, "doc-",
|
||||||
|
[ DocCommentHighlightRules.getEndRule("no_regex") ]);
|
||||||
|
|
||||||
|
this.normalizeRules();
|
||||||
|
};
|
||||||
|
|
||||||
|
oop.inherits(JavaScriptHighlightRules, TextHighlightRules);
|
||||||
|
|
||||||
|
function JSX() {
|
||||||
|
var tagRegex = identifierRe.replace("\\d", "\\d\\-");
|
||||||
|
var jsxTag = {
|
||||||
|
onMatch : function(val, state, stack) {
|
||||||
|
var offset = val.charAt(1) == "/" ? 2 : 1;
|
||||||
|
if (offset == 1) {
|
||||||
|
if (state != this.nextState)
|
||||||
|
stack.unshift(this.next, this.nextState, 0);
|
||||||
|
else
|
||||||
|
stack.unshift(this.next);
|
||||||
|
stack[2]++;
|
||||||
|
} else if (offset == 2) {
|
||||||
|
if (state == this.nextState) {
|
||||||
|
stack[1]--;
|
||||||
|
if (!stack[1] || stack[1] < 0) {
|
||||||
|
stack.shift();
|
||||||
|
stack.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [{
|
||||||
|
type: "meta.tag.punctuation." + (offset == 1 ? "" : "end-") + "tag-open.xml",
|
||||||
|
value: val.slice(0, offset)
|
||||||
|
}, {
|
||||||
|
type: "meta.tag.tag-name.xml",
|
||||||
|
value: val.substr(offset)
|
||||||
|
}];
|
||||||
|
},
|
||||||
|
regex : "</?" + tagRegex + "",
|
||||||
|
next: "jsxAttributes",
|
||||||
|
nextState: "jsx"
|
||||||
|
};
|
||||||
|
this.$rules.start.unshift(jsxTag);
|
||||||
|
var jsxJsRule = {
|
||||||
|
regex: "{",
|
||||||
|
token: "paren.quasi.start",
|
||||||
|
push: "start"
|
||||||
|
};
|
||||||
|
this.$rules.jsx = [
|
||||||
|
jsxJsRule,
|
||||||
|
jsxTag,
|
||||||
|
{include : "reference"},
|
||||||
|
{defaultToken: "string"}
|
||||||
|
];
|
||||||
|
this.$rules.jsxAttributes = [{
|
||||||
|
token : "meta.tag.punctuation.tag-close.xml",
|
||||||
|
regex : "/?>",
|
||||||
|
onMatch : function(value, currentState, stack) {
|
||||||
|
if (currentState == stack[0])
|
||||||
|
stack.shift();
|
||||||
|
if (value.length == 2) {
|
||||||
|
if (stack[0] == this.nextState)
|
||||||
|
stack[1]--;
|
||||||
|
if (!stack[1] || stack[1] < 0) {
|
||||||
|
stack.splice(0, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.next = stack[0] || "start";
|
||||||
|
return [{type: this.token, value: value}];
|
||||||
|
},
|
||||||
|
nextState: "jsx"
|
||||||
|
},
|
||||||
|
jsxJsRule,
|
||||||
|
comments("jsxAttributes"),
|
||||||
|
{
|
||||||
|
token : "entity.other.attribute-name.xml",
|
||||||
|
regex : tagRegex
|
||||||
|
}, {
|
||||||
|
token : "keyword.operator.attribute-equals.xml",
|
||||||
|
regex : "="
|
||||||
|
}, {
|
||||||
|
token : "text.tag-whitespace.xml",
|
||||||
|
regex : "\\s+"
|
||||||
|
}, {
|
||||||
|
token : "string.attribute-value.xml",
|
||||||
|
regex : "'",
|
||||||
|
stateName : "jsx_attr_q",
|
||||||
|
push : [
|
||||||
|
{token : "string.attribute-value.xml", regex: "'", next: "pop"},
|
||||||
|
{include : "reference"},
|
||||||
|
{defaultToken : "string.attribute-value.xml"}
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
token : "string.attribute-value.xml",
|
||||||
|
regex : '"',
|
||||||
|
stateName : "jsx_attr_qq",
|
||||||
|
push : [
|
||||||
|
{token : "string.attribute-value.xml", regex: '"', next: "pop"},
|
||||||
|
{include : "reference"},
|
||||||
|
{defaultToken : "string.attribute-value.xml"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
jsxTag
|
||||||
|
];
|
||||||
|
this.$rules.reference = [{
|
||||||
|
token : "constant.language.escape.reference.xml",
|
||||||
|
regex : "(?:&#[0-9]+;)|(?:&#x[0-9a-fA-F]+;)|(?:&[a-zA-Z0-9_:\\.-]+;)"
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
function comments(next) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
token : "comment", // multi line comment
|
||||||
|
regex : /\/\*/,
|
||||||
|
next: [
|
||||||
|
DocCommentHighlightRules.getTagRule(),
|
||||||
|
{token : "comment", regex : "\\*\\/", next : next || "pop"},
|
||||||
|
{defaultToken : "comment", caseInsensitive: true}
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
token : "comment",
|
||||||
|
regex : "\\/\\/",
|
||||||
|
next: [
|
||||||
|
DocCommentHighlightRules.getTagRule(),
|
||||||
|
{token : "comment", regex : "$|^", next : next || "pop"},
|
||||||
|
{defaultToken : "comment", caseInsensitive: true}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
exports.JavaScriptHighlightRules = JavaScriptHighlightRules;
|
||||||
|
});
|
||||||
|
|
||||||
|
define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"], function(require, exports, module) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var Range = require("../range").Range;
|
||||||
|
|
||||||
|
var MatchingBraceOutdent = function() {};
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
|
||||||
|
this.checkOutdent = function(line, input) {
|
||||||
|
if (! /^\s+$/.test(line))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return /^\s*\}/.test(input);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.autoOutdent = function(doc, row) {
|
||||||
|
var line = doc.getLine(row);
|
||||||
|
var match = line.match(/^(\s*\})/);
|
||||||
|
|
||||||
|
if (!match) return 0;
|
||||||
|
|
||||||
|
var column = match[1].length;
|
||||||
|
var openBracePos = doc.findMatchingBracket({row: row, column: column});
|
||||||
|
|
||||||
|
if (!openBracePos || openBracePos.row == row) return 0;
|
||||||
|
|
||||||
|
var indent = this.$getIndent(doc.getLine(openBracePos.row));
|
||||||
|
doc.replace(new Range(row, 0, row, column-1), indent);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$getIndent = function(line) {
|
||||||
|
return line.match(/^\s*/)[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
}).call(MatchingBraceOutdent.prototype);
|
||||||
|
|
||||||
|
exports.MatchingBraceOutdent = MatchingBraceOutdent;
|
||||||
|
});
|
||||||
|
|
||||||
|
define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"], function(require, exports, module) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var oop = require("../../lib/oop");
|
||||||
|
var Range = require("../../range").Range;
|
||||||
|
var BaseFoldMode = require("./fold_mode").FoldMode;
|
||||||
|
|
||||||
|
var FoldMode = exports.FoldMode = function(commentRegex) {
|
||||||
|
if (commentRegex) {
|
||||||
|
this.foldingStartMarker = new RegExp(
|
||||||
|
this.foldingStartMarker.source.replace(/\|[^|]*?$/, "|" + commentRegex.start)
|
||||||
|
);
|
||||||
|
this.foldingStopMarker = new RegExp(
|
||||||
|
this.foldingStopMarker.source.replace(/\|[^|]*?$/, "|" + commentRegex.end)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
oop.inherits(FoldMode, BaseFoldMode);
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
|
||||||
|
this.foldingStartMarker = /([\{\[\(])[^\}\]\)]*$|^\s*(\/\*)/;
|
||||||
|
this.foldingStopMarker = /^[^\[\{\(]*([\}\]\)])|^[\s\*]*(\*\/)/;
|
||||||
|
this.singleLineBlockCommentRe= /^\s*(\/\*).*\*\/\s*$/;
|
||||||
|
this.tripleStarBlockCommentRe = /^\s*(\/\*\*\*).*\*\/\s*$/;
|
||||||
|
this.startRegionRe = /^\s*(\/\*|\/\/)#?region\b/;
|
||||||
|
this._getFoldWidgetBase = this.getFoldWidget;
|
||||||
|
this.getFoldWidget = function(session, foldStyle, row) {
|
||||||
|
var line = session.getLine(row);
|
||||||
|
|
||||||
|
if (this.singleLineBlockCommentRe.test(line)) {
|
||||||
|
if (!this.startRegionRe.test(line) && !this.tripleStarBlockCommentRe.test(line))
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
var fw = this._getFoldWidgetBase(session, foldStyle, row);
|
||||||
|
|
||||||
|
if (!fw && this.startRegionRe.test(line))
|
||||||
|
return "start"; // lineCommentRegionStart
|
||||||
|
|
||||||
|
return fw;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getFoldWidgetRange = function(session, foldStyle, row, forceMultiline) {
|
||||||
|
var line = session.getLine(row);
|
||||||
|
|
||||||
|
if (this.startRegionRe.test(line))
|
||||||
|
return this.getCommentRegionBlock(session, line, row);
|
||||||
|
|
||||||
|
var match = line.match(this.foldingStartMarker);
|
||||||
|
if (match) {
|
||||||
|
var i = match.index;
|
||||||
|
|
||||||
|
if (match[1])
|
||||||
|
return this.openingBracketBlock(session, match[1], row, i);
|
||||||
|
|
||||||
|
var range = session.getCommentFoldRange(row, i + match[0].length, 1);
|
||||||
|
|
||||||
|
if (range && !range.isMultiLine()) {
|
||||||
|
if (forceMultiline) {
|
||||||
|
range = this.getSectionRange(session, row);
|
||||||
|
} else if (foldStyle != "all")
|
||||||
|
range = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foldStyle === "markbegin")
|
||||||
|
return;
|
||||||
|
|
||||||
|
var match = line.match(this.foldingStopMarker);
|
||||||
|
if (match) {
|
||||||
|
var i = match.index + match[0].length;
|
||||||
|
|
||||||
|
if (match[1])
|
||||||
|
return this.closingBracketBlock(session, match[1], row, i);
|
||||||
|
|
||||||
|
return session.getCommentFoldRange(row, i, -1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getSectionRange = function(session, row) {
|
||||||
|
var line = session.getLine(row);
|
||||||
|
var startIndent = line.search(/\S/);
|
||||||
|
var startRow = row;
|
||||||
|
var startColumn = line.length;
|
||||||
|
row = row + 1;
|
||||||
|
var endRow = row;
|
||||||
|
var maxRow = session.getLength();
|
||||||
|
while (++row < maxRow) {
|
||||||
|
line = session.getLine(row);
|
||||||
|
var indent = line.search(/\S/);
|
||||||
|
if (indent === -1)
|
||||||
|
continue;
|
||||||
|
if (startIndent > indent)
|
||||||
|
break;
|
||||||
|
var subRange = this.getFoldWidgetRange(session, "all", row);
|
||||||
|
|
||||||
|
if (subRange) {
|
||||||
|
if (subRange.start.row <= startRow) {
|
||||||
|
break;
|
||||||
|
} else if (subRange.isMultiLine()) {
|
||||||
|
row = subRange.end.row;
|
||||||
|
} else if (startIndent == indent) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endRow = row;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Range(startRow, startColumn, endRow, session.getLine(endRow).length);
|
||||||
|
};
|
||||||
|
this.getCommentRegionBlock = function(session, line, row) {
|
||||||
|
var startColumn = line.search(/\s*$/);
|
||||||
|
var maxRow = session.getLength();
|
||||||
|
var startRow = row;
|
||||||
|
|
||||||
|
var re = /^\s*(?:\/\*|\/\/|--)#?(end)?region\b/;
|
||||||
|
var depth = 1;
|
||||||
|
while (++row < maxRow) {
|
||||||
|
line = session.getLine(row);
|
||||||
|
var m = re.exec(line);
|
||||||
|
if (!m) continue;
|
||||||
|
if (m[1]) depth--;
|
||||||
|
else depth++;
|
||||||
|
|
||||||
|
if (!depth) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var endRow = row;
|
||||||
|
if (endRow > startRow) {
|
||||||
|
return new Range(startRow, startColumn, endRow, line.length);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}).call(FoldMode.prototype);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
define("ace/mode/javascript",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/javascript_highlight_rules","ace/mode/matching_brace_outdent","ace/worker/worker_client","ace/mode/behaviour/cstyle","ace/mode/folding/cstyle"], function(require, exports, module) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var oop = require("../lib/oop");
|
||||||
|
var TextMode = require("./text").Mode;
|
||||||
|
var JavaScriptHighlightRules = require("./javascript_highlight_rules").JavaScriptHighlightRules;
|
||||||
|
var MatchingBraceOutdent = require("./matching_brace_outdent").MatchingBraceOutdent;
|
||||||
|
var WorkerClient = require("../worker/worker_client").WorkerClient;
|
||||||
|
var CstyleBehaviour = require("./behaviour/cstyle").CstyleBehaviour;
|
||||||
|
var CStyleFoldMode = require("./folding/cstyle").FoldMode;
|
||||||
|
|
||||||
|
var Mode = function() {
|
||||||
|
this.HighlightRules = JavaScriptHighlightRules;
|
||||||
|
|
||||||
|
this.$outdent = new MatchingBraceOutdent();
|
||||||
|
this.$behaviour = new CstyleBehaviour();
|
||||||
|
this.foldingRules = new CStyleFoldMode();
|
||||||
|
};
|
||||||
|
oop.inherits(Mode, TextMode);
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
|
||||||
|
this.lineCommentStart = "//";
|
||||||
|
this.blockComment = {start: "/*", end: "*/"};
|
||||||
|
this.$quotes = {'"': '"', "'": "'", "`": "`"};
|
||||||
|
|
||||||
|
this.getNextLineIndent = function(state, line, tab) {
|
||||||
|
var indent = this.$getIndent(line);
|
||||||
|
|
||||||
|
var tokenizedLine = this.getTokenizer().getLineTokens(line, state);
|
||||||
|
var tokens = tokenizedLine.tokens;
|
||||||
|
var endState = tokenizedLine.state;
|
||||||
|
|
||||||
|
if (tokens.length && tokens[tokens.length-1].type == "comment") {
|
||||||
|
return indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == "start" || state == "no_regex") {
|
||||||
|
var match = line.match(/^.*(?:\bcase\b.*:|[\{\(\[])\s*$/);
|
||||||
|
if (match) {
|
||||||
|
indent += tab;
|
||||||
|
}
|
||||||
|
} else if (state == "doc-start") {
|
||||||
|
if (endState == "start" || endState == "no_regex") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
var match = line.match(/^\s*(\/?)\*/);
|
||||||
|
if (match) {
|
||||||
|
if (match[1]) {
|
||||||
|
indent += " ";
|
||||||
|
}
|
||||||
|
indent += "* ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return indent;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.checkOutdent = function(state, line, input) {
|
||||||
|
return this.$outdent.checkOutdent(line, input);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.autoOutdent = function(state, doc, row) {
|
||||||
|
this.$outdent.autoOutdent(doc, row);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.createWorker = function(session) {
|
||||||
|
var worker = new WorkerClient(["ace"], "ace/mode/javascript_worker", "JavaScriptWorker");
|
||||||
|
worker.attachToDocument(session.getDocument());
|
||||||
|
|
||||||
|
worker.on("annotate", function(results) {
|
||||||
|
session.setAnnotations(results.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.on("terminate", function() {
|
||||||
|
session.clearAnnotations();
|
||||||
|
});
|
||||||
|
|
||||||
|
return worker;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$id = "ace/mode/javascript";
|
||||||
|
}).call(Mode.prototype);
|
||||||
|
|
||||||
|
exports.Mode = Mode;
|
||||||
|
}); (function() {
|
||||||
|
window.require(["ace/mode/javascript"], function(m) {
|
||||||
|
if (typeof module == "object" && typeof exports == "object" && module) {
|
||||||
|
module.exports = m;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
BIN
docs/images/background.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/images/edit.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
docs/images/error.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/images/exec.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/images/follow_cursor.gif
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/images/immutable.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
docs/images/inspect.gif
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/images/mutation.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/images/nav.gif
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/images/self-hosted.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
docs/images/test.png
Normal file
|
After Width: | Height: | Size: 219 KiB |
347
index.html
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
|
|
||||||
|
<script src='ace/ace.js'></script>
|
||||||
|
<script src='ace/keybinding-vim.js'></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--shadow_color: rgb(171 200 214);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0px;
|
||||||
|
/* same as ace editor */
|
||||||
|
font-family: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::backdrop {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
"code eval"
|
||||||
|
"bottom bottom"
|
||||||
|
"statusbar statusbar";
|
||||||
|
grid-template-columns: 60% 40%;
|
||||||
|
grid-template-rows: 1fr 0.7fr 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root.embed_value_explorer {
|
||||||
|
grid-template-areas:
|
||||||
|
"code code"
|
||||||
|
"bottom files"
|
||||||
|
"statusbar statusbar";
|
||||||
|
grid-template-columns: 70% 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor_container, .bottom, .eval, .files_container, .statusbar {
|
||||||
|
box-shadow: 1px 1px 3px 0px var(--shadow_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor_container, .eval, .bottom, .statusbar, .files_container {
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor_container:focus-within,
|
||||||
|
.bottom:focus-within,
|
||||||
|
.eval:focus-within,
|
||||||
|
.files_container:focus-within,
|
||||||
|
.help_dialog {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 1px 1px 6px 3px var(--shadow_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calltree:focus-within, .problems:focus-within {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor_container {
|
||||||
|
position: relative;
|
||||||
|
grid-area: code;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eval {
|
||||||
|
display: grid;
|
||||||
|
grid-area: eval;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eval_content {
|
||||||
|
padding: 5px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ace markers */
|
||||||
|
|
||||||
|
.selection {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #ff00ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evaluated_ok {
|
||||||
|
position: absolute;
|
||||||
|
background-color: rgb(225, 244, 253);
|
||||||
|
}
|
||||||
|
.evaluated_error {
|
||||||
|
position: absolute;
|
||||||
|
background-color: #ff000024;
|
||||||
|
}
|
||||||
|
.error-code {
|
||||||
|
/*
|
||||||
|
TODO: make underline like in all editors
|
||||||
|
*/
|
||||||
|
position: absolute;
|
||||||
|
border-bottom: 7px solid red;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* end of ace markers */
|
||||||
|
|
||||||
|
.eval_error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* calltree */
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
grid-area: bottom;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calltree, .problems {
|
||||||
|
padding: 5px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entrypoint_select {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entrypoint_title {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callnode {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
.callnode .active {
|
||||||
|
background-color: rgb(225, 244, 253);
|
||||||
|
}
|
||||||
|
.call_header {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.call_header.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.call_header.error.native {
|
||||||
|
color: red;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.call_header.native {
|
||||||
|
font-style: italic;
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* problems view */
|
||||||
|
.problem a {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* files */
|
||||||
|
|
||||||
|
.files_container {
|
||||||
|
overflow: auto;
|
||||||
|
grid-area: files;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.allow_file_access {
|
||||||
|
height: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.allow_file_access .subtitle {
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files {
|
||||||
|
overflow: auto;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files .file {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files > .file {
|
||||||
|
margin-left: 0em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files .file_title.active {
|
||||||
|
background-color: rgb(225, 244, 253);
|
||||||
|
}
|
||||||
|
|
||||||
|
.files .file_title .icon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 5px;
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file_actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: rgb(225 244 253 / 80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* value_explorer */
|
||||||
|
|
||||||
|
.embed_value_explorer_container {
|
||||||
|
height: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed_value_explorer_wrapper {
|
||||||
|
margin-left: 1em;
|
||||||
|
/* preserve wrapper from getting clicks for code line left to it */
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed_value_explorer_content {
|
||||||
|
pointer-events: initial;
|
||||||
|
white-space: pre;
|
||||||
|
max-width: fit-content;
|
||||||
|
background-color: white;
|
||||||
|
box-shadow: 1px 2px 3px -1px var(--shadow_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed_value_explorer_content:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 1px 2px 11px 1px var(--shadow_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed_value_explorer_content .value_explorer_node {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value_explorer_node {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value_explorer_header.active {
|
||||||
|
background-color: rgb(148, 227, 191);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value_explorer_key {
|
||||||
|
color: rgb(150, 0, 128);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* status */
|
||||||
|
|
||||||
|
/*
|
||||||
|
.request_fullscreen {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
.statusbar {
|
||||||
|
grid-area: statusbar;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.status, .current_file {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.options {
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
.options > * {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show_help, .github {
|
||||||
|
margin: 0em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help_dialog[open] {
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
width: 70%;
|
||||||
|
height: 70%;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help_dialog::backdrop {
|
||||||
|
background-color: rgb(225 244 253 / 80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help {
|
||||||
|
padding: 2em;
|
||||||
|
border-spacing: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help th {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help th.key {
|
||||||
|
width: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help td.key {
|
||||||
|
background-color: rgb(225, 244, 253, 0.5);
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script type='module'>
|
||||||
|
// TODO remove
|
||||||
|
window.log = console.log
|
||||||
|
|
||||||
|
import {init} from './src/index.js'
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
init(document.body)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
3
package.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type" : "module"
|
||||||
|
}
|
||||||
30
service_worker.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
Should prevent navigator.serviceWorker.controller from being null on first load, but doesn't work for some reason.
|
||||||
|
TODO: compare with
|
||||||
|
https://googlechrome.github.io/samples/service-worker/post-message/
|
||||||
|
which seems to work on first load
|
||||||
|
|
||||||
|
self.addEventListener('install', function(event) {
|
||||||
|
//event.waitUntil(self.skipWaiting()); // Activate worker immediately
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', function(event) {
|
||||||
|
event.waitUntil(self.clients.claim()); // Become available to all pages
|
||||||
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
|
let data
|
||||||
|
|
||||||
|
self.addEventListener('message', async function(e) {
|
||||||
|
const msg = e.data
|
||||||
|
let reply
|
||||||
|
if(msg.type == 'SET') {
|
||||||
|
data = msg.data
|
||||||
|
reply = null
|
||||||
|
} else if(msg.type == 'GET') {
|
||||||
|
reply = data
|
||||||
|
} else {
|
||||||
|
throw new Error('unknown message type: ' + msg.type)
|
||||||
|
}
|
||||||
|
e.ports[0].postMessage(reply)
|
||||||
|
})
|
||||||
167
src/ast_utils.js
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import {uniq} from './utils.js'
|
||||||
|
|
||||||
|
export const collect_destructuring_identifiers = node => {
|
||||||
|
if(Array.isArray(node)) {
|
||||||
|
return node.map(collect_destructuring_identifiers).flat()
|
||||||
|
} else if(node.type == 'identifier') {
|
||||||
|
return [node]
|
||||||
|
} else if(['destructuring_default', 'destructuring_rest'].includes(node.type)){
|
||||||
|
return collect_destructuring_identifiers(node.name_node)
|
||||||
|
} else if(node.type == 'destructuring_pair') {
|
||||||
|
return collect_destructuring_identifiers(node.value)
|
||||||
|
} else if(
|
||||||
|
['array_destructuring', 'object_destructuring', 'function_args']
|
||||||
|
.includes(node.type)
|
||||||
|
) {
|
||||||
|
return node.elements
|
||||||
|
.map(collect_destructuring_identifiers)
|
||||||
|
.flat()
|
||||||
|
} else {
|
||||||
|
console.error(node)
|
||||||
|
throw new Error('not implemented')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const map_destructuring_identifiers = (node, mapper) => {
|
||||||
|
const map = node => map_destructuring_identifiers(node, mapper)
|
||||||
|
if(node.type == 'identifier') {
|
||||||
|
return mapper(node)
|
||||||
|
} else if(node.type == 'destructuring_default') {
|
||||||
|
return {...node, children: [map(node.children[0]), node.children[1]]}
|
||||||
|
} else if(node.type == 'destructuring_rest') {
|
||||||
|
return {...node, children: [mapper(node.children[0])]}
|
||||||
|
} else if(node.type == 'destructuring_pair') {
|
||||||
|
return {...node, children: [node.children[0], map(node.children[1])]}
|
||||||
|
} else if(node.type == 'array_destructuring' || node.type == 'object_destructuring') {
|
||||||
|
return {...node, children: node.children.map(map)}
|
||||||
|
} else {
|
||||||
|
console.error(node)
|
||||||
|
throw new Error('not implemented')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const collect_imports = module => {
|
||||||
|
const imports = module.stmts
|
||||||
|
.filter(n => n.type == 'import')
|
||||||
|
.map(n =>
|
||||||
|
n.imports.map(i =>
|
||||||
|
({name: i.value, module: n.full_import_path})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.flat()
|
||||||
|
|
||||||
|
const modules = uniq(imports.map(i => i.module))
|
||||||
|
const by_module = Object.fromEntries(modules.map(m =>
|
||||||
|
[
|
||||||
|
m,
|
||||||
|
imports
|
||||||
|
.filter(i => i.module == m)
|
||||||
|
.map(i => i.name)
|
||||||
|
]
|
||||||
|
))
|
||||||
|
return by_module
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const find_leaf = (node, index) => {
|
||||||
|
if(!(node.index <= index && node.index + node.length > index)){
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
if(node.children == null){
|
||||||
|
return node
|
||||||
|
} else {
|
||||||
|
const children = node.children.map(n => find_leaf(n, index))
|
||||||
|
const child = children.find(c => c != null)
|
||||||
|
return child || node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const is_child = (child, parent) => {
|
||||||
|
return parent.index <= child.index &&
|
||||||
|
(parent.index + parent.length) >= child.index + child.length
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO inconsistency. Sometimes we compare by identity, sometimes by this
|
||||||
|
// function
|
||||||
|
export const is_eq = (a, b) => {
|
||||||
|
return a.index == b.index && a.length == b.length
|
||||||
|
// Two different nodes can have the same index and length. Currently there
|
||||||
|
// is only one case: single-child do statement and its only child. So we
|
||||||
|
// add `type` to comparison. Better refactor it and add unique id to every
|
||||||
|
// node? Maybe also include `module` to id?
|
||||||
|
&& a.type == b.type
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ancestry = (child, parent) => {
|
||||||
|
if(is_eq(parent, child)){
|
||||||
|
return []
|
||||||
|
} else {
|
||||||
|
if(parent.children == null){
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
const c = parent.children.find(c => is_child(child, c))
|
||||||
|
if(c == null){
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
return ancestry(child, c).concat([parent])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ancestry_inc = (child, parent) =>
|
||||||
|
[child, ...ancestry(child, parent)]
|
||||||
|
|
||||||
|
export const find_fn_by_location = (node, loc) => {
|
||||||
|
if(
|
||||||
|
node.index == loc.index && node.length == loc.length
|
||||||
|
// see comment for is_eq
|
||||||
|
&& node.type == 'function_expr'
|
||||||
|
) {
|
||||||
|
return node
|
||||||
|
} else if(node.children == null){
|
||||||
|
throw new Error('illegal state')
|
||||||
|
} else {
|
||||||
|
const c = node.children.find(c => is_child(loc, c))
|
||||||
|
if(c == null){
|
||||||
|
throw new Error('illegal state')
|
||||||
|
} else {
|
||||||
|
return find_fn_by_location(c, loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const find_node = (node, pred) => {
|
||||||
|
if(pred(node)) {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
if(node.children == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
.children
|
||||||
|
.reduce(
|
||||||
|
(result, c) => result ?? find_node(c, pred),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const find_error_origin_node = node =>
|
||||||
|
find_node(
|
||||||
|
node,
|
||||||
|
n => n.result != null && !n.result.ok && n.result.error != null
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Maps tree nodes, discarding mapped children, so maps only node contents, not
|
||||||
|
* allowing to modify structure */
|
||||||
|
export const map_tree = (node, mapper) => {
|
||||||
|
const mapped = mapper(node)
|
||||||
|
if(node.children == null) {
|
||||||
|
return mapped
|
||||||
|
}
|
||||||
|
return {...mapped,
|
||||||
|
children: node.children.map(c => map_tree(c, mapper))
|
||||||
|
}
|
||||||
|
}
|
||||||
782
src/calltree.js
Normal file
@@ -0,0 +1,782 @@
|
|||||||
|
import {map_accum, map_find, map_object, stringify, findLast} from './utils.js'
|
||||||
|
import {is_eq, find_error_origin_node} from './ast_utils.js'
|
||||||
|
import {find_node, find_leaf, ancestry_inc} from './ast_utils.js'
|
||||||
|
import {color} from './color.js'
|
||||||
|
import {eval_frame} from './eval.js'
|
||||||
|
|
||||||
|
export const pp_calltree = calltree => map_object(
|
||||||
|
calltree,
|
||||||
|
(module, {exports, calls}) => stringify(do_pp_calltree(calls))
|
||||||
|
)
|
||||||
|
|
||||||
|
const do_pp_calltree = tree => ({
|
||||||
|
id: tree.id,
|
||||||
|
has_more_children: tree.has_more_children,
|
||||||
|
string: tree.code.string,
|
||||||
|
children: tree.children && tree.children.map(do_pp_calltree)
|
||||||
|
})
|
||||||
|
|
||||||
|
const is_stackoverflow = node =>
|
||||||
|
// Chrome
|
||||||
|
node.error.message == 'Maximum call stack size exceeded'
|
||||||
|
||
|
||||||
|
// Firefox
|
||||||
|
node.error.message == "too much recursion"
|
||||||
|
|
||||||
|
export const calltree_node_loc = node => node.toplevel
|
||||||
|
? {module: node.module}
|
||||||
|
: node.fn.__location
|
||||||
|
|
||||||
|
// Finds the last module that was evaluated. If all modules evaluated without
|
||||||
|
// problems, then it is the entrypoint module. Else it is the module that
|
||||||
|
// throws error
|
||||||
|
export const root_calltree_module = state =>
|
||||||
|
findLast(
|
||||||
|
state.parse_result.sorted,
|
||||||
|
module => state.calltree[module] != null
|
||||||
|
)
|
||||||
|
|
||||||
|
export const root_calltree_node = state =>
|
||||||
|
state.calltree[root_calltree_module(state)].calls
|
||||||
|
|
||||||
|
export const is_native_fn = calltree_node =>
|
||||||
|
!calltree_node.toplevel && calltree_node.fn.__location == null
|
||||||
|
|
||||||
|
export const active_frame = state =>
|
||||||
|
state.frames[state.active_calltree_node.id]
|
||||||
|
|
||||||
|
const get_calltree_node_by_loc = (state, node) =>
|
||||||
|
state.calltree_node_by_loc
|
||||||
|
?.[state.current_module]
|
||||||
|
?.[
|
||||||
|
state.parse_result.modules[state.current_module] == node
|
||||||
|
// identify toplevel by index `-1`, because function and toplevel can
|
||||||
|
// have the same index (in case when module starts with function_expr)
|
||||||
|
? -1
|
||||||
|
: node.index
|
||||||
|
]
|
||||||
|
|
||||||
|
const add_calltree_node_by_loc = (state, loc, node_id) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
calltree_node_by_loc:
|
||||||
|
{...state.calltree_node_by_loc,
|
||||||
|
[loc.module]: {
|
||||||
|
...state.calltree_node_by_loc?.[loc.module],
|
||||||
|
[loc.index ?? -1]: node_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const set_active_calltree_node = (
|
||||||
|
state,
|
||||||
|
active_calltree_node,
|
||||||
|
current_calltree_node = state.current_calltree_node,
|
||||||
|
) => {
|
||||||
|
const result = {
|
||||||
|
...state,
|
||||||
|
active_calltree_node,
|
||||||
|
current_calltree_node,
|
||||||
|
}
|
||||||
|
// TODO currently commented, required to implement livecoding second and
|
||||||
|
// subsequent fn calls
|
||||||
|
/*
|
||||||
|
// Record last_good_state every time active_calltree_node changes
|
||||||
|
return {...result, last_good_state: result}
|
||||||
|
*/
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const add_frame = (
|
||||||
|
state,
|
||||||
|
active_calltree_node,
|
||||||
|
current_calltree_node = active_calltree_node,
|
||||||
|
) => {
|
||||||
|
let with_frame
|
||||||
|
if(state.frames?.[active_calltree_node.id] == null) {
|
||||||
|
const frame = eval_frame(active_calltree_node, state.calltree)
|
||||||
|
const coloring = color(frame)
|
||||||
|
with_frame = {...state,
|
||||||
|
frames: {...state.frames,
|
||||||
|
[active_calltree_node.id]: {...frame, coloring}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
with_frame = state
|
||||||
|
}
|
||||||
|
const result = add_calltree_node_by_loc(
|
||||||
|
with_frame,
|
||||||
|
calltree_node_loc(active_calltree_node),
|
||||||
|
active_calltree_node.id,
|
||||||
|
)
|
||||||
|
return set_active_calltree_node(result, active_calltree_node, current_calltree_node)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const replace_calltree_node = (root, node, replacement) => {
|
||||||
|
const do_replace = root => {
|
||||||
|
if(root.id == node.id) {
|
||||||
|
return [true, replacement]
|
||||||
|
}
|
||||||
|
|
||||||
|
if(root.children == null) {
|
||||||
|
return [false, root]
|
||||||
|
}
|
||||||
|
|
||||||
|
const [replaced, children] = map_accum(
|
||||||
|
(replaced, c) => replaced
|
||||||
|
// Already replaced, do not look for replacement
|
||||||
|
? [true, c]
|
||||||
|
: do_replace(c),
|
||||||
|
false,
|
||||||
|
root.children,
|
||||||
|
)
|
||||||
|
|
||||||
|
if(replaced) {
|
||||||
|
return [true, {...root, children}]
|
||||||
|
} else {
|
||||||
|
return [false, root]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [replaced, result] = do_replace(root)
|
||||||
|
|
||||||
|
if(!replaced) {
|
||||||
|
throw new Error('illegal state')
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const expand_calltree_node = (state, node) => {
|
||||||
|
if(node.has_more_children) {
|
||||||
|
const root = root_calltree_module(state)
|
||||||
|
const next_node = state.calltree_actions.expand_calltree_node(node)
|
||||||
|
// Update calltree, replacing node with expanded node
|
||||||
|
const {exports, calls} = state.calltree[root]
|
||||||
|
const calltree = {...state.calltree,
|
||||||
|
[root]: {
|
||||||
|
exports,
|
||||||
|
calls: replace_calltree_node(calls, node, next_node),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {state: {...state, calltree}, node: next_node}
|
||||||
|
} else {
|
||||||
|
return {state, node}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const jump_calltree_node = (_state, _current_calltree_node) => {
|
||||||
|
const {state, node: current_calltree_node} = expand_calltree_node(
|
||||||
|
_state, _current_calltree_node
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
When node is selected or expanded/collapsed
|
||||||
|
If native, goto call site
|
||||||
|
If hosted
|
||||||
|
If parent is native
|
||||||
|
goto inside fn
|
||||||
|
If parent is hosted
|
||||||
|
If expanded, goto inside fn
|
||||||
|
If collapsed, goto call site
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Whether to show fn body (true) or callsite (false) */
|
||||||
|
let show_body
|
||||||
|
const [parent] = path_to_root(
|
||||||
|
root_calltree_node(state),
|
||||||
|
current_calltree_node
|
||||||
|
)
|
||||||
|
if(current_calltree_node.toplevel) {
|
||||||
|
show_body = true
|
||||||
|
} else if(is_native_fn(current_calltree_node)) {
|
||||||
|
show_body = false
|
||||||
|
} else {
|
||||||
|
if(is_native_fn(parent)) {
|
||||||
|
show_body = true
|
||||||
|
} else {
|
||||||
|
const is_expanded = state.calltree_node_is_expanded[current_calltree_node.id]
|
||||||
|
show_body = is_expanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const active_calltree_node = show_body ? current_calltree_node : parent
|
||||||
|
|
||||||
|
const next = add_frame(state, active_calltree_node, current_calltree_node)
|
||||||
|
|
||||||
|
const loc = show_body
|
||||||
|
? calltree_node_loc(next.active_calltree_node)
|
||||||
|
: find_callsite(next.calltree, active_calltree_node, current_calltree_node)
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: {...next, current_module: loc.module},
|
||||||
|
effects: next.current_calltree_node.toplevel
|
||||||
|
? {type: 'unembed_value_explorer'}
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
type: 'set_caret_position',
|
||||||
|
// TODO: better jump not start of function (arguments), but start
|
||||||
|
// of body?
|
||||||
|
args: [loc.index],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'embed_value_explorer',
|
||||||
|
args: [{
|
||||||
|
index: loc.index,
|
||||||
|
result: {
|
||||||
|
ok: true,
|
||||||
|
value: current_calltree_node.ok
|
||||||
|
? {
|
||||||
|
'*arguments*': current_calltree_node.args,
|
||||||
|
'*return*': current_calltree_node.value,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
'*arguments*': current_calltree_node.args,
|
||||||
|
'*throws*': current_calltree_node.error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const path_to_root = (root, child) => {
|
||||||
|
const do_path = (root) => {
|
||||||
|
if(root.id == child.id) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if(root.children == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return root.children.reduce(
|
||||||
|
(result, c) => {
|
||||||
|
if(result != null) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
const path = do_path(c)
|
||||||
|
if(path == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return [...path, root]
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = do_path(root)
|
||||||
|
|
||||||
|
if(result == null) {
|
||||||
|
throw new Error('illegal state')
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const is_expandable = node =>
|
||||||
|
// Hosted node always can be expanded, even if has not children
|
||||||
|
// Toplevel cannot be expanded if has no children
|
||||||
|
(!is_native_fn(node) && !node.toplevel)
|
||||||
|
||
|
||||||
|
(node.children != null || node.has_more_children)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Right -
|
||||||
|
- does not has children - nothing
|
||||||
|
- has children - first click expands, second jumps to first element
|
||||||
|
|
||||||
|
Left -
|
||||||
|
- root - nothing
|
||||||
|
- not root collapse node, goes to parent if already collapsed
|
||||||
|
|
||||||
|
Up - goes to prev visible element
|
||||||
|
Down - goes to next visible element
|
||||||
|
|
||||||
|
Click - select and toggle expand
|
||||||
|
|
||||||
|
step_into - select and expand
|
||||||
|
*/
|
||||||
|
|
||||||
|
const arrow_down = state => {
|
||||||
|
const current = state.current_calltree_node
|
||||||
|
let next_node
|
||||||
|
|
||||||
|
if(
|
||||||
|
is_expandable(current)
|
||||||
|
&& state.calltree_node_is_expanded[current.id]
|
||||||
|
&& current.children != null
|
||||||
|
) {
|
||||||
|
|
||||||
|
next_node = current.children[0]
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const next = (n, path) => {
|
||||||
|
if(n == root_calltree_node(state)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const [parent, ...grandparents] = path
|
||||||
|
const child_index = parent.children.findIndex(c =>
|
||||||
|
c == n
|
||||||
|
)
|
||||||
|
const next_child = parent.children[child_index + 1]
|
||||||
|
if(next_child == null) {
|
||||||
|
return next(parent, grandparents)
|
||||||
|
} else {
|
||||||
|
return next_child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next_node = next(
|
||||||
|
current,
|
||||||
|
path_to_root(root_calltree_node(state), current)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return next_node == null
|
||||||
|
? state
|
||||||
|
: jump_calltree_node(state, next_node)
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrow_up = state => {
|
||||||
|
const current = state.current_calltree_node
|
||||||
|
if(current == root_calltree_node(state)) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
const [parent] = path_to_root(root_calltree_node(state), current)
|
||||||
|
const child_index = parent.children.findIndex(c =>
|
||||||
|
c == current
|
||||||
|
)
|
||||||
|
const next_child = parent.children[child_index - 1]
|
||||||
|
let next_node
|
||||||
|
if(next_child == null) {
|
||||||
|
next_node = parent
|
||||||
|
} else {
|
||||||
|
const last = node => {
|
||||||
|
if(
|
||||||
|
!is_expandable(node)
|
||||||
|
|| !state.calltree_node_is_expanded[node.id]
|
||||||
|
|| node.children == null
|
||||||
|
) {
|
||||||
|
return node
|
||||||
|
} else {
|
||||||
|
return last(node.children[node.children.length - 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next_node = last(next_child)
|
||||||
|
}
|
||||||
|
return jump_calltree_node(state, next_node)
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrow_left = state => {
|
||||||
|
const current = state.current_calltree_node
|
||||||
|
const is_expanded = state.calltree_node_is_expanded[current.id]
|
||||||
|
if(!is_expandable(current) || !is_expanded) {
|
||||||
|
if(current != root_calltree_node(state)) {
|
||||||
|
const [parent] = path_to_root(root_calltree_node(state), current)
|
||||||
|
return jump_calltree_node(state, parent)
|
||||||
|
} else {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return toggle_expanded(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrow_right = state => {
|
||||||
|
const current = state.current_calltree_node
|
||||||
|
if(is_expandable(current)) {
|
||||||
|
const is_expanded = state.calltree_node_is_expanded[current.id]
|
||||||
|
if(!is_expanded) {
|
||||||
|
return toggle_expanded(state)
|
||||||
|
} else {
|
||||||
|
if(current.children != null) {
|
||||||
|
return jump_calltree_node(state, current.children[0])
|
||||||
|
} else {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const find_callsite = (calltree, parent, node) => {
|
||||||
|
const frame = eval_frame(parent, calltree)
|
||||||
|
const result = find_node(frame, n => n.result?.call == node)
|
||||||
|
return {module: calltree_node_loc(parent).module, index: result.index}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggle_expanded = (state, is_exp) => {
|
||||||
|
const node_id = state.current_calltree_node.id
|
||||||
|
const prev = state.calltree_node_is_expanded[node_id]
|
||||||
|
const next_is_exp = is_exp ?? !prev
|
||||||
|
const expanded_state = {
|
||||||
|
...state,
|
||||||
|
calltree_node_is_expanded: {
|
||||||
|
...state.calltree_node_is_expanded,
|
||||||
|
[node_id]: next_is_exp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jump_calltree_node(
|
||||||
|
expanded_state,
|
||||||
|
state.current_calltree_node,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const click = (state, id) => {
|
||||||
|
const node = find_node(root_calltree_node(state), n => n.id == id)
|
||||||
|
// Effects are intentionally discarded, correct `set_caret_position` will be
|
||||||
|
// applied in `toggle_expanded`
|
||||||
|
const {state: nextstate, effects} = jump_calltree_node(state, node)
|
||||||
|
if(is_expandable(node)) {
|
||||||
|
return toggle_expanded(nextstate)
|
||||||
|
} else {
|
||||||
|
return {state: nextstate}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
After find_call, we have two calltrees that have common subtree (starting from
|
||||||
|
root node). Nodes in these subtrees are the same, but have different ids. Copy
|
||||||
|
nodes that are in the second tree that are not in the first tree
|
||||||
|
*/
|
||||||
|
const merge_calltrees = (prev, next) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(prev).map(([module, {exports, calls}]) =>
|
||||||
|
[
|
||||||
|
module,
|
||||||
|
{exports, calls: merge_calltree_nodes(calls, next[module].calls)[1]}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const merge_calltree_nodes = (a, b) => {
|
||||||
|
// TODO Quick workaround, should be fixed by not saving stack in eval.js in
|
||||||
|
// case of stackoverflow
|
||||||
|
if(!a.ok && is_stackoverflow(a)) {
|
||||||
|
return [false, a]
|
||||||
|
}
|
||||||
|
|
||||||
|
if(a.has_more_children) {
|
||||||
|
if(b.has_more_children) {
|
||||||
|
return [false, a]
|
||||||
|
} else {
|
||||||
|
// Preserve id
|
||||||
|
return [true, {...b, id: a.id}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(a.children == null) {
|
||||||
|
return [false, a]
|
||||||
|
}
|
||||||
|
|
||||||
|
if(b.has_more_children) {
|
||||||
|
return [false, a]
|
||||||
|
}
|
||||||
|
|
||||||
|
const [has_update, children] = map_accum(
|
||||||
|
(has_update, c, i) => {
|
||||||
|
const [upd, merged] = merge_calltree_nodes(c, b.children[i])
|
||||||
|
return [has_update || upd, merged]
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
a.children
|
||||||
|
)
|
||||||
|
|
||||||
|
if(has_update) {
|
||||||
|
return [true, {...a, children}]
|
||||||
|
} else {
|
||||||
|
return [false, a]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Finds node in calltree `a` that has the same position that node with id `id`
|
||||||
|
in calltree `b`. Null if not found
|
||||||
|
*/
|
||||||
|
const find_same_node = (a, b, id) => {
|
||||||
|
return map_find(
|
||||||
|
Object.entries(a),
|
||||||
|
([module, {calls}]) => do_find_same_node(calls, b[module].calls, id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const do_find_same_node = (a, b, id) => {
|
||||||
|
if(b == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if(b.id == id) {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
if(a.children == null || b.children == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return map_find(
|
||||||
|
a.children,
|
||||||
|
(c, i) => do_find_same_node(c, b.children[i], id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const expand_path = (state, node) => ({
|
||||||
|
...state,
|
||||||
|
calltree_node_is_expanded: {
|
||||||
|
...state.calltree_node_is_expanded,
|
||||||
|
...Object.fromEntries(
|
||||||
|
path_to_root(root_calltree_node(state), node)
|
||||||
|
.map(n => [n.id, true])
|
||||||
|
),
|
||||||
|
// Also expand node, since it is not included in
|
||||||
|
// path_to_root
|
||||||
|
[node.id]: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const initial_calltree_node = state => {
|
||||||
|
const root = root_calltree_node(state)
|
||||||
|
if(
|
||||||
|
root.ok
|
||||||
|
||
|
||||||
|
// Not looking for error origin, stack too deep
|
||||||
|
is_stackoverflow(root)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
state: expand_path(state, root),
|
||||||
|
node: root,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Find error origin
|
||||||
|
const node = find_node(root,
|
||||||
|
n => !n.ok && (
|
||||||
|
// All children are ok
|
||||||
|
n.children == null
|
||||||
|
||
|
||||||
|
n.children.find(c => !c.ok) == null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return {state: expand_path(state, node), node}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const default_expand_path = state => initial_calltree_node(state).state
|
||||||
|
|
||||||
|
export const find_call_node = (state, index) => {
|
||||||
|
const module = state.parse_result.modules[state.current_module]
|
||||||
|
|
||||||
|
if(module == null) {
|
||||||
|
// Module is not executed
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let node
|
||||||
|
|
||||||
|
if(index < module.index || index >= module.index + module.length) {
|
||||||
|
// index is outside of module, it can happen because of whitespace and
|
||||||
|
// comments in the beginning and the end
|
||||||
|
node = module
|
||||||
|
} else {
|
||||||
|
const leaf = find_leaf(module, index)
|
||||||
|
const anc = ancestry_inc(leaf, module)
|
||||||
|
const fn = anc.find(n => n.type == 'function_expr')
|
||||||
|
node = fn == null
|
||||||
|
? module
|
||||||
|
: fn
|
||||||
|
}
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
export const find_call = (state, index) => {
|
||||||
|
const node = find_call_node(state, index)
|
||||||
|
|
||||||
|
if(node == null) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
if(state.active_calltree_node != null && is_eq(node, state.active_calltree_node.code)) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
const ct_node_id = get_calltree_node_by_loc(state, node)
|
||||||
|
|
||||||
|
if(ct_node_id === null) {
|
||||||
|
// strict compare (===) with null, to check if we put null earlier to
|
||||||
|
// designate that fn is not reachable
|
||||||
|
return set_active_calltree_node(state, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ct_node_id != null) {
|
||||||
|
const ct_node = find_node(
|
||||||
|
root_calltree_node(state),
|
||||||
|
n => n.id == ct_node_id
|
||||||
|
)
|
||||||
|
if(ct_node == null) {
|
||||||
|
throw new Error('illegal state')
|
||||||
|
}
|
||||||
|
return set_active_calltree_node(state, ct_node, ct_node)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(node == state.parse_result.modules[root_calltree_module(state)]) {
|
||||||
|
const toplevel = root_calltree_node(state)
|
||||||
|
return add_frame(
|
||||||
|
expand_path(
|
||||||
|
state,
|
||||||
|
toplevel
|
||||||
|
),
|
||||||
|
toplevel,
|
||||||
|
)
|
||||||
|
} else if(node.type == 'do') {
|
||||||
|
// Currently we only allow to eval in toplevel of entrypoint module
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
const loc = {index: node.index, module: state.current_module}
|
||||||
|
const {calltree, call} = state.calltree_actions.find_call(
|
||||||
|
root_calltree_module(state),
|
||||||
|
loc
|
||||||
|
)
|
||||||
|
if(call == null) {
|
||||||
|
return add_calltree_node_by_loc(
|
||||||
|
// Remove active_calltree_node
|
||||||
|
// current_calltree_node may stay not null, because it is calltree node
|
||||||
|
// explicitly selected by user in calltree view
|
||||||
|
set_active_calltree_node(state, null),
|
||||||
|
loc,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const merged = merge_calltrees(state.calltree, calltree)
|
||||||
|
const active_calltree_node = find_same_node(merged, calltree, call.id)
|
||||||
|
|
||||||
|
return add_frame(
|
||||||
|
expand_path(
|
||||||
|
{...state,
|
||||||
|
calltree: merged,
|
||||||
|
calltree_changed_token: {},
|
||||||
|
},
|
||||||
|
active_calltree_node
|
||||||
|
),
|
||||||
|
active_calltree_node,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const select_return_value = state => {
|
||||||
|
if(state.current_calltree_node.toplevel) {
|
||||||
|
return {state}
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = state.active_calltree_node.code
|
||||||
|
const loc = calltree_node_loc(state.active_calltree_node)
|
||||||
|
const frame = active_frame(state)
|
||||||
|
|
||||||
|
let node, result_node
|
||||||
|
|
||||||
|
if(state.current_calltree_node == state.active_calltree_node) {
|
||||||
|
if(frame.result.ok) {
|
||||||
|
if(code.body.type == 'do') {
|
||||||
|
const return_statement = find_node(frame, n =>
|
||||||
|
n.type == 'return' && n.result?.ok
|
||||||
|
)
|
||||||
|
|
||||||
|
if(return_statement == null) {
|
||||||
|
// Fn has no return statement
|
||||||
|
return {
|
||||||
|
state: {...state, current_module: loc.module},
|
||||||
|
effects: {type: 'set_caret_position', args: [code.body.index, true]}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result_node = return_statement.children[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Last children is function body expr
|
||||||
|
result_node = frame.children[frame.children.length - 1]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result_node = find_error_origin_node(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
node = find_node(code, n => is_eq(result_node, n))
|
||||||
|
|
||||||
|
} else {
|
||||||
|
result_node = find_node(frame, n =>
|
||||||
|
n.type == 'function_call'
|
||||||
|
&&
|
||||||
|
n.result.call.id == state.current_calltree_node.id
|
||||||
|
)
|
||||||
|
node = find_node(code, n => is_eq(result_node, n))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: {...state,
|
||||||
|
current_module: loc.module,
|
||||||
|
selection_state: {
|
||||||
|
index: node.index,
|
||||||
|
node,
|
||||||
|
initial_is_expand: true,
|
||||||
|
result: result_node.result,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
effects: {type: 'set_caret_position', args: [node.index, true]}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const select_arguments = state => {
|
||||||
|
if(state.current_calltree_node.toplevel) {
|
||||||
|
return {state}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loc = calltree_node_loc(state.active_calltree_node)
|
||||||
|
const frame = active_frame(state)
|
||||||
|
|
||||||
|
let node, result
|
||||||
|
|
||||||
|
if(state.current_calltree_node == state.active_calltree_node) {
|
||||||
|
if(state.active_calltree_node.toplevel) {
|
||||||
|
return {state}
|
||||||
|
}
|
||||||
|
node = state.active_calltree_node.code.children[0] // function_args
|
||||||
|
result = frame.children[0].result
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const call = find_node(frame, n =>
|
||||||
|
n.type == 'function_call'
|
||||||
|
&&
|
||||||
|
n.result.call.id == state.current_calltree_node.id
|
||||||
|
)
|
||||||
|
const call_node = find_node(state.active_calltree_node.code, n => is_eq(n, call))
|
||||||
|
node = call_node.children[1] // call_args
|
||||||
|
result = call.children[1].result
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state: {...state,
|
||||||
|
current_module: loc.module,
|
||||||
|
selection_state: {
|
||||||
|
index: node.index,
|
||||||
|
node,
|
||||||
|
initial_is_expand: true,
|
||||||
|
result,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
effects: {type: 'set_caret_position', args: [node.index, true]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const calltree_commands = {
|
||||||
|
arrow_down,
|
||||||
|
arrow_up,
|
||||||
|
arrow_left,
|
||||||
|
arrow_right,
|
||||||
|
click,
|
||||||
|
select_return_value,
|
||||||
|
select_arguments,
|
||||||
|
}
|
||||||
661
src/cmd.js
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
import {map_object, pick_keys} from './utils.js'
|
||||||
|
import {
|
||||||
|
is_eq, is_child, ancestry, ancestry_inc, map_tree,
|
||||||
|
find_leaf, find_fn_by_location, find_node, find_error_origin_node
|
||||||
|
} from './ast_utils.js'
|
||||||
|
import {load_modules} from './parse_js.js'
|
||||||
|
import {find_export} from './find_definitions.js'
|
||||||
|
import {eval_modules} from './eval.js'
|
||||||
|
import {
|
||||||
|
root_calltree_node, root_calltree_module, calltree_commands,
|
||||||
|
add_frame, calltree_node_loc, expand_path,
|
||||||
|
initial_calltree_node, default_expand_path, toggle_expanded, active_frame,
|
||||||
|
find_call, find_call_node, set_active_calltree_node
|
||||||
|
} from './calltree.js'
|
||||||
|
|
||||||
|
const apply_active_calltree_node = (state, index) => {
|
||||||
|
if(!state.parse_result.ok) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = find_call_node(state, index)
|
||||||
|
|
||||||
|
if(
|
||||||
|
// edit module that is not imported (maybe recursively by state.entrypoint)
|
||||||
|
node == null
|
||||||
|
||
|
||||||
|
node.type == 'do' /* toplevel AST node */
|
||||||
|
) {
|
||||||
|
const result = eval_modules(state.parse_result.modules, state.parse_result.sorted)
|
||||||
|
const next = {
|
||||||
|
...state,
|
||||||
|
calltree: result.calltree,
|
||||||
|
calltree_actions: result.calltree_actions,
|
||||||
|
}
|
||||||
|
if(node == state.parse_result.modules[root_calltree_module(next)]) {
|
||||||
|
const toplevel = root_calltree_node(next)
|
||||||
|
return add_frame(
|
||||||
|
default_expand_path(
|
||||||
|
next,
|
||||||
|
toplevel
|
||||||
|
),
|
||||||
|
toplevel,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const {node, state: next2} = initial_calltree_node(next)
|
||||||
|
return set_active_calltree_node(next2, null, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {calltree, call, calltree_actions} = eval_modules(
|
||||||
|
state.parse_result.modules,
|
||||||
|
state.parse_result.sorted,
|
||||||
|
{index: node.index, module: state.current_module},
|
||||||
|
)
|
||||||
|
|
||||||
|
if(call == null) {
|
||||||
|
// Unreachable call
|
||||||
|
const {node, state: next} = initial_calltree_node({
|
||||||
|
...state,
|
||||||
|
calltree,
|
||||||
|
calltree_actions,
|
||||||
|
})
|
||||||
|
return set_active_calltree_node(next, null, node)
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = {...state, calltree, calltree_actions }
|
||||||
|
// We cannot use `call` because `code` was not assigned to it
|
||||||
|
const active_calltree_node = find_node(root_calltree_node(next),
|
||||||
|
n => n.id == call.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return add_frame(
|
||||||
|
default_expand_path(
|
||||||
|
expand_path(
|
||||||
|
next,
|
||||||
|
active_calltree_node
|
||||||
|
)
|
||||||
|
),
|
||||||
|
active_calltree_node,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = (state, code, index) => {
|
||||||
|
const files = {...state.files, [state.current_module]: code}
|
||||||
|
const next = apply_active_calltree_node(
|
||||||
|
apply_code({...state, files}, [state.current_module]),
|
||||||
|
index
|
||||||
|
)
|
||||||
|
const effects1 = next.current_module == ''
|
||||||
|
? {type: 'save_to_localstorage', args: ['code', code]}
|
||||||
|
: {type: 'write', args: [
|
||||||
|
next.current_module,
|
||||||
|
next.files[next.current_module],
|
||||||
|
]}
|
||||||
|
const {state: next2, effects: effects2} = do_move_cursor(next, index)
|
||||||
|
return {
|
||||||
|
state: next2,
|
||||||
|
effects: [effects1, effects2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const apply_code = (state, dirty_files) => {
|
||||||
|
const parse_result = load_modules(state.entrypoint, module => {
|
||||||
|
if(dirty_files != null && dirty_files.includes(module)) {
|
||||||
|
return state.files[module]
|
||||||
|
}
|
||||||
|
|
||||||
|
if(state.parse_result != null) {
|
||||||
|
const result = state.parse_result.cache[module]
|
||||||
|
if(result != null) {
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
return state.files[module]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return state.files[module]
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
parse_result,
|
||||||
|
calltree: null,
|
||||||
|
|
||||||
|
// Shows that calltree is brand new and requires entire rerender
|
||||||
|
calltree_changed_token: {},
|
||||||
|
|
||||||
|
calltree_actions: null,
|
||||||
|
current_calltree_node: null,
|
||||||
|
active_calltree_node: null,
|
||||||
|
calltree_node_is_expanded: null,
|
||||||
|
frames: null,
|
||||||
|
calltree_node_by_loc: null,
|
||||||
|
// TODO keep selection_state?
|
||||||
|
selection_state: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apply_code_with_active_calltree_node = (state, index) => {
|
||||||
|
const next = apply_code(state)
|
||||||
|
return apply_active_calltree_node(next, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const can_evaluate_node = (parent, node) => {
|
||||||
|
// TODO also can evaluate in top level even if stepped into (and evaluate in
|
||||||
|
// any stack frame that was before current one)
|
||||||
|
|
||||||
|
const anc = ancestry(node, parent)
|
||||||
|
if(anc == null){
|
||||||
|
return {ok: false, message: 'out of scope'}
|
||||||
|
}
|
||||||
|
|
||||||
|
const intermediate_fn = anc.find(n =>
|
||||||
|
!is_eq(n, parent) && !is_eq(n, node) && n.type == 'function_expr'
|
||||||
|
)
|
||||||
|
|
||||||
|
if(intermediate_fn != null){
|
||||||
|
// TODO check if identifier is defined in current scope, and eval
|
||||||
|
return {ok: false, message: 'cannot eval inside function: first step into it'}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {ok: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validate_index_action = state => {
|
||||||
|
if(!state.parse_result.ok){
|
||||||
|
return {state, effects: {type: 'set_status', args: ['invalid syntax']}}
|
||||||
|
}
|
||||||
|
if(
|
||||||
|
state.active_calltree_node == null
|
||||||
|
||
|
||||||
|
calltree_node_loc(state.active_calltree_node).module != state.current_module
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
effects: {
|
||||||
|
type: 'set_status',
|
||||||
|
args: ['code was not reached during program execution']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const get_step_into_node = (ast, frame, index) => {
|
||||||
|
// TODO step into from toplevel (must be fixed by frame follows cursor)
|
||||||
|
|
||||||
|
const node = find_leaf(ast, index)
|
||||||
|
|
||||||
|
// Find parent node with function call
|
||||||
|
const call = ancestry_inc(node, ast).find(n => n.type == 'function_call')
|
||||||
|
|
||||||
|
if(call == null){
|
||||||
|
return {ok: false, message: 'no function call to step into'}
|
||||||
|
}
|
||||||
|
|
||||||
|
const can_eval = can_evaluate_node(frame, call)
|
||||||
|
if(!can_eval.ok){
|
||||||
|
return {ok: false, message: can_eval.message}
|
||||||
|
}
|
||||||
|
|
||||||
|
const callnode = find_node(frame, n => is_eq(n, call))
|
||||||
|
if(callnode.result == null) {
|
||||||
|
return {ok: false, message: 'call was not reached during program execution'}
|
||||||
|
} else {
|
||||||
|
return {ok: true, calltree_node: callnode.result.call}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const step_into = (state, index) => {
|
||||||
|
|
||||||
|
const validate_result = validate_index_action(state)
|
||||||
|
if(validate_result != null) {
|
||||||
|
return validate_result
|
||||||
|
}
|
||||||
|
|
||||||
|
const {ok, message, calltree_node} = get_step_into_node(
|
||||||
|
state.parse_result.modules[state.current_module],
|
||||||
|
active_frame(state),
|
||||||
|
index
|
||||||
|
)
|
||||||
|
if(!ok){
|
||||||
|
return {state, effects: {type: 'set_status', args: [message]}}
|
||||||
|
} else {
|
||||||
|
const expanded = {
|
||||||
|
...state, calltree_node_is_expanded: {
|
||||||
|
...state.calltree_node_is_expanded, [calltree_node.id]: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toggle_expanded(
|
||||||
|
{...expanded, current_calltree_node: calltree_node},
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const get_next_selection_state = (selection_state, frame, is_expand, index) => {
|
||||||
|
if(selection_state != null && selection_state.index == index){
|
||||||
|
// Expanding/collapsing selection
|
||||||
|
let next_node
|
||||||
|
const effective_is_expand = selection_state.initial_is_expand == is_expand
|
||||||
|
if(effective_is_expand){
|
||||||
|
if(is_eq(selection_state.node, frame)) {
|
||||||
|
next_node = selection_state.node
|
||||||
|
} else {
|
||||||
|
next_node = ancestry(selection_state.node, frame).find(n => !n.not_evaluatable)
|
||||||
|
if(next_node.is_statement) {
|
||||||
|
next_node = selection_state.node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO when collapsing, also check that node is evaluatable
|
||||||
|
// collapse
|
||||||
|
if(selection_state.node.children != null){
|
||||||
|
next_node =
|
||||||
|
selection_state.node.children.find(n =>
|
||||||
|
n.index <= index && n.index + n.length > index
|
||||||
|
)
|
||||||
|
??
|
||||||
|
// caret not inside child but in whitespace
|
||||||
|
selection_state.node
|
||||||
|
} else {
|
||||||
|
// no children, cannot collapse
|
||||||
|
next_node = selection_state.node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
initial_is_expand: selection_state.initial_is_expand,
|
||||||
|
node: next_node,
|
||||||
|
index,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Creating new selection
|
||||||
|
const leaf = find_leaf(frame, index);
|
||||||
|
const a = ancestry_inc(leaf, frame);
|
||||||
|
const node = a.find(n => !n.not_evaluatable);
|
||||||
|
if(node.is_statement) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message: 'can only evaluate expression, not statement',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
index,
|
||||||
|
node,
|
||||||
|
initial_is_expand: is_expand,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selection = (selection_state, frame, is_expand, index) => {
|
||||||
|
const leaf = find_leaf(frame, index)
|
||||||
|
if(leaf == null) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message: 'out of scope',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const next_selection_state = get_next_selection_state(selection_state, frame, is_expand, index)
|
||||||
|
|
||||||
|
if(!next_selection_state.ok) {
|
||||||
|
return next_selection_state
|
||||||
|
}
|
||||||
|
|
||||||
|
const {ok, message} = can_evaluate_node(frame, next_selection_state.node)
|
||||||
|
if(ok){
|
||||||
|
const node = find_node(frame, n => is_eq(n, next_selection_state.node))
|
||||||
|
if(node.result == null) {
|
||||||
|
return {
|
||||||
|
...next_selection_state,
|
||||||
|
ok: false,
|
||||||
|
message: 'expression was not reached during program execution',
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let result
|
||||||
|
if(node.result.ok) {
|
||||||
|
result = node.result
|
||||||
|
} else {
|
||||||
|
const error_node = find_error_origin_node(node)
|
||||||
|
result = error_node.result
|
||||||
|
}
|
||||||
|
return {...next_selection_state, ok: true, result}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {...next_selection_state, ok: false, message}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eval_selection = (state, index, is_expand) => {
|
||||||
|
const validate_result = validate_index_action(state)
|
||||||
|
if(validate_result != null) {
|
||||||
|
return validate_result
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection_state = selection(
|
||||||
|
state.selection_state,
|
||||||
|
active_frame(state),
|
||||||
|
is_expand,
|
||||||
|
index
|
||||||
|
)
|
||||||
|
|
||||||
|
const nextstate = {...state, selection_state}
|
||||||
|
|
||||||
|
if(!selection_state.ok) {
|
||||||
|
return {state: nextstate, effects: {type: 'set_status', args: [selection_state.message]}}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {state: nextstate}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const change_current_module = (state, current_module) => {
|
||||||
|
if(state.files[current_module] == null) {
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
effects: {type: 'set_status', args: ['File not found']}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {...state, current_module}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const change_entrypoint = (state, entrypoint, index) => {
|
||||||
|
return apply_code_with_active_calltree_node(
|
||||||
|
{...state,
|
||||||
|
entrypoint,
|
||||||
|
current_module: entrypoint,
|
||||||
|
},
|
||||||
|
index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const goto_definition = (state, index) => {
|
||||||
|
if(!state.parse_result.ok){
|
||||||
|
return {state, effects: {type: 'set_status', args: ['unresolved syntax errors']}}
|
||||||
|
} else {
|
||||||
|
const module = state.parse_result.modules[state.current_module]
|
||||||
|
const node = find_leaf(module, index)
|
||||||
|
if(node == null || node.type != 'identifier') {
|
||||||
|
return {state, effects: {type: 'set_status', args: ['not an identifier']}}
|
||||||
|
} else {
|
||||||
|
const d = node.definition
|
||||||
|
if(d == 'global') {
|
||||||
|
return {state, effects: {type: 'set_status', args: ['global variable']}}
|
||||||
|
} else if (d == 'self') {
|
||||||
|
// place where identifier is declared, nothing to do
|
||||||
|
return {state}
|
||||||
|
} else {
|
||||||
|
let loc
|
||||||
|
if(d.module != null) {
|
||||||
|
const exp = find_export(node.value, state.parse_result.modules[d.module])
|
||||||
|
loc = {module: d.module, index: exp.index}
|
||||||
|
} else {
|
||||||
|
loc = {module: state.current_module, index: d.index}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
state: {...state, current_module: loc.module},
|
||||||
|
effects: {type: 'set_caret_position', args: [loc.index]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goto_problem = (state, p) => {
|
||||||
|
return {
|
||||||
|
state: {...state, current_module: p.module},
|
||||||
|
// TODO set focus after jump
|
||||||
|
effects: {type: 'set_caret_position', args: [p.index]}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO remove?
|
||||||
|
// TODO: to every child, add displayed_children property
|
||||||
|
/*
|
||||||
|
const filter_calltree = (calltree, pred) => {
|
||||||
|
const do_filter_calltree = calltree => {
|
||||||
|
const children = calltree.children && calltree.children
|
||||||
|
.map(c => do_filter_calltree(c))
|
||||||
|
.flat()
|
||||||
|
|
||||||
|
if(pred(calltree)) {
|
||||||
|
return [{...calltree, children}]
|
||||||
|
} else {
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = do_filter_calltree(calltree)
|
||||||
|
|
||||||
|
if(result.length == 1 && result[0].toplevel) {
|
||||||
|
return result[0]
|
||||||
|
} else {
|
||||||
|
return {...calltree, children: result}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const get_value_explorer = (state, index) => {
|
||||||
|
if(state.active_calltree_node == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const frame = active_frame(state)
|
||||||
|
|
||||||
|
if(frame.type == 'function_expr' && frame.body.type != 'do') {
|
||||||
|
return {
|
||||||
|
index: frame.children[1].index + frame.children[1].length,
|
||||||
|
result: frame.children[1].result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(
|
||||||
|
true
|
||||||
|
// not toplevel, function expr
|
||||||
|
&& frame.type == 'function_expr'
|
||||||
|
&& index >= frame.children[0].index
|
||||||
|
&& index < frame.children[0].index + frame.children[0].length
|
||||||
|
) {
|
||||||
|
if(frame.children[0].children.length == 0) {
|
||||||
|
// Zero args
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
// cursor in args, show args
|
||||||
|
return {
|
||||||
|
index: frame.children[0].index + frame.children[0].length,
|
||||||
|
result: frame.children[0].result,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const leaf = find_leaf(frame, index)
|
||||||
|
const adjusted_leaf = (
|
||||||
|
// We are in the whitespace at the beginning or at the end of the file
|
||||||
|
leaf == null
|
||||||
|
||
|
||||||
|
// Empty body or cursor between statements
|
||||||
|
leaf.type == 'do' && index > frame.index
|
||||||
|
)
|
||||||
|
// Try find statement one symbol before, in case we are typing at the end
|
||||||
|
// of current statement
|
||||||
|
? find_leaf(frame, index - 1)
|
||||||
|
: leaf
|
||||||
|
|
||||||
|
if(
|
||||||
|
adjusted_leaf == null
|
||||||
|
||
|
||||||
|
adjusted_leaf.type == 'do'
|
||||||
|
||
|
||||||
|
/* between body and args*/
|
||||||
|
is_eq(frame, adjusted_leaf)
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const anc = ancestry_inc(adjusted_leaf, frame)
|
||||||
|
const intermediate_fn = anc.find(n =>
|
||||||
|
!is_eq(n, frame) && !is_eq(n, adjusted_leaf) && n.type == 'function_expr'
|
||||||
|
)
|
||||||
|
if(intermediate_fn != null) {
|
||||||
|
// TODO maybe cut `anc` from frame to intermediate fn, so we do not look
|
||||||
|
// inside intermediate fn. But it should be fixed by frame follows cursor
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find inner do
|
||||||
|
const do_index = anc.findIndex(n => n.type == 'do')
|
||||||
|
const do_node = anc[do_index]
|
||||||
|
const stmt = anc[do_index - 1]
|
||||||
|
|
||||||
|
if(stmt.result == null) {
|
||||||
|
// statement was not evaluated
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let result
|
||||||
|
|
||||||
|
if(stmt.result.ok) {
|
||||||
|
if(['const', 'assignment'].includes(stmt.type)) {
|
||||||
|
result = stmt.children[1].result
|
||||||
|
} else if(stmt.type == 'return') {
|
||||||
|
result = stmt.children[0].result
|
||||||
|
} else if(stmt.type == 'let') {
|
||||||
|
return {
|
||||||
|
index: stmt.index + stmt.length,
|
||||||
|
result:
|
||||||
|
{
|
||||||
|
ok: true,
|
||||||
|
value: Object.fromEntries(
|
||||||
|
stmt.children.map(c =>
|
||||||
|
[c.value, c.result.value]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if(stmt.type == 'if'){
|
||||||
|
return null
|
||||||
|
} else if(stmt.type == 'import'){
|
||||||
|
result = {
|
||||||
|
ok: true,
|
||||||
|
value: pick_keys(
|
||||||
|
state.calltree[stmt.full_import_path].exports,
|
||||||
|
stmt.imports.map(i => i.value)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else if (stmt.type == 'export') {
|
||||||
|
result = stmt.children[0].children[1].result
|
||||||
|
} else {
|
||||||
|
result = stmt.result
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = find_error_origin_node(stmt).result
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = stmt.index + stmt.length
|
||||||
|
|
||||||
|
return {index: pos, result}
|
||||||
|
}
|
||||||
|
|
||||||
|
const do_move_cursor = (state, index) => {
|
||||||
|
const value_exp = get_value_explorer(state, index)
|
||||||
|
if(value_exp == null) {
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
effects: {type: 'unembed_value_explorer', args: []}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
effects:
|
||||||
|
state.current_module ==
|
||||||
|
calltree_node_loc(state.active_calltree_node).module
|
||||||
|
? {type: 'embed_value_explorer', args: [value_exp]}
|
||||||
|
: null
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const move_cursor = (s, index) => {
|
||||||
|
if(!s.parse_result.ok){
|
||||||
|
return {state: s}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove selection on move cursor
|
||||||
|
const state_sel_removed = {...s, selection_state: null}
|
||||||
|
|
||||||
|
const state = find_call(state_sel_removed, index)
|
||||||
|
|
||||||
|
const validate_result = validate_index_action(state)
|
||||||
|
if(validate_result != null) {
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
effects: {type: 'unembed_value_explorer', args: []}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return do_move_cursor(state, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
const load_dir = (state, dir) => {
|
||||||
|
const collect_files = dir => dir.kind == 'file'
|
||||||
|
? [dir]
|
||||||
|
: dir.children.map(collect_files).flat()
|
||||||
|
|
||||||
|
const files = Object.fromEntries(
|
||||||
|
collect_files(dir).map(f => [f.path, f.contents])
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
project_dir: dir,
|
||||||
|
files: {...files, ...state.files}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const create_file = (state, dir, current_module) => {
|
||||||
|
return {...load_dir(state, dir), current_module}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const get_initial_state = state => {
|
||||||
|
const with_files = state.project_dir == null
|
||||||
|
? state
|
||||||
|
: load_dir(state, state.project_dir)
|
||||||
|
|
||||||
|
const entrypoint = with_files.entrypoint
|
||||||
|
const current_module = with_files.current_module
|
||||||
|
|
||||||
|
const s = {
|
||||||
|
...with_files,
|
||||||
|
// If module for entrypoint or current_module does not exist, use *scratch*
|
||||||
|
entrypoint:
|
||||||
|
with_files.files[entrypoint] == null
|
||||||
|
? ''
|
||||||
|
: entrypoint,
|
||||||
|
current_module: with_files.files[current_module] == null
|
||||||
|
? ''
|
||||||
|
: current_module,
|
||||||
|
}
|
||||||
|
|
||||||
|
return apply_code_with_active_calltree_node(s, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const COMMANDS = {
|
||||||
|
input,
|
||||||
|
load_dir,
|
||||||
|
create_file,
|
||||||
|
step_into,
|
||||||
|
change_current_module,
|
||||||
|
change_entrypoint,
|
||||||
|
goto_definition,
|
||||||
|
goto_problem,
|
||||||
|
move_cursor,
|
||||||
|
eval_selection,
|
||||||
|
calltree: calltree_commands,
|
||||||
|
}
|
||||||
214
src/color.js
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
const is_result_eq = (a,b) => a.result == null
|
||||||
|
? b.result == null
|
||||||
|
: b.result != null
|
||||||
|
&& a.result.ok == b.result.ok
|
||||||
|
&& a.result.error_origin == b.result.error_origin
|
||||||
|
|
||||||
|
const node_to_color = node => ({
|
||||||
|
index: node.index,
|
||||||
|
length: node.length,
|
||||||
|
result: node.result == null
|
||||||
|
? null
|
||||||
|
: node.type == 'function_expr'
|
||||||
|
? null
|
||||||
|
: node.result.ok
|
||||||
|
? {ok: true}
|
||||||
|
: node.result.error == null
|
||||||
|
? {ok: false, error_origin: false}
|
||||||
|
: {ok: false, error_origin: true}
|
||||||
|
})
|
||||||
|
|
||||||
|
const is_short_circuit = node =>
|
||||||
|
node.type == 'binary' || node.type == 'ternary'
|
||||||
|
|
||||||
|
const color_children = (node, is_root) => {
|
||||||
|
const coloring = node.children.map(n => do_color(n)).reduce(
|
||||||
|
(coloring, [range, ...rest]) => {
|
||||||
|
if(coloring.length == 0) {
|
||||||
|
return [range, ...rest]
|
||||||
|
} else {
|
||||||
|
const prev_range = coloring[coloring.length - 1]
|
||||||
|
if(is_result_eq(prev_range, range)) {
|
||||||
|
// Merge ranges
|
||||||
|
return [
|
||||||
|
...coloring.slice(0, coloring.length - 1),
|
||||||
|
{
|
||||||
|
index: prev_range.index,
|
||||||
|
length: range.index - prev_range.index + range.length,
|
||||||
|
result: range.result == null ? null : {ok: range.result.ok}
|
||||||
|
},
|
||||||
|
...rest
|
||||||
|
]
|
||||||
|
} else if(!is_short_circuit(node) && prev_range.result == null && range.result?.ok){
|
||||||
|
// Expand range back to the end of prev range
|
||||||
|
const index = prev_range.index + prev_range.length
|
||||||
|
return [
|
||||||
|
...coloring,
|
||||||
|
{...range,
|
||||||
|
index,
|
||||||
|
length: range.index - index + range.length,
|
||||||
|
},
|
||||||
|
...rest,
|
||||||
|
]
|
||||||
|
} else if(!is_short_circuit(node) && prev_range.result?.ok && range.result == null) {
|
||||||
|
// Expand prev_range until beginning of range
|
||||||
|
const index = prev_range.index + prev_range.length
|
||||||
|
return [
|
||||||
|
...coloring.slice(0, coloring.length - 1),
|
||||||
|
{...prev_range,
|
||||||
|
length: range.index - prev_range.index
|
||||||
|
},
|
||||||
|
range,
|
||||||
|
...rest,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
// Append range
|
||||||
|
return [
|
||||||
|
...coloring,
|
||||||
|
range,
|
||||||
|
...rest,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
if(
|
||||||
|
node.result == null || node.result?.ok
|
||||||
|
&&
|
||||||
|
// All colors the same
|
||||||
|
coloring.reduce(
|
||||||
|
(result, c) => result && is_result_eq(coloring[0], c),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
|
||||||
|
if(is_result_eq(node, coloring[0])) {
|
||||||
|
if(is_root && node.type == 'function_expr') {
|
||||||
|
// Override null result for function expr
|
||||||
|
return [{...node_to_color(node), result: {ok: node.result.ok}}]
|
||||||
|
} else {
|
||||||
|
return [node_to_color(node)]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const node_color = node_to_color(node)
|
||||||
|
const last = coloring[coloring.length - 1]
|
||||||
|
const index = coloring[0].index + coloring[0].length
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...node_color,
|
||||||
|
length: coloring[0].index - node_color.index,
|
||||||
|
},
|
||||||
|
...coloring,
|
||||||
|
{
|
||||||
|
...node_color,
|
||||||
|
index,
|
||||||
|
length: node.index + node.length - index,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if(coloring.length == 0) {
|
||||||
|
throw new Error('illegal state')
|
||||||
|
}
|
||||||
|
|
||||||
|
// if first child is ok, then expand it to the beginning of parent
|
||||||
|
const first = coloring[0]
|
||||||
|
const adj_left = is_result_eq(first, node) && node.result?.ok
|
||||||
|
? [
|
||||||
|
{...first,
|
||||||
|
index: node.index,
|
||||||
|
length: first.length + first.index - node.index
|
||||||
|
},
|
||||||
|
...coloring.slice(1),
|
||||||
|
]
|
||||||
|
: coloring
|
||||||
|
|
||||||
|
// if last child is ok, then expand it to the end of parent
|
||||||
|
const last = adj_left[adj_left.length - 1]
|
||||||
|
const adj_right = is_result_eq(last, node) && node.result?.ok
|
||||||
|
? [
|
||||||
|
...adj_left.slice(0, adj_left.length - 1),
|
||||||
|
{...last,
|
||||||
|
index: last.index,
|
||||||
|
length: node.index + node.length - last.index,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: adj_left
|
||||||
|
|
||||||
|
return adj_right
|
||||||
|
}
|
||||||
|
|
||||||
|
const do_color = (node, is_root = false) => {
|
||||||
|
if(node.type == 'function_expr' && !is_root) {
|
||||||
|
return [{...node_to_color(node), result: null}]
|
||||||
|
}
|
||||||
|
|
||||||
|
if(
|
||||||
|
false
|
||||||
|
|| node.children == null
|
||||||
|
|| node.children.length == 0
|
||||||
|
) {
|
||||||
|
return [node_to_color(node)]
|
||||||
|
}
|
||||||
|
|
||||||
|
if(node.result?.error != null) {
|
||||||
|
return [node_to_color(node)]
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = color_children(node, is_root)
|
||||||
|
return node.result != null && !node.result.ok
|
||||||
|
? result.map(c => c.result == null
|
||||||
|
? {...c, result: {ok: false, error_origin: false}}
|
||||||
|
: c
|
||||||
|
)
|
||||||
|
: result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const color = frame => {
|
||||||
|
const coloring = do_color(frame, true)
|
||||||
|
.filter(c =>
|
||||||
|
// Previously we colored nodes that were not reach to grey color, now we
|
||||||
|
// just skip them
|
||||||
|
c.result != null
|
||||||
|
&&
|
||||||
|
// Parts that were not error origins
|
||||||
|
(c.result.ok || c.result.error_origin)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sanity-check result
|
||||||
|
const {ok} = coloring.reduce(
|
||||||
|
({ok, prev}, c) => {
|
||||||
|
if(!ok) {
|
||||||
|
return {ok}
|
||||||
|
}
|
||||||
|
if(prev == null) {
|
||||||
|
return {ok, prev: c}
|
||||||
|
} else {
|
||||||
|
// Check that prev is before next
|
||||||
|
// TODO check that next is right after prev, ie change > to ==
|
||||||
|
if(prev.index + prev.length > c.index) {
|
||||||
|
return {ok: false}
|
||||||
|
} else {
|
||||||
|
return {ok: true, prev: c}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ok: true, prev: null}
|
||||||
|
)
|
||||||
|
if(!ok) {
|
||||||
|
throw new Error('illegal state')
|
||||||
|
}
|
||||||
|
return coloring
|
||||||
|
}
|
||||||
|
|
||||||
|
export const color_file = (state, file) =>
|
||||||
|
Object
|
||||||
|
.values(state.calltree_node_by_loc?.[file] ?? {})
|
||||||
|
// node_id == null means it is unreachable, so do not color
|
||||||
|
.filter(node_id => node_id != null)
|
||||||
|
.map(node_id => state.frames[node_id].coloring)
|
||||||
|
.flat()
|
||||||
173
src/editor/calltree.js
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import {exec} from '../index.js'
|
||||||
|
import {el, stringify, fn_link, scrollIntoViewIfNeeded} from './domutils.js'
|
||||||
|
import {FLAGS} from '../feature_flags.js'
|
||||||
|
import {stringify_for_header} from './value_explorer.js'
|
||||||
|
import {find_node} from '../ast_utils.js'
|
||||||
|
import {is_expandable, root_calltree_node} from '../calltree.js'
|
||||||
|
|
||||||
|
// TODO perf - quadratic difficulty
|
||||||
|
const join = arr => arr.reduce(
|
||||||
|
(acc, el) => acc.length == 0
|
||||||
|
? [el]
|
||||||
|
: [...acc, ',', el],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
export class CallTree {
|
||||||
|
constructor(ui, container) {
|
||||||
|
this.ui = ui
|
||||||
|
this.container = container
|
||||||
|
|
||||||
|
this.container.addEventListener('keydown', (e) => {
|
||||||
|
|
||||||
|
// Do not scroll
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if(e.key == 'F1') {
|
||||||
|
this.ui.editor.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
if(e.key == 'F2') {
|
||||||
|
this.ui.editor.focus_value_explorer(this.container)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(e.key == 'a') {
|
||||||
|
if(FLAGS.embed_value_explorer) {
|
||||||
|
exec('calltree.select_arguments')
|
||||||
|
} else {
|
||||||
|
// TODO make clear that arguments are shown
|
||||||
|
this.ui.eval.show_value(this.state.current_calltree_node.args)
|
||||||
|
this.ui.eval.focus_value_or_error(this.container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(e.key == 'r' || e.key == 'Enter') {
|
||||||
|
if(FLAGS.embed_value_explorer) {
|
||||||
|
exec('calltree.select_return_value')
|
||||||
|
} else {
|
||||||
|
// TODO make clear that return value is shown
|
||||||
|
this.ui.eval.show_value_or_error(this.state.current_calltree_node)
|
||||||
|
this.ui.eval.focus_value_or_error(this.container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(e.key == 'ArrowDown' || e.key == 'j'){
|
||||||
|
exec('calltree.arrow_down')
|
||||||
|
}
|
||||||
|
|
||||||
|
if(e.key == 'ArrowUp' || e.key == 'k'){
|
||||||
|
exec('calltree.arrow_up')
|
||||||
|
}
|
||||||
|
|
||||||
|
if(e.key == 'ArrowLeft' || e.key == 'h'){
|
||||||
|
exec('calltree.arrow_left')
|
||||||
|
}
|
||||||
|
|
||||||
|
if(e.key == 'ArrowRight' || e.key == 'l'){
|
||||||
|
exec('calltree.arrow_right')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
on_click_node(id) {
|
||||||
|
exec('calltree.click', id)
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_calltree(){
|
||||||
|
this.container.innerHTML = ''
|
||||||
|
this.node_to_el = new Map()
|
||||||
|
this.state = null
|
||||||
|
}
|
||||||
|
|
||||||
|
render_node(n, current_node){
|
||||||
|
const is_expanded = this.state.calltree_node_is_expanded[n.id]
|
||||||
|
|
||||||
|
const result = el('div', 'callnode',
|
||||||
|
el('div', {
|
||||||
|
'class': (n == current_node ? 'call_el active' : 'call_el'),
|
||||||
|
click: () => this.on_click_node(n.id),
|
||||||
|
},
|
||||||
|
!is_expandable(n)
|
||||||
|
? '\xa0'
|
||||||
|
: is_expanded ? '▼' : '▶',
|
||||||
|
n.toplevel
|
||||||
|
? el('span', '',
|
||||||
|
el('i', '',
|
||||||
|
'toplevel: ' + (n.module == '' ? '*scratch*' : n.module),
|
||||||
|
),
|
||||||
|
n.ok ? '' : el('span', 'call_header error', '\xa0', n.error.toString()),
|
||||||
|
)
|
||||||
|
: el('span',
|
||||||
|
'call_header '
|
||||||
|
+ (n.ok ? '' : 'error')
|
||||||
|
+ (n.fn.__location == null ? ' native' : '')
|
||||||
|
,
|
||||||
|
// TODO show `this` argument
|
||||||
|
n.fn.__location == null
|
||||||
|
? fn_link(n.fn)
|
||||||
|
: n.fn.name
|
||||||
|
,
|
||||||
|
'(' ,
|
||||||
|
...join(
|
||||||
|
n.args.map(
|
||||||
|
a => typeof(a) == 'function'
|
||||||
|
? fn_link(a)
|
||||||
|
: stringify_for_header(a)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
')' ,
|
||||||
|
// TODO: show error message only where it was thrown, not every frame?
|
||||||
|
': ', (n.ok ? stringify_for_header(n.value) : n.error.toString())
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(n.children == null || !is_expanded)
|
||||||
|
? null
|
||||||
|
: n.children.map(c => this.render_node(c, current_node))
|
||||||
|
)
|
||||||
|
|
||||||
|
this.node_to_el.set(n.id, result)
|
||||||
|
|
||||||
|
result.is_expanded = is_expanded
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
render_active(node, is_active) {
|
||||||
|
const dom = this.node_to_el.get(node.id).getElementsByClassName('call_el')[0]
|
||||||
|
if(is_active) {
|
||||||
|
dom.classList.add('active')
|
||||||
|
} else {
|
||||||
|
dom.classList.remove('active')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_select_node(state) {
|
||||||
|
this.render_active(this.state.current_calltree_node, false)
|
||||||
|
this.state = state
|
||||||
|
this.render_active(this.state.current_calltree_node, true)
|
||||||
|
scrollIntoViewIfNeeded(
|
||||||
|
this.container,
|
||||||
|
this.node_to_el.get(this.state.current_calltree_node.id).getElementsByClassName('call_el')[0]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render_expand_node(state) {
|
||||||
|
this.state = state
|
||||||
|
const current_node = this.state.current_calltree_node
|
||||||
|
const prev_dom_node = this.node_to_el.get(current_node.id)
|
||||||
|
const next = this.render_node(current_node, current_node)
|
||||||
|
prev_dom_node.parentNode.replaceChild(next, prev_dom_node)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO on hover highlight line where function defined/
|
||||||
|
// TODO hover ?
|
||||||
|
render_calltree(state){
|
||||||
|
this.clear_calltree()
|
||||||
|
this.state = state
|
||||||
|
const root = root_calltree_node(this.state)
|
||||||
|
const current_node = state.current_calltree_node
|
||||||
|
this.container.appendChild(this.render_node(root, current_node))
|
||||||
|
this.render_select_node(state, root, current_node)
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/editor/domutils.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
export function el(tag, className, ...children){
|
||||||
|
const result = document.createElement(tag)
|
||||||
|
if(typeof(className) == 'string'){
|
||||||
|
result.setAttribute('class', className)
|
||||||
|
} else {
|
||||||
|
const attrs = className
|
||||||
|
for(let attrName in attrs){
|
||||||
|
const value = attrs[attrName]
|
||||||
|
if(['change','click'].includes(attrName)){
|
||||||
|
result.addEventListener(attrName, value)
|
||||||
|
} else if(attrName == 'checked') {
|
||||||
|
if(attrs[attrName]){
|
||||||
|
result.setAttribute(attrName, "checked")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.setAttribute(attrName, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
children.forEach(child => {
|
||||||
|
const append = child => {
|
||||||
|
if(typeof(child) == 'undefined') {
|
||||||
|
throw new Error('illegal state')
|
||||||
|
} else if(child !== null) {
|
||||||
|
result.appendChild(
|
||||||
|
typeof(child) == 'string'
|
||||||
|
? document.createTextNode(child)
|
||||||
|
: child
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(Array.isArray(child)) {
|
||||||
|
child.forEach(append)
|
||||||
|
} else {
|
||||||
|
append(child)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stringify(val){
|
||||||
|
function fn_to_str(fn){
|
||||||
|
// TODO if name is 'anonymous', then change name for code
|
||||||
|
return fn.__location == null
|
||||||
|
? `<span>${fn.name}</span>`
|
||||||
|
: `<a
|
||||||
|
href='javascript:void(0)'
|
||||||
|
data-location=${JSON.stringify(fn.__location)}
|
||||||
|
><i>fn</i> ${fn.name}</a>`
|
||||||
|
}
|
||||||
|
if(typeof(val) == 'undefined') {
|
||||||
|
return 'undefined'
|
||||||
|
} else if(typeof(val) == 'function'){
|
||||||
|
return fn_to_str(val)
|
||||||
|
} else {
|
||||||
|
return JSON.stringify(val, (key, value) => {
|
||||||
|
if(typeof(value) == 'function'){
|
||||||
|
return fn_to_str(value)
|
||||||
|
} else {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fn_link(fn){
|
||||||
|
const str = stringify(fn)
|
||||||
|
const c = document.createElement('div')
|
||||||
|
c.innerHTML = str
|
||||||
|
return c.children[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Idea is borrowed from:
|
||||||
|
// https://mhk-bit.medium.com/scroll-into-view-if-needed-10a96e0bdb61
|
||||||
|
// https://stackoverflow.com/questions/37137450/scroll-all-nested-scrollbars-to-bring-an-html-element-into-view
|
||||||
|
export const scrollIntoViewIfNeeded = (container, target) => {
|
||||||
|
|
||||||
|
// Target is outside the viewport from the top
|
||||||
|
if(target.offsetTop - container.scrollTop - container.offsetTop < 0){
|
||||||
|
// The top of the target will be aligned to the top of the visible area of the scrollable ancestor
|
||||||
|
target.scrollIntoView(true);
|
||||||
|
// Do not scroll horizontally
|
||||||
|
container.scrollLeft = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target is outside the view from the bottom
|
||||||
|
if(target.offsetTop - container.scrollTop - container.offsetTop - container.clientHeight + target.clientHeight > 0) {
|
||||||
|
// The bottom of the target will be aligned to the bottom of the visible area of the scrollable ancestor.
|
||||||
|
target.scrollIntoView(false);
|
||||||
|
// Do not scroll horizontally
|
||||||
|
container.scrollLeft = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Also works
|
||||||
|
|
||||||
|
// Target is outside the view from the top
|
||||||
|
if (target.getBoundingClientRect().y < container.getBoundingClientRect().y) {
|
||||||
|
// The top of the target will be aligned to the top of the visible area of the scrollable ancestor
|
||||||
|
target.scrollIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Target is outside the view from the bottom
|
||||||
|
if (
|
||||||
|
target.getBoundingClientRect().bottom - container.getBoundingClientRect().bottom +
|
||||||
|
// Adjust for scrollbar size
|
||||||
|
container.offsetHeight - container.clientHeight
|
||||||
|
> 0
|
||||||
|
) {
|
||||||
|
// The bottom of the target will be aligned to the bottom of the visible area of the scrollable ancestor.
|
||||||
|
target.scrollIntoView(false);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
};
|
||||||
469
src/editor/editor.js
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
import {exec, get_state} from '../index.js'
|
||||||
|
import {ValueExplorer} from './value_explorer.js'
|
||||||
|
import {el, stringify, fn_link} from './domutils.js'
|
||||||
|
import {FLAGS} from '../feature_flags.js'
|
||||||
|
|
||||||
|
/*
|
||||||
|
normalize events 'change' and 'changeSelection':
|
||||||
|
- change is debounced
|
||||||
|
- changeSelection must not fire if 'change' is fired. So for every keystroke,
|
||||||
|
either change or changeSelection should be fired, not both
|
||||||
|
- changeSelection fired only once (ace fires it multiple times for single
|
||||||
|
keystroke)
|
||||||
|
*/
|
||||||
|
const normalize_events = (ace_editor, {
|
||||||
|
on_change,
|
||||||
|
on_change_selection,
|
||||||
|
is_change_selection_supressed,
|
||||||
|
on_change_immediate,
|
||||||
|
}) => {
|
||||||
|
const TIMEOUT = 1000
|
||||||
|
|
||||||
|
let state
|
||||||
|
|
||||||
|
const set_initial_state = () => {
|
||||||
|
state = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_initial_state()
|
||||||
|
|
||||||
|
const flush = () => {
|
||||||
|
if(state.change_args != null) {
|
||||||
|
on_change(...state.change_args)
|
||||||
|
} else if(state.change_selection_args != null) {
|
||||||
|
on_change_selection(...state.change_selection_args)
|
||||||
|
}
|
||||||
|
set_initial_state()
|
||||||
|
}
|
||||||
|
|
||||||
|
ace_editor.on('change', (...args) => {
|
||||||
|
on_change_immediate()
|
||||||
|
|
||||||
|
if(state.tid != null) {
|
||||||
|
clearTimeout(state.tid)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.change_args = args
|
||||||
|
|
||||||
|
state.tid = setTimeout(() => {
|
||||||
|
state.tid = null
|
||||||
|
flush()
|
||||||
|
}, TIMEOUT)
|
||||||
|
})
|
||||||
|
|
||||||
|
ace_editor.on('changeSelection', (...args) => {
|
||||||
|
if(is_change_selection_supressed()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(state.tid != null) {
|
||||||
|
// flush is already by `change`, skip `changeSelection`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.change_selection_args = args
|
||||||
|
if(!state.is_flush_set) {
|
||||||
|
state.is_flush_set = true
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
if(state.tid == null) {
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Editor {
|
||||||
|
|
||||||
|
constructor(ui, editor_container){
|
||||||
|
this.ui = ui
|
||||||
|
this.editor_container = editor_container
|
||||||
|
|
||||||
|
this.markers = {}
|
||||||
|
this.sessions = {}
|
||||||
|
|
||||||
|
this.ace_editor = ace.edit(this.editor_container)
|
||||||
|
|
||||||
|
this.ace_editor.setOptions({
|
||||||
|
behavioursEnabled: false,
|
||||||
|
// Scroll past end for value explorer
|
||||||
|
scrollPastEnd: 100 /* Allows to scroll 100*<screen size> */,
|
||||||
|
})
|
||||||
|
|
||||||
|
normalize_events(this.ace_editor, {
|
||||||
|
on_change: () => {
|
||||||
|
try {
|
||||||
|
exec('input', this.ace_editor.getValue(), this.get_caret_position())
|
||||||
|
} catch(e) {
|
||||||
|
// Do not throw Error to ACE because it breaks typing
|
||||||
|
console.error(e)
|
||||||
|
this.ui.set_status(e.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
on_change_immediate: () => {
|
||||||
|
this.update_value_explorer_margin()
|
||||||
|
},
|
||||||
|
|
||||||
|
on_change_selection: () => {
|
||||||
|
try {
|
||||||
|
if(!this.is_change_selection_supressed) {
|
||||||
|
exec('move_cursor', this.get_caret_position())
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
// Do not throw Error to ACE because it breaks typing
|
||||||
|
console.error(e)
|
||||||
|
this.ui.set_status(e.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
is_change_selection_supressed: () => {
|
||||||
|
return this.is_change_selection_supressed
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.focus()
|
||||||
|
|
||||||
|
this.init_keyboard()
|
||||||
|
}
|
||||||
|
|
||||||
|
focus() {
|
||||||
|
this.ace_editor.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
supress_change_selection(action) {
|
||||||
|
try {
|
||||||
|
this.is_change_selection_supressed = true
|
||||||
|
action()
|
||||||
|
} finally {
|
||||||
|
this.is_change_selection_supressed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_session(file, code) {
|
||||||
|
let session = this.sessions[file]
|
||||||
|
if(session == null) {
|
||||||
|
session = ace.createEditSession(code)
|
||||||
|
this.sessions[file] = session
|
||||||
|
session.setUseWorker(false)
|
||||||
|
session.setOptions({
|
||||||
|
mode: "ace/mode/javascript",
|
||||||
|
tabSize: 2,
|
||||||
|
useSoftTabs: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
get_session(file) {
|
||||||
|
return this.sessions[file]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch_session(file) {
|
||||||
|
// Supress selection change triggered by switching sessions
|
||||||
|
this.supress_change_selection(() => {
|
||||||
|
this.ace_editor.setSession(this.get_session(file))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
unembed_value_explorer() {
|
||||||
|
if(this.widget != null) {
|
||||||
|
this.ace_editor.getSession().widgetManager.removeLineWidget(this.widget)
|
||||||
|
this.widget = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_value_explorer_margin() {
|
||||||
|
if(this.widget != null) {
|
||||||
|
this.widget.content.style.marginLeft =
|
||||||
|
(this.ace_editor.getSession().getScreenWidth() + 1) + 'ch'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
embed_value_explorer({index, result: {ok, value, error}}) {
|
||||||
|
this.unembed_value_explorer()
|
||||||
|
|
||||||
|
const session = this.ace_editor.getSession()
|
||||||
|
const pos = session.doc.indexToPosition(index)
|
||||||
|
const row = pos.row
|
||||||
|
|
||||||
|
const line_height = this.ace_editor.renderer.lineHeight
|
||||||
|
|
||||||
|
let content
|
||||||
|
const container = el('div', {'class': 'embed_value_explorer_container'},
|
||||||
|
el('div', {'class': 'embed_value_explorer_wrapper'},
|
||||||
|
content = el('div', {
|
||||||
|
// Ace editor cannot render widget before the first line. So we
|
||||||
|
// render in on the next line and apply translate
|
||||||
|
'style': `transform: translate(0px, -${line_height}px)`,
|
||||||
|
'class': 'embed_value_explorer_content',
|
||||||
|
tabindex: 0
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
let initial_scroll_top
|
||||||
|
|
||||||
|
const escape = () => {
|
||||||
|
if(initial_scroll_top != null) {
|
||||||
|
// restore scroll
|
||||||
|
session.setScrollTop(initial_scroll_top)
|
||||||
|
}
|
||||||
|
if(this.widget.return_to == null) {
|
||||||
|
this.focus()
|
||||||
|
} else {
|
||||||
|
this.widget.return_to.focus()
|
||||||
|
}
|
||||||
|
// TODO select root in value explorer
|
||||||
|
}
|
||||||
|
|
||||||
|
container.addEventListener('keydown', e => {
|
||||||
|
if(e.key == 'Escape') {
|
||||||
|
escape()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if(ok) {
|
||||||
|
const exp = new ValueExplorer({
|
||||||
|
container: content,
|
||||||
|
event_target: container,
|
||||||
|
on_escape: escape,
|
||||||
|
scroll_to_element: t => {
|
||||||
|
if(initial_scroll_top == null) {
|
||||||
|
initial_scroll_top = session.getScrollTop()
|
||||||
|
}
|
||||||
|
let scroll
|
||||||
|
const out_of_bottom = t.getBoundingClientRect().bottom - this.editor_container.getBoundingClientRect().bottom
|
||||||
|
if(out_of_bottom > 0) {
|
||||||
|
session.setScrollTop(session.getScrollTop() + out_of_bottom)
|
||||||
|
}
|
||||||
|
const out_of_top = this.editor_container.getBoundingClientRect().top - t.getBoundingClientRect().top
|
||||||
|
if(out_of_top > 0) {
|
||||||
|
session.setScrollTop(session.getScrollTop() - out_of_top)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
exp.render(value)
|
||||||
|
} else {
|
||||||
|
content.appendChild(el('span', 'eval_error', error.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.widget = {
|
||||||
|
row,
|
||||||
|
fixedWidth: true,
|
||||||
|
el: container,
|
||||||
|
content,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update_value_explorer_margin()
|
||||||
|
|
||||||
|
const LineWidgets = require("ace/line_widgets").LineWidgets;
|
||||||
|
if (!session.widgetManager) {
|
||||||
|
session.widgetManager = new LineWidgets(session);
|
||||||
|
session.widgetManager.attach(this.ace_editor);
|
||||||
|
}
|
||||||
|
session.widgetManager.addLineWidget(this.widget)
|
||||||
|
}
|
||||||
|
|
||||||
|
focus_value_explorer(return_to) {
|
||||||
|
if(FLAGS.embed_value_explorer) {
|
||||||
|
if(this.widget != null) {
|
||||||
|
this.widget.return_to = return_to
|
||||||
|
this.widget.content.focus({preventScroll: true})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(get_state().selection_state != null) {
|
||||||
|
this.ui.eval.focus_value_or_error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_keyboard_handler(type) {
|
||||||
|
if(type != null) {
|
||||||
|
localStorage.keyboard = type
|
||||||
|
}
|
||||||
|
this.ace_editor.setKeyboardHandler(
|
||||||
|
type == 'vim' ? "ace/keyboard/vim" : null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
init_keyboard(){
|
||||||
|
this.set_keyboard_handler(localStorage.keyboard)
|
||||||
|
|
||||||
|
const VimApi = require("ace/keyboard/vim").CodeMirror.Vim
|
||||||
|
|
||||||
|
|
||||||
|
this.ace_editor.commands.bindKey("F1", "switch_window");
|
||||||
|
VimApi._mapCommand({
|
||||||
|
keys: '<C-w>',
|
||||||
|
type: 'action',
|
||||||
|
action: 'aceCommand',
|
||||||
|
actionArgs: { name: "switch_window" }
|
||||||
|
})
|
||||||
|
this.ace_editor.commands.addCommand({
|
||||||
|
name: 'switch_window',
|
||||||
|
exec: (editor) => {
|
||||||
|
this.ui.calltree_container.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
this.ace_editor.commands.bindKey("F3", "goto_definition");
|
||||||
|
VimApi._mapCommand({
|
||||||
|
keys: 'gd',
|
||||||
|
type: 'action',
|
||||||
|
action: 'aceCommand',
|
||||||
|
actionArgs: { name: "goto_definition" }
|
||||||
|
})
|
||||||
|
this.ace_editor.commands.addCommand({
|
||||||
|
name: 'goto_definition',
|
||||||
|
exec: (editor) => {
|
||||||
|
this.goto_definition()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
this.ace_editor.commands.bindKey("F2", "focus_value_explorer");
|
||||||
|
this.ace_editor.commands.addCommand({
|
||||||
|
name: 'focus_value_explorer',
|
||||||
|
exec: (editor) => {
|
||||||
|
this.focus_value_explorer()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
this.ace_editor.commands.bindKey("ctrl-i", 'step_into')
|
||||||
|
VimApi._mapCommand({
|
||||||
|
keys: '\\i',
|
||||||
|
type: 'action',
|
||||||
|
action: 'aceCommand',
|
||||||
|
actionArgs: { name: "step_into" }
|
||||||
|
})
|
||||||
|
this.ace_editor.commands.addCommand({
|
||||||
|
name: 'step_into',
|
||||||
|
exec: (editor) => {
|
||||||
|
exec('step_into', this.get_caret_position())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
this.ace_editor.commands.bindKey("ctrl-o", 'step_out')
|
||||||
|
VimApi._mapCommand({
|
||||||
|
keys: '\\o',
|
||||||
|
type: 'action',
|
||||||
|
action: 'aceCommand',
|
||||||
|
actionArgs: { name: "step_out" }
|
||||||
|
})
|
||||||
|
this.ace_editor.commands.addCommand({
|
||||||
|
name: 'step_out',
|
||||||
|
exec: (editor) => {
|
||||||
|
exec('calltree.arrow_left')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
this.ace_editor.commands.addCommand({
|
||||||
|
name: 'expand_selection',
|
||||||
|
exec: () => {
|
||||||
|
exec('eval_selection', this.get_caret_position(), true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.ace_editor.commands.addCommand({
|
||||||
|
name: 'collapse_selection',
|
||||||
|
exec: () => {
|
||||||
|
exec('eval_selection', this.get_caret_position(), false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.ace_editor.commands.bindKey("ctrl-j", 'expand_selection')
|
||||||
|
this.ace_editor.commands.bindKey("ctrl-down", 'expand_selection')
|
||||||
|
this.ace_editor.commands.bindKey("ctrl-k", 'collapse_selection')
|
||||||
|
this.ace_editor.commands.bindKey("ctrl-up", 'collapse_selection')
|
||||||
|
|
||||||
|
|
||||||
|
this.ace_editor.commands.addCommand({
|
||||||
|
name: 'edit',
|
||||||
|
exec: (editor, input) => {
|
||||||
|
const module = input.args == null ? '' : input.args[0]
|
||||||
|
exec('change_current_module', module)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
VimApi.defineEx("edit", "e", function(cm, input) {
|
||||||
|
cm.ace.execCommand("edit", input)
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO remove my custom binding
|
||||||
|
VimApi.map('jj', '<Esc>', 'insert')
|
||||||
|
}
|
||||||
|
|
||||||
|
add_marker(file, className, from, to){
|
||||||
|
const session = this.get_session(file)
|
||||||
|
const from_pos = session.doc.indexToPosition(from)
|
||||||
|
const to_pos = session.doc.indexToPosition(to)
|
||||||
|
const markerId = session.addMarker(
|
||||||
|
new ace.Range(from_pos.row,from_pos.column,to_pos.row,to_pos.column),
|
||||||
|
className
|
||||||
|
)
|
||||||
|
if(this.markers[file] == null){
|
||||||
|
this.markers[file] = []
|
||||||
|
}
|
||||||
|
this.markers[file].push({className, from, to, markerId})
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_markers_of_type(file, type){
|
||||||
|
if(this.markers[file] == null){
|
||||||
|
this.markers[file] = []
|
||||||
|
}
|
||||||
|
const for_removal = this.markers[file].filter(h => h.className == type)
|
||||||
|
const session = this.get_session(file)
|
||||||
|
for(let marker of for_removal){
|
||||||
|
session.removeMarker(marker.markerId)
|
||||||
|
}
|
||||||
|
this.markers[file] = this.markers[file].filter(h => h.className != type)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get_caret_position(file){
|
||||||
|
const session = file == null
|
||||||
|
? this.ace_editor.getSession()
|
||||||
|
: this.get_session(file)
|
||||||
|
|
||||||
|
// Session was not created for file
|
||||||
|
if(session == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.doc.positionToIndex(session.selection.getCursor())
|
||||||
|
}
|
||||||
|
|
||||||
|
set_caret_position(index){
|
||||||
|
if(index == null) {
|
||||||
|
throw new Error('illegal state')
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = this.ace_editor.session.doc.indexToPosition(index)
|
||||||
|
console.log('set caret position', index, pos)
|
||||||
|
|
||||||
|
this.supress_change_selection(() => {
|
||||||
|
const pos = this.ace_editor.session.doc.indexToPosition(index)
|
||||||
|
this.ace_editor.moveCursorToPosition(pos)
|
||||||
|
// Moving cursor performs selection, clear it
|
||||||
|
this.ace_editor.clearSelection()
|
||||||
|
const first = this.ace_editor.renderer.getFirstVisibleRow()
|
||||||
|
const last = this.ace_editor.renderer.getLastVisibleRow()
|
||||||
|
if(pos.row < first || pos.row > last) {
|
||||||
|
this.ace_editor.scrollToLine(pos.row)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
goto_definition(){
|
||||||
|
const index = this.get_caret_position()
|
||||||
|
exec('goto_definition', index)
|
||||||
|
}
|
||||||
|
|
||||||
|
for_each_session(cb) {
|
||||||
|
for(let file in this.sessions) {
|
||||||
|
cb(file, this.sessions[file])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
67
src/editor/eval.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import {ValueExplorer} from './value_explorer.js'
|
||||||
|
import {el} from './domutils.js'
|
||||||
|
|
||||||
|
export class Eval {
|
||||||
|
|
||||||
|
constructor(ui, container) {
|
||||||
|
this.ui = ui
|
||||||
|
this.container = container
|
||||||
|
|
||||||
|
this.container.addEventListener('keydown', (e) => {
|
||||||
|
if(e.key == 'Escape') {
|
||||||
|
this.escape()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO jump to fn location, view function calls
|
||||||
|
// container.addEventListener('click', jump_to_fn_location)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
escape() {
|
||||||
|
if(this.focusedFrom == null) {
|
||||||
|
this.ui.editor.focus()
|
||||||
|
} else {
|
||||||
|
this.focusedFrom.focus()
|
||||||
|
this.focusedFrom = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
show_value(value){
|
||||||
|
this.container.innerHTML = ''
|
||||||
|
const container = el('div', {'class': 'eval_content', tabindex: 0})
|
||||||
|
this.container.appendChild(container)
|
||||||
|
const explorer = new ValueExplorer({
|
||||||
|
container,
|
||||||
|
on_escape: () => this.escape()
|
||||||
|
})
|
||||||
|
explorer.render(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
show_error(error){
|
||||||
|
this.container.innerHTML = ''
|
||||||
|
this.container.appendChild(el('span', 'eval_error', error.toString()))
|
||||||
|
}
|
||||||
|
|
||||||
|
show_value_or_error({ok, value, error}){
|
||||||
|
if(ok) {
|
||||||
|
this.show_value(value)
|
||||||
|
} else {
|
||||||
|
this.show_error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_value_or_error() {
|
||||||
|
this.container.innerHTML = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
focus_value_or_error(from) {
|
||||||
|
this.focusedFrom = from
|
||||||
|
if(this.container.childElementCount != 1) {
|
||||||
|
throw new Error('illegal state')
|
||||||
|
}
|
||||||
|
this.container.children[0].focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
164
src/editor/files.js
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import {el} from './domutils.js'
|
||||||
|
import {map_find} from '../utils.js'
|
||||||
|
import {load_dir, create_file} from '../filesystem.js'
|
||||||
|
import {exec, get_state} from '../index.js'
|
||||||
|
|
||||||
|
export class Files {
|
||||||
|
constructor(ui) {
|
||||||
|
this.ui = ui
|
||||||
|
this.el = el('div', 'files_container')
|
||||||
|
this.render(get_state())
|
||||||
|
}
|
||||||
|
|
||||||
|
open_directory() {
|
||||||
|
load_dir(true).then(dir => {
|
||||||
|
exec('load_dir', dir)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render(state) {
|
||||||
|
if(state.project_dir == null) {
|
||||||
|
this.el.innerHTML = ''
|
||||||
|
this.el.appendChild(
|
||||||
|
el('div', 'allow_file_access',
|
||||||
|
el('a', {
|
||||||
|
href: 'javascript:void(0)',
|
||||||
|
click: this.open_directory.bind(this),
|
||||||
|
},
|
||||||
|
`Allow access to local project folder`,
|
||||||
|
),
|
||||||
|
el('div', 'subtitle', `Your files will never leave your device`)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.render_files(state.project_dir, state.current_module)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_files(dir, current_module) {
|
||||||
|
const files = this.el.querySelector('.files')
|
||||||
|
|
||||||
|
const children = [
|
||||||
|
this.render_file({name: '*scratch*', path: ''}, current_module),
|
||||||
|
this.render_file(dir, current_module),
|
||||||
|
]
|
||||||
|
|
||||||
|
if(files == null) {
|
||||||
|
this.el.innerHTML = ''
|
||||||
|
this.el.appendChild(
|
||||||
|
el('div', 'file_actions',
|
||||||
|
el('a', {
|
||||||
|
href: 'javascript: void(0)',
|
||||||
|
click: this.create_file.bind(this, false),
|
||||||
|
},
|
||||||
|
'Create file'
|
||||||
|
),
|
||||||
|
el('a', {
|
||||||
|
href: 'javascript: void(0)',
|
||||||
|
click: this.create_file.bind(this, true),
|
||||||
|
}, 'Create dir'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
this.el.appendChild(
|
||||||
|
el('div', 'files',
|
||||||
|
children
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Replace to preserve scroll position
|
||||||
|
files.replaceChildren(...children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render_file(file, current_module) {
|
||||||
|
const result = el('div', 'file',
|
||||||
|
el('div', {
|
||||||
|
'class': 'file_title' + (file.path == current_module ? ' active' : ''),
|
||||||
|
click: e => this.on_click(e, file)
|
||||||
|
},
|
||||||
|
el('span', 'icon',
|
||||||
|
file.kind == 'directory'
|
||||||
|
? '\u{1F4C1}' // folder icon
|
||||||
|
: '\xa0',
|
||||||
|
),
|
||||||
|
file.name,
|
||||||
|
),
|
||||||
|
file.children == null
|
||||||
|
? null
|
||||||
|
: file.children.map(c => this.render_file(c, current_module))
|
||||||
|
)
|
||||||
|
|
||||||
|
if(file.path == current_module) {
|
||||||
|
this.active_el = result
|
||||||
|
this.active_file = file
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
async create_file(is_dir) {
|
||||||
|
|
||||||
|
if(this.active_file == null) {
|
||||||
|
throw new Error('no active file')
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = prompt(`Enter ${is_dir ? 'directory' : 'file'} name`)
|
||||||
|
if(name == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir
|
||||||
|
|
||||||
|
const root = get_state().project_dir
|
||||||
|
|
||||||
|
if(this.active_file.path == '' /* scratch */) {
|
||||||
|
// Create in root directory
|
||||||
|
dir = root
|
||||||
|
} else {
|
||||||
|
if(this.active_file.kind == 'directory') {
|
||||||
|
dir = this.active_file
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const find_parent = (dir, parent) => {
|
||||||
|
if(dir.path == this.active_file.path) {
|
||||||
|
return parent
|
||||||
|
}
|
||||||
|
if(dir.children == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return map_find(dir.children, c => find_parent(c, dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = find_parent(root)
|
||||||
|
|
||||||
|
if(dir == null) {
|
||||||
|
throw new Error('illegal state')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = dir == root ? name : dir.path + '/' + name
|
||||||
|
await create_file(path, is_dir)
|
||||||
|
|
||||||
|
// Reload all files for simplicity
|
||||||
|
load_dir(false).then(dir => {
|
||||||
|
if(is_dir) {
|
||||||
|
exec('load_dir', dir)
|
||||||
|
} else {
|
||||||
|
exec('create_file', dir, path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
on_click(e, file) {
|
||||||
|
e.stopPropagation()
|
||||||
|
this.active_el.querySelector('.file_title').classList.remove('active')
|
||||||
|
this.active_el = e.currentTarget.parentElement
|
||||||
|
e.currentTarget.classList.add('active')
|
||||||
|
this.active_file = file
|
||||||
|
if(file.kind != 'directory') {
|
||||||
|
exec('change_current_module', file.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
267
src/editor/ui.js
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import {exec, get_state} from '../index.js'
|
||||||
|
import {Editor} from './editor.js'
|
||||||
|
import {Files} from './files.js'
|
||||||
|
import {CallTree} from './calltree.js'
|
||||||
|
import {Eval} from './eval.js'
|
||||||
|
import {el} from './domutils.js'
|
||||||
|
import {FLAGS} from '../feature_flags.js'
|
||||||
|
|
||||||
|
export class UI {
|
||||||
|
constructor(container, state){
|
||||||
|
this.change_entrypoint = this.change_entrypoint.bind(this)
|
||||||
|
|
||||||
|
this.files = new Files(this)
|
||||||
|
|
||||||
|
container.appendChild(
|
||||||
|
(this.root = el('div',
|
||||||
|
'root ' + (FLAGS.embed_value_explorer ? 'embed_value_explorer' : ''),
|
||||||
|
this.editor_container = el('div', 'editor_container'),
|
||||||
|
FLAGS.embed_value_explorer
|
||||||
|
? null
|
||||||
|
: (this.eval_container = el('div', {class: 'eval'})),
|
||||||
|
el('div', 'bottom',
|
||||||
|
this.calltree_container = el('div', {"class": 'calltree', tabindex: 0}),
|
||||||
|
this.problems_container = el('div', {"class": 'problems', tabindex: 0}),
|
||||||
|
this.entrypoint_select = el('div', 'entrypoint_select')
|
||||||
|
),
|
||||||
|
|
||||||
|
this.files.el,
|
||||||
|
|
||||||
|
this.statusbar = el('div', 'statusbar',
|
||||||
|
this.status = el('div', 'status'),
|
||||||
|
this.current_module = el('div', 'current_module'),
|
||||||
|
/*
|
||||||
|
// Fullscreen cancelled on escape, TODO
|
||||||
|
el('a', {
|
||||||
|
"class" : 'request_fullscreen',
|
||||||
|
href: 'javascript:void(0)',
|
||||||
|
click: e => document.body.requestFullscreen(),
|
||||||
|
},
|
||||||
|
'Fullscreen'
|
||||||
|
),
|
||||||
|
*/
|
||||||
|
this.options = el('div', 'options',
|
||||||
|
el('label', {'for': 'standard'},
|
||||||
|
el('input', {
|
||||||
|
id: 'standard',
|
||||||
|
type: 'radio',
|
||||||
|
name: 'keyboard',
|
||||||
|
checked: localStorage.keyboard == 'standard'
|
||||||
|
|| localStorage.keyboard == null,
|
||||||
|
change: () => {
|
||||||
|
this.editor.set_keyboard_handler('standard')
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
'Standard'
|
||||||
|
),
|
||||||
|
el('label', {'for': 'vim'},
|
||||||
|
el('input', {
|
||||||
|
id: 'vim',
|
||||||
|
type: 'radio',
|
||||||
|
name: 'keyboard',
|
||||||
|
checked: localStorage.keyboard == 'vim',
|
||||||
|
change: () => {
|
||||||
|
this.editor.set_keyboard_handler('vim')
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
'VIM'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
el('a', {
|
||||||
|
'class': 'show_help',
|
||||||
|
href: 'javascript: void(0)',
|
||||||
|
click: () => this.help_dialog.showModal(),
|
||||||
|
},
|
||||||
|
'Help',
|
||||||
|
),
|
||||||
|
el('a', {
|
||||||
|
'class': 'github',
|
||||||
|
href: 'https://github.com/leporello-js/leporello-js',
|
||||||
|
target: '__blank',
|
||||||
|
}, 'Github'),
|
||||||
|
this.help_dialog = this.render_help(),
|
||||||
|
)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
this.root.addEventListener('keydown', () => this.clear_status(), true)
|
||||||
|
this.root.addEventListener('click', () => this.clear_status(), true)
|
||||||
|
|
||||||
|
this.editor_container.addEventListener('keydown', e => {
|
||||||
|
if(
|
||||||
|
e.key.toLowerCase() == 'w' && e.ctrlKey == true
|
||||||
|
||
|
||||||
|
// We bind F1 later, this one to work from embed_value_explorer
|
||||||
|
e.key == 'F1'
|
||||||
|
){
|
||||||
|
this.calltree_container.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.calltree_container.addEventListener('keydown', e => {
|
||||||
|
if(
|
||||||
|
(e.key.toLowerCase() == 'w' && e.ctrlKey == true)
|
||||||
|
||
|
||||||
|
e.key == 'Escape'
|
||||||
|
){
|
||||||
|
this.editor.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if(!FLAGS.embed_value_explorer) {
|
||||||
|
this.eval = new Eval(this, this.eval_container)
|
||||||
|
} else {
|
||||||
|
// Stub
|
||||||
|
this.eval = {
|
||||||
|
show_value_or_error(){},
|
||||||
|
clear_value_or_error(){},
|
||||||
|
focus_value_or_error(){},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor = new Editor(this, this.editor_container)
|
||||||
|
|
||||||
|
this.calltree = new CallTree(this, this.calltree_container)
|
||||||
|
|
||||||
|
// TODO jump to another module
|
||||||
|
// TODO use exec
|
||||||
|
const jump_to_fn_location = (e) => {
|
||||||
|
let loc
|
||||||
|
if((loc = e.target.dataset.location) != null){
|
||||||
|
loc = JSON.parse(loc)
|
||||||
|
this.editor.set_caret_position(loc.index)
|
||||||
|
this.editor.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO when click in calltree, do not jump to location, navigateCallTree
|
||||||
|
// instead
|
||||||
|
this.calltree_container.addEventListener('click', jump_to_fn_location)
|
||||||
|
|
||||||
|
this.render_entrypoint_select(state)
|
||||||
|
this.render_current_module(state.current_module)
|
||||||
|
}
|
||||||
|
|
||||||
|
render_entrypoint_select(state) {
|
||||||
|
this.entrypoint_select.replaceChildren(
|
||||||
|
el('span', 'entrypoint_title', 'entrypoint'),
|
||||||
|
el('select', {
|
||||||
|
click: e => e.stopPropagation(),
|
||||||
|
change: this.change_entrypoint,
|
||||||
|
},
|
||||||
|
Object.keys(state.files).sort().map(f =>
|
||||||
|
el('option',
|
||||||
|
state.entrypoint == f
|
||||||
|
? { value: f, selected: true }
|
||||||
|
: { value: f},
|
||||||
|
f == '' ? "*scratch*" : f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
change_entrypoint(e) {
|
||||||
|
const file = e.target.value
|
||||||
|
const index = this.editor.get_caret_position(file)
|
||||||
|
// if index is null, session was not created, and index after session
|
||||||
|
// creation will be 0
|
||||||
|
?? 0
|
||||||
|
exec('change_entrypoint', file, index)
|
||||||
|
this.editor.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
render_calltree(state) {
|
||||||
|
this.calltree_container.style = ''
|
||||||
|
this.problems_container.style = 'display: none'
|
||||||
|
this.calltree.render_calltree(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
render_problems(problems) {
|
||||||
|
this.calltree_container.style = 'display: none'
|
||||||
|
this.problems_container.style = ''
|
||||||
|
this.problems_container.innerHTML = ''
|
||||||
|
problems.forEach(p => {
|
||||||
|
const s = this.editor.get_session(p.module)
|
||||||
|
const pos = s.doc.indexToPosition(p.index)
|
||||||
|
const module = p.module == '' ? "*scratch*" : p.module
|
||||||
|
this.problems_container.appendChild(
|
||||||
|
el('div', 'problem',
|
||||||
|
el('a', {
|
||||||
|
href: 'javascript:void(0)',
|
||||||
|
click: () => exec('goto_problem', p)
|
||||||
|
},
|
||||||
|
`${module}:${pos.row + 1}:${pos.column} - ${p.message}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
set_status(text){
|
||||||
|
this.current_module.style = 'display: none'
|
||||||
|
this.status.style = ''
|
||||||
|
this.status.innerText = text
|
||||||
|
}
|
||||||
|
|
||||||
|
clear_status(){
|
||||||
|
this.render_current_module(get_state().current_module)
|
||||||
|
}
|
||||||
|
|
||||||
|
render_current_module(current_module) {
|
||||||
|
this.status.style = 'display: none'
|
||||||
|
this.current_module.innerText =
|
||||||
|
current_module == ''
|
||||||
|
? '*scratch*'
|
||||||
|
: current_module
|
||||||
|
this.current_module.style = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
render_help() {
|
||||||
|
const options = [
|
||||||
|
['Switch between editor and call tree', 'F1 or Ctrl-w'],
|
||||||
|
['Go from call tree to editor', 'F1 or Esc'],
|
||||||
|
['Focus value explorer', 'F2'],
|
||||||
|
['Navigate value explorer', '← → ↑ ↓ or hjkl'],
|
||||||
|
['Leave value explorer', 'Esc'],
|
||||||
|
['Jump to definition', 'F3', 'gd'],
|
||||||
|
['Expand selection to eval expression', 'Ctrl-↓ or Ctrl-j'],
|
||||||
|
['Collapse selection', 'Ctrl-↑ or Ctrl-k'],
|
||||||
|
['Navigate call tree view', '← → ↑ ↓ or hjkl'],
|
||||||
|
['Step into call', 'Ctrl-i', '\\i'],
|
||||||
|
['Step out of call', 'Ctrl-o', '\\o'],
|
||||||
|
['When in call tree view, jump to return statement', 'Enter'],
|
||||||
|
['When in call tree view, jump to function arguments', 'a'],
|
||||||
|
]
|
||||||
|
return el('dialog', 'help_dialog',
|
||||||
|
el('table', 'help',
|
||||||
|
el('thead', '',
|
||||||
|
el('th', '', 'Action'),
|
||||||
|
el('th', 'key', 'Standard'),
|
||||||
|
el('th', 'key', 'VIM'),
|
||||||
|
),
|
||||||
|
el('tbody', '',
|
||||||
|
options.map(([text, standard, vim]) =>
|
||||||
|
el('tr', '',
|
||||||
|
el('td', '', text),
|
||||||
|
el('td',
|
||||||
|
vim == null
|
||||||
|
? {'class': 'key spanned', colspan: 2}
|
||||||
|
: {'class': 'key'},
|
||||||
|
standard
|
||||||
|
),
|
||||||
|
vim == null
|
||||||
|
? null
|
||||||
|
: el('td', 'key', vim),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
el('form', {method: 'dialog'},
|
||||||
|
el('button', null, 'Close'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
356
src/editor/value_explorer.js
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
// TODO large arrays/objects
|
||||||
|
// TODO maps, sets
|
||||||
|
// TODO show Errors in red
|
||||||
|
|
||||||
|
import {el, stringify, scrollIntoViewIfNeeded} from './domutils.js'
|
||||||
|
|
||||||
|
const displayed_entries = object => {
|
||||||
|
if(Array.isArray(object)) {
|
||||||
|
return object.map((v, i) => [i, v])
|
||||||
|
} else {
|
||||||
|
const result = Object.entries(object)
|
||||||
|
return (object instanceof Error)
|
||||||
|
? [['message', object.message], ...result]
|
||||||
|
: result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_expandable = v => typeof(v) == 'object'
|
||||||
|
&& v != null
|
||||||
|
&& displayed_entries(v).length != 0
|
||||||
|
|
||||||
|
|
||||||
|
export const stringify_for_header = v => {
|
||||||
|
const type = typeof(v)
|
||||||
|
|
||||||
|
if(v === null) {
|
||||||
|
return 'null'
|
||||||
|
} else if(v === undefined) {
|
||||||
|
return 'undefined'
|
||||||
|
} else if(type == 'function') {
|
||||||
|
// TODO clickable link, 'fn', cursive
|
||||||
|
return 'fn ' + v.name
|
||||||
|
} else if(v instanceof Error) {
|
||||||
|
return v.toString()
|
||||||
|
} else if(type == 'object') {
|
||||||
|
if(Array.isArray(v)) {
|
||||||
|
if(v.length == 0) {
|
||||||
|
return '[]'
|
||||||
|
} else {
|
||||||
|
return '[…]'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(displayed_entries(v).length == 0) {
|
||||||
|
return '{}'
|
||||||
|
} else {
|
||||||
|
return '{…}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if(type == 'string') {
|
||||||
|
return JSON.stringify(v)
|
||||||
|
} else {
|
||||||
|
return v.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = object => {
|
||||||
|
if(typeof(object) == 'undefined') {
|
||||||
|
return 'undefined'
|
||||||
|
} else if(object == null) {
|
||||||
|
return 'null'
|
||||||
|
} else if(typeof(object) == 'object') {
|
||||||
|
if(object instanceof Error) {
|
||||||
|
return object.toString()
|
||||||
|
} else if(Array.isArray(object)) {
|
||||||
|
return '['
|
||||||
|
+ object
|
||||||
|
.map(stringify_for_header)
|
||||||
|
.join(', ')
|
||||||
|
+ ']'
|
||||||
|
} else {
|
||||||
|
const inner = displayed_entries(object)
|
||||||
|
.map(([k,v]) => {
|
||||||
|
const value = stringify_for_header(v)
|
||||||
|
return `${k}: ${value}`
|
||||||
|
})
|
||||||
|
.join(', ')
|
||||||
|
return `{${inner}}`
|
||||||
|
}
|
||||||
|
} else if(typeof(object) == 'function') {
|
||||||
|
// TODO clickable link, 'fn', cursive
|
||||||
|
return 'fn ' + object.name
|
||||||
|
} else if(typeof(object) == 'string') {
|
||||||
|
return JSON.stringify(object)
|
||||||
|
} else {
|
||||||
|
return object.toString()
|
||||||
|
}
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
const get_path = (o, path) => {
|
||||||
|
if(path.length == 0) {
|
||||||
|
return o
|
||||||
|
} else {
|
||||||
|
const [start, ...rest] = path
|
||||||
|
return get_path(o[start], rest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValueExplorer {
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
container,
|
||||||
|
event_target = container,
|
||||||
|
scroll_to_element,
|
||||||
|
on_escape = () => {},
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
this.container = container
|
||||||
|
this.scroll_to_element = scroll_to_element
|
||||||
|
this.on_escape = on_escape
|
||||||
|
|
||||||
|
event_target.addEventListener('keydown', (e) => {
|
||||||
|
|
||||||
|
/*
|
||||||
|
Right -
|
||||||
|
- does not has children - nothing
|
||||||
|
- has children - first click expands, second jumps to first element
|
||||||
|
|
||||||
|
Left -
|
||||||
|
- root - nothing
|
||||||
|
- not root collapse node, goes to parent if already collapsed
|
||||||
|
|
||||||
|
Up - goes to prev visible element
|
||||||
|
Down - goes to next visible element
|
||||||
|
|
||||||
|
Click - select and toggles expand
|
||||||
|
*/
|
||||||
|
|
||||||
|
const current_object = get_path(this.value, this.current_path)
|
||||||
|
|
||||||
|
if(e.key == 'ArrowDown' || e.key == 'j'){
|
||||||
|
// Do not scroll
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if(is_expandable(current_object) && this.is_expanded(this.current_path)) {
|
||||||
|
this.select_path(this.current_path.concat(
|
||||||
|
displayed_entries(current_object)[0][0]
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
const next = p => {
|
||||||
|
if(p.length == 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const parent = p.slice(0, p.length - 1)
|
||||||
|
const children = displayed_entries(get_path(this.value, parent))
|
||||||
|
const child_index = children.findIndex(([k,v]) =>
|
||||||
|
k == p[p.length - 1]
|
||||||
|
)
|
||||||
|
const next_child = children[child_index + 1]
|
||||||
|
if(next_child == null) {
|
||||||
|
return next(parent)
|
||||||
|
} else {
|
||||||
|
return [...parent, next_child[0]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const next_path = next(this.current_path)
|
||||||
|
if(next_path != null) {
|
||||||
|
this.select_path(next_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(e.key == 'ArrowUp' || e.key == 'k'){
|
||||||
|
// Do not scroll
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if(this.current_path.length == 0) {
|
||||||
|
this.on_escape()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parent = this.current_path.slice(0, this.current_path.length - 1)
|
||||||
|
const children = displayed_entries(get_path(this.value, parent))
|
||||||
|
const child_index = children.findIndex(([k,v]) =>
|
||||||
|
k == this.current_path[this.current_path.length - 1]
|
||||||
|
)
|
||||||
|
const next_child = children[child_index - 1]
|
||||||
|
if(next_child == null) {
|
||||||
|
this.select_path(parent)
|
||||||
|
} else {
|
||||||
|
const last = p => {
|
||||||
|
if(!is_expandable(get_path(this.value, p)) || !this.is_expanded(p)) {
|
||||||
|
return p
|
||||||
|
} else {
|
||||||
|
const children = displayed_entries(get_path(this.value, p))
|
||||||
|
.map(([k,v]) => k)
|
||||||
|
return last([...p, children[children.length - 1]])
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.select_path(last([...parent, next_child[0]]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(e.key == 'ArrowLeft' || e.key == 'h'){
|
||||||
|
// Do not scroll
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const is_expanded = this.is_expanded(this.current_path)
|
||||||
|
if(!is_expandable(current_object) || !is_expanded) {
|
||||||
|
if(this.current_path.length != 0) {
|
||||||
|
const parent = this.current_path.slice(0, this.current_path.length - 1)
|
||||||
|
this.select_path(parent)
|
||||||
|
} else {
|
||||||
|
this.on_escape()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.toggle_expanded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(e.key == 'ArrowRight' || e.key == 'l'){
|
||||||
|
// Do not scroll
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if(is_expandable(current_object)) {
|
||||||
|
const is_expanded = this.is_expanded(this.current_path)
|
||||||
|
if(!is_expanded) {
|
||||||
|
this.toggle_expanded()
|
||||||
|
} else {
|
||||||
|
const children = displayed_entries(get_path(this.value, this.current_path))
|
||||||
|
this.select_path(
|
||||||
|
[
|
||||||
|
...this.current_path,
|
||||||
|
children[0][0],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get_node_data(path, node_data = this.node_data) {
|
||||||
|
if(path.length == 0) {
|
||||||
|
return node_data
|
||||||
|
} else {
|
||||||
|
const [start, ...rest] = path
|
||||||
|
return this.get_node_data(rest, node_data.children[start])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is_expanded(path) {
|
||||||
|
return this.get_node_data(path).is_expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
on_click(path) {
|
||||||
|
this.select_path(path)
|
||||||
|
this.toggle_expanded()
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.container.innerHTML = ''
|
||||||
|
this.node_data = {is_expanded: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
render(value) {
|
||||||
|
this.clear()
|
||||||
|
this.value = value
|
||||||
|
const path = []
|
||||||
|
this.container.appendChild(this.render_value_explorer_node(null, value, path, this.node_data))
|
||||||
|
this.select_path(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
select_path(current_path) {
|
||||||
|
if(this.current_path != null) {
|
||||||
|
this.set_active(this.current_path, false)
|
||||||
|
}
|
||||||
|
this.current_path = current_path
|
||||||
|
this.set_active(this.current_path, true)
|
||||||
|
// Check that was already added to document
|
||||||
|
if(document.contains(this.container)) {
|
||||||
|
const target = this.get_node_data(current_path).el.getElementsByClassName('value_explorer_header')[0]
|
||||||
|
if(this.scroll_to_element == null) {
|
||||||
|
scrollIntoViewIfNeeded(this.container.parentNode, target)
|
||||||
|
} else {
|
||||||
|
this.scroll_to_element(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_active(path, is_active) {
|
||||||
|
const el = this.get_node_data(path).el.getElementsByClassName('value_explorer_header')[0]
|
||||||
|
if(is_active) {
|
||||||
|
el.classList.add('active')
|
||||||
|
} else {
|
||||||
|
el.classList.remove('active')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_expanded(fn) {
|
||||||
|
if(typeof(fn) == 'boolean') {
|
||||||
|
return this.set_expanded(() => fn)
|
||||||
|
}
|
||||||
|
const val = this.is_expanded(this.current_path)
|
||||||
|
const data = this.get_node_data(this.current_path)
|
||||||
|
data.is_expanded = fn(data.is_expanded)
|
||||||
|
const prev_dom_node = data.el
|
||||||
|
const key = this.current_path.length == 0
|
||||||
|
? null
|
||||||
|
: this.current_path[this.current_path.length - 1]
|
||||||
|
const value = get_path(this.value, this.current_path)
|
||||||
|
const next = this.render_value_explorer_node(key, value, this.current_path, data)
|
||||||
|
prev_dom_node.parentNode.replaceChild(next, prev_dom_node)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle_expanded() {
|
||||||
|
this.set_expanded(e => !e)
|
||||||
|
this.set_active(this.current_path, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
render_value_explorer_node(key, value, path, node_data) {
|
||||||
|
|
||||||
|
const is_exp = is_expandable(value)
|
||||||
|
const is_expanded = is_exp && node_data.is_expanded
|
||||||
|
|
||||||
|
node_data.children = {}
|
||||||
|
|
||||||
|
const result = el('div', 'value_explorer_node',
|
||||||
|
|
||||||
|
el('span', {
|
||||||
|
class: 'value_explorer_header',
|
||||||
|
click: this.on_click.bind(this, path),
|
||||||
|
},
|
||||||
|
is_exp
|
||||||
|
? (is_expanded ? '▼' : '▶')
|
||||||
|
: '\xa0',
|
||||||
|
|
||||||
|
key == null
|
||||||
|
? null
|
||||||
|
: el('span', 'value_explorer_key', key.toString(), ': '),
|
||||||
|
|
||||||
|
key == null || !is_exp || !is_expanded
|
||||||
|
// Full header
|
||||||
|
? header(value)
|
||||||
|
// Short header
|
||||||
|
: Array.isArray(value)
|
||||||
|
? 'Array(' + value.length + ')'
|
||||||
|
: ''
|
||||||
|
),
|
||||||
|
|
||||||
|
(is_exp && is_expanded)
|
||||||
|
? displayed_entries(value).map(([k,v]) => {
|
||||||
|
node_data.children[k] = {}
|
||||||
|
return this.render_value_explorer_node(k, v, [...path, k], node_data.children[k])
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
|
||||||
|
node_data.el = result
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
232
src/effects.js
vendored
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import {write_file} from './filesystem.js'
|
||||||
|
import {color_file} from './color.js'
|
||||||
|
import {root_calltree_node, calltree_node_loc} from './calltree.js'
|
||||||
|
import {FLAGS} from './feature_flags.js'
|
||||||
|
|
||||||
|
const ensure_session = (ui, state, file = state.current_module) => {
|
||||||
|
ui.editor.ensure_session(file, state.files[file])
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear_coloring = (ui, file) => {
|
||||||
|
ui.editor.remove_markers_of_type(file, 'evaluated_ok')
|
||||||
|
ui.editor.remove_markers_of_type(file, 'evaluated_error')
|
||||||
|
}
|
||||||
|
|
||||||
|
const render_coloring = (ui, state) => {
|
||||||
|
const file = state.current_module
|
||||||
|
|
||||||
|
clear_coloring(ui, file)
|
||||||
|
|
||||||
|
color_file(state, file).forEach(c => {
|
||||||
|
ui.editor.add_marker(
|
||||||
|
file,
|
||||||
|
c.result.ok
|
||||||
|
? 'evaluated_ok'
|
||||||
|
: 'evaluated_error',
|
||||||
|
c.index,
|
||||||
|
c.index + c.length
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const render_parse_result = (ui, state) => {
|
||||||
|
ui.editor.for_each_session((file, session) => {
|
||||||
|
ui.editor.remove_markers_of_type(file, 'error-code')
|
||||||
|
session.clearAnnotations()
|
||||||
|
})
|
||||||
|
|
||||||
|
if(!state.parse_result.ok){
|
||||||
|
|
||||||
|
ui.editor.for_each_session((file, session) => {
|
||||||
|
session.setAnnotations(
|
||||||
|
state.parse_result.problems
|
||||||
|
.filter(p => p.module == file)
|
||||||
|
.map(p => {
|
||||||
|
const pos = session.doc.indexToPosition(p.index)
|
||||||
|
return {
|
||||||
|
row: pos.row,
|
||||||
|
column: pos.column,
|
||||||
|
text: p.message,
|
||||||
|
type: "error",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
state.parse_result.problems.forEach(problem => {
|
||||||
|
ensure_session(ui, state, problem.module)
|
||||||
|
// TODO unexpected end of input
|
||||||
|
ui.editor.add_marker(
|
||||||
|
problem.module,
|
||||||
|
'error-code',
|
||||||
|
problem.index,
|
||||||
|
// TODO check if we can show token
|
||||||
|
problem.token == null
|
||||||
|
? problem.index + 1
|
||||||
|
: problem.index + problem.token.length
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
ui.render_problems(state.parse_result.problems)
|
||||||
|
} else {
|
||||||
|
// Ensure session for each loaded module
|
||||||
|
Object.keys(state.parse_result.modules).forEach(file => {
|
||||||
|
ensure_session(ui, state, file)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const render_initial_state = (ui, state) => {
|
||||||
|
ensure_session(ui, state)
|
||||||
|
ui.editor.switch_session(state.current_module)
|
||||||
|
render_parse_result(ui, state)
|
||||||
|
if(state.current_calltree_node != null) {
|
||||||
|
ui.render_calltree(state)
|
||||||
|
render_coloring(ui, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const render_common_side_effects = (prev, next, command, ui) => {
|
||||||
|
if(
|
||||||
|
prev.project_dir != next.project_dir
|
||||||
|
||
|
||||||
|
prev.current_module != next.current_module
|
||||||
|
) {
|
||||||
|
ui.render_entrypoint_select(next)
|
||||||
|
ui.files.render(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(prev.current_module != next.current_module) {
|
||||||
|
localStorage.current_module = next.current_module
|
||||||
|
ui.render_current_module(next.current_module)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(prev.entrypoint != next.entrypoint) {
|
||||||
|
localStorage.entrypoint = next.entrypoint
|
||||||
|
}
|
||||||
|
|
||||||
|
if(prev.current_module != next.current_module) {
|
||||||
|
ensure_session(ui, next)
|
||||||
|
ui.editor.unembed_value_explorer()
|
||||||
|
ui.editor.switch_session(next.current_module)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(prev.parse_result != next.parse_result) {
|
||||||
|
render_parse_result(ui, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(next.current_calltree_node == null) {
|
||||||
|
|
||||||
|
ui.calltree.clear_calltree()
|
||||||
|
ui.editor.for_each_session((file, session) => clear_coloring(ui, file))
|
||||||
|
ui.editor.unembed_value_explorer()
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if(
|
||||||
|
prev.current_calltree_node == null
|
||||||
|
||
|
||||||
|
prev.calltree_changed_token != next.calltree_changed_token
|
||||||
|
) {
|
||||||
|
// Rerender entire calltree
|
||||||
|
ui.render_calltree(next)
|
||||||
|
ui.eval.clear_value_or_error()
|
||||||
|
ui.editor.for_each_session(f => clear_coloring(ui, f))
|
||||||
|
render_coloring(ui, next)
|
||||||
|
ui.editor.unembed_value_explorer()
|
||||||
|
} else {
|
||||||
|
const node_changed = next.current_calltree_node != prev.current_calltree_node
|
||||||
|
const id = next.current_calltree_node.id
|
||||||
|
const exp_changed = !!prev.calltree_node_is_expanded[id]
|
||||||
|
!=
|
||||||
|
!!next.calltree_node_is_expanded[id]
|
||||||
|
|
||||||
|
if(node_changed) {
|
||||||
|
ui.calltree.render_select_node(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(exp_changed) {
|
||||||
|
ui.calltree.render_expand_node(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(node_changed) {
|
||||||
|
if(!next.current_calltree_node.toplevel) {
|
||||||
|
ui.eval.show_value_or_error(next.current_calltree_node)
|
||||||
|
} else {
|
||||||
|
ui.eval.clear_value_or_error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(prev.calltree_node_by_loc != next.calltree_node_by_loc) {
|
||||||
|
render_coloring(ui, next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render
|
||||||
|
|
||||||
|
/* Eval selection */
|
||||||
|
|
||||||
|
const selnode = next.selection_state?.node
|
||||||
|
if(prev.selection_state?.node != selnode) {
|
||||||
|
ui.editor.remove_markers_of_type(next.current_module, 'selection')
|
||||||
|
if(selnode != null) {
|
||||||
|
ui.editor.add_marker(
|
||||||
|
next.current_module,
|
||||||
|
'selection',
|
||||||
|
selnode.index,
|
||||||
|
selnode.index + selnode.length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selresult = next.selection_state?.result
|
||||||
|
if(selresult != null && prev.selection_state?.result != selresult) {
|
||||||
|
if(FLAGS.embed_value_explorer) {
|
||||||
|
const node = next.selection_state.node
|
||||||
|
ui.editor.embed_value_explorer({
|
||||||
|
index: node.index + node.length,
|
||||||
|
result: next.selection_state.result,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
ui.eval.show_value_or_error(next.selection_state.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const EFFECTS = {
|
||||||
|
set_caret_position: (state, [index, with_focus], ui) => {
|
||||||
|
ui.editor.set_caret_position(index)
|
||||||
|
if(with_focus) {
|
||||||
|
ui.editor.focus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
set_status: (state, [msg], ui) => {
|
||||||
|
ui.set_status(msg)
|
||||||
|
},
|
||||||
|
|
||||||
|
save_to_localstorage(state, [key, value]){
|
||||||
|
localStorage[key] = value
|
||||||
|
},
|
||||||
|
|
||||||
|
write: (state, [name, contents], ui) => write_file(name, contents),
|
||||||
|
|
||||||
|
embed_value_explorer(state, [{index, result}], ui){
|
||||||
|
if(FLAGS.embed_value_explorer) {
|
||||||
|
ui.editor.embed_value_explorer({index, result})
|
||||||
|
} else {
|
||||||
|
ui.eval.show_value_or_error(result)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
unembed_value_explorer(state, _, ui){
|
||||||
|
if(FLAGS.embed_value_explorer) {
|
||||||
|
ui.editor.unembed_value_explorer()
|
||||||
|
} else {
|
||||||
|
ui.eval.clear_value_or_error()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
1224
src/eval.js
Normal file
3
src/feature_flags.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const FLAGS = {
|
||||||
|
embed_value_explorer: true,
|
||||||
|
}
|
||||||
135
src/filesystem.js
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// code is borrowed from
|
||||||
|
// https://googlechrome.github.io/samples/service-worker/post-message/
|
||||||
|
const send_message = (message) => {
|
||||||
|
return new Promise(function(resolve) {
|
||||||
|
const messageChannel = new MessageChannel();
|
||||||
|
messageChannel.port1.onmessage = function(event) {
|
||||||
|
resolve(event.data)
|
||||||
|
};
|
||||||
|
if(navigator.serviceWorker.controller == null) {
|
||||||
|
// Service worker will be available after reload
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
navigator.serviceWorker.controller.postMessage(message,
|
||||||
|
[messageChannel.port2]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.clear_directory_handle = () => {
|
||||||
|
send_message({type: 'SET', data: null})
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir_handle
|
||||||
|
|
||||||
|
const request_directory_handle = async () => {
|
||||||
|
dir_handle = await showDirectoryPicker()
|
||||||
|
await send_message({type: 'SET', data: dir_handle})
|
||||||
|
return dir_handle
|
||||||
|
}
|
||||||
|
|
||||||
|
export const load_persisted_directory_handle = () => {
|
||||||
|
return navigator.serviceWorker.register('service_worker.js')
|
||||||
|
.then(() => navigator.serviceWorker.ready)
|
||||||
|
.then(() => send_message({type: 'GET'}))
|
||||||
|
.then(async h => {
|
||||||
|
if(h == null || (await h.queryPermission()) != 'granted') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
// test if directory handle is valid
|
||||||
|
try {
|
||||||
|
await h.entries().next()
|
||||||
|
} catch(e) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
dir_handle = h
|
||||||
|
return dir_handle
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const file_handle = async (dir_handle, filename, is_directory = false, options) => {
|
||||||
|
if(typeof(filename) == 'string') {
|
||||||
|
filename = filename.split('/')
|
||||||
|
}
|
||||||
|
const [first, ...rest] = filename
|
||||||
|
if(rest.length == 0) {
|
||||||
|
return is_directory
|
||||||
|
? await dir_handle.getDirectoryHandle(first, options)
|
||||||
|
: await dir_handle.getFileHandle(first, options)
|
||||||
|
} else {
|
||||||
|
const nested_dir_handle = await dir_handle.getDirectoryHandle(first)
|
||||||
|
return file_handle(nested_dir_handle, rest, is_directory, options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const write_file = async (name, contents) => {
|
||||||
|
const f_hanlde = await file_handle(dir_handle, name)
|
||||||
|
// Create a FileSystemWritableFileStream to write to.
|
||||||
|
const writable = await f_hanlde.createWritable()
|
||||||
|
// Write the contents of the file to the stream.
|
||||||
|
await writable.write(contents)
|
||||||
|
// Close the file and write the contents to disk.
|
||||||
|
await writable.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blacklist hidden dirs and node_modules
|
||||||
|
const is_blacklisted = h => h.name == 'node_modules' || h.name.startsWith('.')
|
||||||
|
|
||||||
|
const read_file = async handle => {
|
||||||
|
const file_data = await handle.getFile()
|
||||||
|
return await file_data.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
const do_load_dir = async (handle, path) => {
|
||||||
|
if(handle.kind == 'directory') {
|
||||||
|
const children = []
|
||||||
|
for await (let [name, h] of handle) {
|
||||||
|
if(!is_blacklisted(h)) {
|
||||||
|
children.push(h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name: handle.name,
|
||||||
|
path,
|
||||||
|
kind: 'directory',
|
||||||
|
children: await Promise.all(
|
||||||
|
children
|
||||||
|
.map(c =>
|
||||||
|
do_load_dir(c, path == null ? c.name : path + '/' + c.name)
|
||||||
|
)
|
||||||
|
.sort((a,b) => a.name > b.name)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if(handle.kind == 'file') {
|
||||||
|
return {
|
||||||
|
name: handle.name,
|
||||||
|
path,
|
||||||
|
kind: 'file',
|
||||||
|
contents: await read_file(handle)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('unknown kind')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const create_file = (path, is_dir) => {
|
||||||
|
return file_handle(
|
||||||
|
dir_handle,
|
||||||
|
path,
|
||||||
|
is_dir,
|
||||||
|
{create: true}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const load_dir = async (should_request_access) => {
|
||||||
|
let handle
|
||||||
|
if(should_request_access) {
|
||||||
|
handle = await request_directory_handle()
|
||||||
|
} else {
|
||||||
|
handle = await load_persisted_directory_handle()
|
||||||
|
if(handle == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return do_load_dir(handle, null)
|
||||||
|
}
|
||||||
304
src/find_definitions.js
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
// TODO rename to analyze.js
|
||||||
|
|
||||||
|
import {set_push, set_diff, set_union, map_object, map_find, uniq} from './utils.js'
|
||||||
|
import {collect_destructuring_identifiers, collect_imports, ancestry, find_leaf} from './ast_utils.js'
|
||||||
|
|
||||||
|
// TODO get complete list of globals (borrow from eslint?)
|
||||||
|
import {globals} from './globals.js'
|
||||||
|
|
||||||
|
const map_find_definitions = (nodes, mapper) => {
|
||||||
|
const result = nodes.map(mapper)
|
||||||
|
const undeclared = result.reduce(
|
||||||
|
(acc, el) => el.undeclared == null ? acc : acc.concat(el.undeclared),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const closed = result.map(r => r.closed).reduce(set_union, new Set())
|
||||||
|
return {
|
||||||
|
nodes: result.map(r => r.node),
|
||||||
|
undeclared,
|
||||||
|
closed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const scope_from_node = n => {
|
||||||
|
if(n.type == 'import') {
|
||||||
|
return Object.fromEntries(
|
||||||
|
n.imports.map(i => [i.value, i])
|
||||||
|
)
|
||||||
|
} else if(n.type == 'export'){
|
||||||
|
return scope_from_node(n.binding)
|
||||||
|
} else if(n.type == 'const' || n.type == 'let'){
|
||||||
|
return Object.fromEntries(
|
||||||
|
collect_destructuring_identifiers(n.name_node).map(node => [
|
||||||
|
node.value, node
|
||||||
|
])
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const add_trivial_definition = node => {
|
||||||
|
if(node.type == 'identifier') {
|
||||||
|
return {...node, definition: 'self'}
|
||||||
|
} else if(['destructuring_default', 'destructuring_rest'].includes(node.type)){
|
||||||
|
return {...node,
|
||||||
|
children: [add_trivial_definition(node.name_node), ...node.children.slice(1)]
|
||||||
|
}
|
||||||
|
} else if(node.type == 'destructuring_pair') {
|
||||||
|
return {...node, children: [
|
||||||
|
node.children[0], // key
|
||||||
|
add_trivial_definition(node.children[1]), // value
|
||||||
|
]}
|
||||||
|
} else if(['array_destructuring', 'object_destructuring'].includes(node.type)) {
|
||||||
|
return {...node, children: node.children.map(add_trivial_definition)}
|
||||||
|
} else {
|
||||||
|
console.error(node)
|
||||||
|
throw new Error('not implemented')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The function does these things:
|
||||||
|
* - For each occurence of identifier, attaches definition to this identifier
|
||||||
|
* - For each closure, attaches 'closed` property with set of vars it closes
|
||||||
|
* over
|
||||||
|
* - Finds undeclared identifiers
|
||||||
|
*
|
||||||
|
* `scope` is names that are already defined and can be used immediately.
|
||||||
|
* `closure_scope` is names that are defined but not yet assigned, but they
|
||||||
|
* will be assigned by the time the closures would be called
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO in same pass find already declared
|
||||||
|
export const find_definitions = (ast, scope = {}, closure_scope = {}, module_name) => {
|
||||||
|
if(ast.type == 'identifier'){
|
||||||
|
if(ast.definition != null) {
|
||||||
|
// Definition previously added by add_trivial_definition
|
||||||
|
return {node: ast, undeclared: null, closed: new Set([ast.value])}
|
||||||
|
} else {
|
||||||
|
const definition = scope[ast.value]
|
||||||
|
if(definition == null){
|
||||||
|
if(globals.has(ast.value)) {
|
||||||
|
return {node: {...ast, definition: 'global'}, undeclared: null, closed: new Set()}
|
||||||
|
} else {
|
||||||
|
return {node: ast, undeclared: [ast], closed: new Set()}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
node: {...ast, definition: {index: definition.index}},
|
||||||
|
undeclared: null,
|
||||||
|
closed: new Set([ast.value])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if(ast.type == 'do'){
|
||||||
|
const children_with_scope = ast.children.reduce(
|
||||||
|
({scope, children}, node) => ({
|
||||||
|
scope: {...scope, ...scope_from_node(node)},
|
||||||
|
children: children.concat([{node, scope}]),
|
||||||
|
})
|
||||||
|
,
|
||||||
|
{scope: {}, children: []}
|
||||||
|
)
|
||||||
|
const local_scope = children_with_scope.scope
|
||||||
|
const {nodes, undeclared, closed} = map_find_definitions(children_with_scope.children, cs =>
|
||||||
|
find_definitions(cs.node, {...scope, ...cs.scope}, local_scope, module_name)
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
node: {...ast, children: nodes},
|
||||||
|
undeclared,
|
||||||
|
closed: set_diff(closed, new Set(Object.keys(local_scope))),
|
||||||
|
}
|
||||||
|
} else if (ast.type == 'function_expr'){
|
||||||
|
const args_identifiers = collect_destructuring_identifiers(ast.function_args)
|
||||||
|
const args_scope = Object.fromEntries(args_identifiers.map(a => [
|
||||||
|
a.value, a
|
||||||
|
]))
|
||||||
|
const {nodes, undeclared, closed} = map_find_definitions(ast.children,
|
||||||
|
node => find_definitions(node, {...scope, ...closure_scope, ...args_scope})
|
||||||
|
)
|
||||||
|
const next_closed = set_diff(closed, new Set(args_identifiers.map(a => a.value)))
|
||||||
|
return {
|
||||||
|
node: {...ast, children: nodes, closed: next_closed},
|
||||||
|
undeclared,
|
||||||
|
closed: new Set(),
|
||||||
|
}
|
||||||
|
} else if(ast.children != null){
|
||||||
|
let children, full_import_path
|
||||||
|
if(ast.type == 'import') {
|
||||||
|
full_import_path = concat_path(module_name, ast.module)
|
||||||
|
children = ast.children.map(c => ({...c, definition: {module: full_import_path}}))
|
||||||
|
} else if(ast.type == 'const') {
|
||||||
|
children = [add_trivial_definition(ast.name_node), ...ast.children.slice(1)]
|
||||||
|
} else if(ast.type == 'let') {
|
||||||
|
children = ast.name_node.map(add_trivial_definition)
|
||||||
|
} else {
|
||||||
|
children = ast.children
|
||||||
|
}
|
||||||
|
|
||||||
|
const {nodes, undeclared, closed} = map_find_definitions(children,
|
||||||
|
c => find_definitions(c, scope, closure_scope)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
node: ast.type == 'import'
|
||||||
|
? {...ast, children: nodes, full_import_path}
|
||||||
|
: {...ast, children: nodes},
|
||||||
|
undeclared,
|
||||||
|
closed
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {node: ast, undeclared: null, closed: new Set()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// see https://stackoverflow.com/a/29855511
|
||||||
|
|
||||||
|
// Joins path segments. Preserves initial "/" and resolves ".." and "."
|
||||||
|
// Does not support using ".." to go above/outside the root.
|
||||||
|
// This means that join("foo", "../../bar") will not resolve to "../bar"
|
||||||
|
|
||||||
|
const join = new Function(`
|
||||||
|
// Split the inputs into a list of path commands.
|
||||||
|
var parts = [];
|
||||||
|
for (var i = 0, l = arguments.length; i < l; i++) {
|
||||||
|
parts = parts.concat(arguments[i].split("/"));
|
||||||
|
}
|
||||||
|
// Interpret the path commands to get the new resolved path.
|
||||||
|
var newParts = [];
|
||||||
|
for (i = 0, l = parts.length; i < l; i++) {
|
||||||
|
var part = parts[i];
|
||||||
|
// Remove leading and trailing slashes
|
||||||
|
// Also remove "." segments
|
||||||
|
if (!part || part === ".") continue;
|
||||||
|
// Interpret ".." to pop the last segment
|
||||||
|
if (part === "..") newParts.pop();
|
||||||
|
// Push new path segments.
|
||||||
|
else newParts.push(part);
|
||||||
|
}
|
||||||
|
// Preserve the initial slash if there was one.
|
||||||
|
if (parts[0] === "") newParts.unshift("");
|
||||||
|
// Turn back into a single string path.
|
||||||
|
return newParts.join("/") || (newParts.length ? "/" : ".");
|
||||||
|
`)
|
||||||
|
|
||||||
|
const concat_path = (base, imported) =>
|
||||||
|
base == '' ? join(imported) : join(base, '..', imported)
|
||||||
|
|
||||||
|
export const find_export = (name, module) => {
|
||||||
|
return map_find(module.stmts, n => {
|
||||||
|
if(n.type != 'export') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const ids = collect_destructuring_identifiers(n.binding.name_node)
|
||||||
|
return ids.find(i => i.value == name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const topsort_modules = (modules) => {
|
||||||
|
const sort_module_deps = (module) => {
|
||||||
|
return Object.keys(collect_imports(modules[module]))
|
||||||
|
.reduce(
|
||||||
|
(result, m) => result.concat(sort_module_deps(m)),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
.concat(module)
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = Object.keys(modules).reduce(
|
||||||
|
(result, module) => result.concat(sort_module_deps(module)),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
// now remove duplicates
|
||||||
|
// quadratic, but N is supposed to be small
|
||||||
|
return sorted.reduce(
|
||||||
|
(result, elem) =>
|
||||||
|
result.includes(elem)
|
||||||
|
? result
|
||||||
|
: [...result, elem]
|
||||||
|
,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO not implemented
|
||||||
|
// TODO detect cycles when loading modules
|
||||||
|
export const check_imports = modules => {
|
||||||
|
// TODO allow circular imports
|
||||||
|
return map_object(modules, (module, node) => {
|
||||||
|
const imports = node.stmts
|
||||||
|
.filter(n => n.type == 'import')
|
||||||
|
.reduce(
|
||||||
|
(imports, n) => [
|
||||||
|
...imports,
|
||||||
|
...(n.imports.map(i => ({name: i.value, from: n.module})))
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const exports = node.statement
|
||||||
|
.filter(n => n.type == 'export')
|
||||||
|
.map(n => collect_destructuring_identifiers(n.binding.name_node))
|
||||||
|
.reduce((all, current) => [...all, ...current], [])
|
||||||
|
|
||||||
|
return {imports, exports}
|
||||||
|
//TODO check for each import, there is export
|
||||||
|
})
|
||||||
|
// Topological sort
|
||||||
|
// For each module
|
||||||
|
// Depth-traverse deps and detect cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
TODO: relax, only disallow code that leads to broken target code
|
||||||
|
|
||||||
|
code analysis:
|
||||||
|
- function must have one and only one return statement in every branch
|
||||||
|
- return must be the last statement in block
|
||||||
|
|
||||||
|
- name is declared once and only once (including function args). Name can be imported once
|
||||||
|
- let must be assigned once and only once (in each branch)
|
||||||
|
- every assignment can only be to if identifier is earlier declared by let
|
||||||
|
- assignment can only be inside if statement (after let) (relax it?)
|
||||||
|
- cannot import names that are not exported from modules
|
||||||
|
- cannot return from modules (even inside toplevel if statements)
|
||||||
|
*/
|
||||||
|
export const analyze = (node, is_toplevel = true) => {
|
||||||
|
// TODO remove
|
||||||
|
return []
|
||||||
|
|
||||||
|
/*
|
||||||
|
// TODO sort by location?
|
||||||
|
if(node.type == 'do') {
|
||||||
|
let illegal_returns
|
||||||
|
if(is_toplevel) {
|
||||||
|
illegal_returns = node.stmts.filter(s => s.type == 'return')
|
||||||
|
} else {
|
||||||
|
const last = node.stmts[node.stmts.length - 1];
|
||||||
|
illegal_returns = node.stmts.filter(s => s.type == 'return' && s != last);
|
||||||
|
|
||||||
|
returns.map(node => ({
|
||||||
|
node,
|
||||||
|
message: 'illegal return statement',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const last_return = last.type == 'return'
|
||||||
|
? null
|
||||||
|
: {node: last, message: 'block must end with return statement'}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO recur to childs
|
||||||
|
}
|
||||||
|
} else if(node.children != null){
|
||||||
|
return node.children
|
||||||
|
.map(n => analyze(n, node.type == 'function_expr' ? false : is_toplevel))
|
||||||
|
.reduce((ps, p) => ps.concat(p), [])
|
||||||
|
} else {
|
||||||
|
// TODO
|
||||||
|
1
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
39
src/globals.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
export const globals = new Set([
|
||||||
|
'globalThis',
|
||||||
|
// TODO Promise,
|
||||||
|
// TODO Symbol
|
||||||
|
'Set',
|
||||||
|
'Map',
|
||||||
|
"Infinity",
|
||||||
|
"NaN",
|
||||||
|
"undefined",
|
||||||
|
"Function",
|
||||||
|
"Object",
|
||||||
|
"Array",
|
||||||
|
"Number",
|
||||||
|
"String",
|
||||||
|
"Boolean",
|
||||||
|
"Date",
|
||||||
|
"Math",
|
||||||
|
"RegExp",
|
||||||
|
"JSON",
|
||||||
|
"Error",
|
||||||
|
"EvalError",
|
||||||
|
"RangeError",
|
||||||
|
"ReferenceError",
|
||||||
|
"SyntaxError",
|
||||||
|
"TypeError",
|
||||||
|
"URIError",
|
||||||
|
"isNaN",
|
||||||
|
"isFinite",
|
||||||
|
"parseFloat",
|
||||||
|
"parseInt",
|
||||||
|
"eval",
|
||||||
|
"escape",
|
||||||
|
"unescape",
|
||||||
|
"decodeURI",
|
||||||
|
"decodeURIComponent",
|
||||||
|
"encodeURI",
|
||||||
|
"encodeURIComponent",
|
||||||
|
"console",
|
||||||
|
])
|
||||||
112
src/index.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import {COMMANDS, get_initial_state} from './cmd.js'
|
||||||
|
import {active_frame} from './calltree.js'
|
||||||
|
import {UI} from './editor/ui.js'
|
||||||
|
import {EFFECTS, render_initial_state, render_common_side_effects} from './effects.js'
|
||||||
|
import {load_dir} from './filesystem.js'
|
||||||
|
|
||||||
|
const EXAMPLE = `const fib = n =>
|
||||||
|
n == 0 || n == 1
|
||||||
|
? n
|
||||||
|
: fib(n - 1) + fib(n - 2)
|
||||||
|
fib(6)`
|
||||||
|
|
||||||
|
const read_modules = async () => {
|
||||||
|
const default_module = {'': localStorage.code || EXAMPLE}
|
||||||
|
const current = {
|
||||||
|
// TODO fix when there are no such modules anymore
|
||||||
|
current_module: localStorage.current_module ?? '',
|
||||||
|
entrypoint: localStorage.entrypoint ?? '',
|
||||||
|
}
|
||||||
|
const project_dir = await load_dir(false)
|
||||||
|
if(project_dir == null) {
|
||||||
|
// Single anonymous module
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
files: default_module,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...current,
|
||||||
|
project_dir,
|
||||||
|
files: default_module,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let ui
|
||||||
|
let state
|
||||||
|
|
||||||
|
export const init = (container) => {
|
||||||
|
// TODO err.message
|
||||||
|
window.onerror = (msg, src, lineNum, colNum, err) => {
|
||||||
|
ui.set_status(msg)
|
||||||
|
}
|
||||||
|
window.addEventListener('unhandledrejection', (event) => {
|
||||||
|
ui.set_status(event.reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
read_modules().then(initial_state => {
|
||||||
|
state = get_initial_state(initial_state)
|
||||||
|
// Expose state for debugging
|
||||||
|
globalThis.__state = state
|
||||||
|
ui = new UI(container, state)
|
||||||
|
// Expose for debugging
|
||||||
|
globalThis.__ui = ui
|
||||||
|
render_initial_state(ui, state)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const get_state = () => state
|
||||||
|
|
||||||
|
export const exec = (cmd, ...args) => {
|
||||||
|
if(cmd == 'input' || cmd == 'write') {
|
||||||
|
// Do not print file to console
|
||||||
|
console.log('exec', cmd)
|
||||||
|
} else {
|
||||||
|
console.log('exec', cmd, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
const comm = cmd.split('.').reduce(
|
||||||
|
(comm, segment) => comm?.[segment],
|
||||||
|
COMMANDS
|
||||||
|
)
|
||||||
|
if(comm == null) {
|
||||||
|
throw new Error('command ' + cmd + ' + not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = comm(state, ...args)
|
||||||
|
console.log('nextstate', result)
|
||||||
|
|
||||||
|
let nextstate, effects
|
||||||
|
if(result.state != null) {
|
||||||
|
({state: nextstate, effects} = result)
|
||||||
|
} else {
|
||||||
|
nextstate = result
|
||||||
|
effects = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
if(state?.parse_result == null) {
|
||||||
|
console.error('command did not return state, returned', result)
|
||||||
|
throw new Error('illegal state')
|
||||||
|
}
|
||||||
|
|
||||||
|
render_common_side_effects(state, nextstate, cmd, ui);
|
||||||
|
|
||||||
|
if(effects != null) {
|
||||||
|
(Array.isArray(effects) ? effects : [effects]).forEach(e => {
|
||||||
|
if(e.type == 'write' || e.type == 'save_to_localstorage') {
|
||||||
|
// do not spam to console
|
||||||
|
console.log('apply effect', e.type)
|
||||||
|
} else {
|
||||||
|
console.log('apply effect', e.type, ...(e.args ?? []))
|
||||||
|
}
|
||||||
|
EFFECTS[e.type](nextstate, e.args, ui)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose for debugging
|
||||||
|
globalThis.__prev_state = state
|
||||||
|
globalThis.__state = nextstate
|
||||||
|
state = nextstate
|
||||||
|
}
|
||||||
1633
src/parse_js.js
Normal file
48
src/reserved.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#keywords
|
||||||
|
|
||||||
|
export const reserved = [
|
||||||
|
'break',
|
||||||
|
'case',
|
||||||
|
'catch',
|
||||||
|
'class',
|
||||||
|
'const',
|
||||||
|
'continue',
|
||||||
|
'debugger',
|
||||||
|
'default',
|
||||||
|
'delete',
|
||||||
|
'do',
|
||||||
|
'else',
|
||||||
|
'export',
|
||||||
|
'extends',
|
||||||
|
'finally',
|
||||||
|
'for',
|
||||||
|
'function',
|
||||||
|
'if',
|
||||||
|
'import',
|
||||||
|
'in',
|
||||||
|
'instanceof',
|
||||||
|
'new',
|
||||||
|
'return',
|
||||||
|
'super',
|
||||||
|
'switch',
|
||||||
|
'this',
|
||||||
|
'throw',
|
||||||
|
'try',
|
||||||
|
'typeof',
|
||||||
|
'var',
|
||||||
|
'void',
|
||||||
|
'while',
|
||||||
|
'with',
|
||||||
|
'yield',
|
||||||
|
'enum',
|
||||||
|
'implements',
|
||||||
|
'interface',
|
||||||
|
'let',
|
||||||
|
'package',
|
||||||
|
'private',
|
||||||
|
'protected',
|
||||||
|
'public',
|
||||||
|
'static',
|
||||||
|
'yield',
|
||||||
|
'await',
|
||||||
|
]
|
||||||
95
src/utils.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
export const findLast = new Function('arr', 'pred', `
|
||||||
|
for(let i = arr.length - 1; i >= 0; i--) {
|
||||||
|
if(pred(arr[i])) {
|
||||||
|
return arr[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
export const set_push = (x,y) => new Set([...x, y])
|
||||||
|
|
||||||
|
export const set_union = (x,y) => new Set([...x, ...y])
|
||||||
|
|
||||||
|
export const set_diff = (x,y) => {
|
||||||
|
return new Set([...x].filter(el => !y.has(el)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const map_object = (obj, mapper) => Object.fromEntries(
|
||||||
|
Object.entries(obj).map(([k, v]) => [k, mapper(k,v)])
|
||||||
|
)
|
||||||
|
|
||||||
|
export const filter_object = (obj, pred) => Object.fromEntries(
|
||||||
|
Object.entries(obj).filter(([k, v]) => pred(k,v))
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://bit.cloud/ramda/ramda/map-accum/~code
|
||||||
|
export const map_accum = new Function('fn', 'acc', 'arr', `
|
||||||
|
let idx = 0;
|
||||||
|
const len = arr.length;
|
||||||
|
const result = [];
|
||||||
|
let tuple = [acc];
|
||||||
|
while (idx < len) {
|
||||||
|
tuple = fn(tuple[0], arr[idx], idx);
|
||||||
|
result[idx] = tuple[1];
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
return [tuple[0], result];
|
||||||
|
`)
|
||||||
|
|
||||||
|
export const map_find = (arr, mapper) => arr.reduce(
|
||||||
|
(result, curr, i) => result ?? mapper(curr, i),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
export const pick_keys = (obj, keys) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(obj).filter(([k,v]) => keys.includes(k))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stringify = val => JSON.stringify(val, null, 2)
|
||||||
|
|
||||||
|
export const zip = (x,y) => {
|
||||||
|
if(x.length != y.length){
|
||||||
|
throw new Error('zipped arrays must have same length')
|
||||||
|
} else {
|
||||||
|
return x.map((el, i) => [el, y[i]])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uniq = arr => [...new Set(arr)]
|
||||||
|
|
||||||
|
// TODO remove
|
||||||
|
/*
|
||||||
|
function object_diff(a,b){
|
||||||
|
function do_object_diff(a,b, context=[]) {
|
||||||
|
if(a == b){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(a == null && b == null){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(typeof(a) != 'object' || typeof(b) != 'object'){
|
||||||
|
throw new Error(`not an object ${a} ${b}`)
|
||||||
|
}
|
||||||
|
for(let key in a) {
|
||||||
|
if(b[key] == null) {
|
||||||
|
throw new Error(`missing ${key} in right object ${context.join('.')}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(let key in b) {
|
||||||
|
if(a[key] == null) {
|
||||||
|
throw new Error(`missing ${key} in left object ${context.join('.')}`)
|
||||||
|
}
|
||||||
|
do_object_diff(a[key], b[key], context.concat([key]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
do_object_diff(a,b)
|
||||||
|
} catch(e){
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
4
test/run.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import {tests} from './test.js'
|
||||||
|
import {run} from './utils.js'
|
||||||
|
|
||||||
|
run(tests)
|
||||||
82
test/self_hosted_test.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
import {load_modules} from '../src/parse_js.js'
|
||||||
|
import {eval_modules, eval_frame} from '../src/eval.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
assert_equal,
|
||||||
|
run,
|
||||||
|
stringify,
|
||||||
|
test,
|
||||||
|
} from './utils.js'
|
||||||
|
|
||||||
|
const entry = `
|
||||||
|
import {parse, load_modules} from './src/parse_js.js';
|
||||||
|
|
||||||
|
import {get_initial_state} from './src/cmd.js';
|
||||||
|
//console.time('p');
|
||||||
|
//const parsed = parse(globalThis.module_cache['./src/parse_js.js']);
|
||||||
|
//console.timeEnd('p');
|
||||||
|
//const parsed = parse('1');
|
||||||
|
|
||||||
|
const loader = module => globalThis.module_cache[module];
|
||||||
|
console.time('p2');
|
||||||
|
load_modules('src/parse_js.js', (m) => {
|
||||||
|
return loader(m)
|
||||||
|
|
||||||
|
});
|
||||||
|
console.timeEnd('p2')
|
||||||
|
//import {} from './test/test.js'
|
||||||
|
`
|
||||||
|
|
||||||
|
globalThis.module_cache = {}
|
||||||
|
|
||||||
|
const load_module = (dir, module) => {
|
||||||
|
return (globalThis.module_cache[module] = fs.readFileSync(dir + module, 'utf8'))
|
||||||
|
}
|
||||||
|
const loader = module => {
|
||||||
|
return module == ''
|
||||||
|
? entry
|
||||||
|
: load_module('./', module)
|
||||||
|
}
|
||||||
|
|
||||||
|
run([
|
||||||
|
test('self-hosted', () => {
|
||||||
|
//console.time('p0')
|
||||||
|
const parsed = load_modules('', loader)
|
||||||
|
//log('cache', Object.keys(globalThis.module_cache))
|
||||||
|
//console.log('p', parsed)
|
||||||
|
//console.timeEnd('p0')
|
||||||
|
if(!parsed.ok) {
|
||||||
|
const p = parsed.problems[0]
|
||||||
|
console.error('FAIL', p.index, p.message, p.module)
|
||||||
|
console.log(loader(p.module).slice(p.index, p.index + 100))
|
||||||
|
} else {
|
||||||
|
assert_equal(parsed.ok, true)
|
||||||
|
console.time('eval')
|
||||||
|
const result = eval_modules(parsed.modules, parsed.sorted).calltree
|
||||||
|
console.timeEnd('eval')
|
||||||
|
|
||||||
|
/* TODO remove
|
||||||
|
|
||||||
|
const count_nodes = node => node.children == null
|
||||||
|
? 1
|
||||||
|
: 1 + node.children.reduce(
|
||||||
|
(total, c) => total + count_nodes(c),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
Object.entries(result)
|
||||||
|
.map(([k,v]) => count_nodes(v.calls))
|
||||||
|
.reduce((total, c) => total +c)
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
///const frame = eval_frame(result[''].calls, result)
|
||||||
|
///log('f', frame.children[frame.children.length - 1])
|
||||||
|
///assert_equal(
|
||||||
|
/// frame.children[frame.children.length - 1].result.value.value,
|
||||||
|
/// 1
|
||||||
|
///)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
])
|
||||||
1998
test/test.js
Normal file
141
test/utils.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import {parse, print_debug_node, load_modules} from '../src/parse_js.js'
|
||||||
|
import {eval_tree, eval_frame} from '../src/eval.js'
|
||||||
|
import {get_initial_state} from '../src/cmd.js'
|
||||||
|
|
||||||
|
Object.assign(globalThis, {log: console.log})
|
||||||
|
|
||||||
|
export const parse_modules = (entry, modules) =>
|
||||||
|
load_modules(entry, module_name => modules[module_name])
|
||||||
|
|
||||||
|
export const assert_code_evals_to = (codestring, expected) => {
|
||||||
|
const parse_result = parse(codestring)
|
||||||
|
assert_equal(parse_result.ok, true)
|
||||||
|
const tree = eval_tree(parse_result.node)
|
||||||
|
const frame = eval_frame(tree)
|
||||||
|
const result = frame.children[frame.children.length - 1].result
|
||||||
|
assert_equal({ok: result.ok, value: result.value}, {ok: true, value: expected})
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
|
||||||
|
export const assert_code_error = (codestring, error) => {
|
||||||
|
const parse_result = parse(codestring)
|
||||||
|
assert_equal(parse_result.ok, true)
|
||||||
|
const tree = eval_tree(parse_result.node)
|
||||||
|
const frame = eval_frame(tree)
|
||||||
|
const result = frame.children[frame.children.length - 1].result
|
||||||
|
assert_equal(result.ok, false)
|
||||||
|
assert_equal(result.error, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const test_initial_state = code => {
|
||||||
|
return get_initial_state({
|
||||||
|
files: typeof(code) == 'object' ? code : { '' : code},
|
||||||
|
entrypoint: '',
|
||||||
|
current_module: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stringify = val =>
|
||||||
|
JSON.stringify(val, (key, value) => {
|
||||||
|
// TODO do not use instanceof because currently not implemented in parser
|
||||||
|
if(value?.constructor == Set){
|
||||||
|
return [...value]
|
||||||
|
} else {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}, 2)
|
||||||
|
|
||||||
|
export const assert_equal = (exp, actual) => {
|
||||||
|
if(typeof(exp) == 'object' && typeof(actual) == 'object'){
|
||||||
|
const exp_json = stringify(exp)
|
||||||
|
const act_json = stringify(actual)
|
||||||
|
if(exp_json != act_json){
|
||||||
|
throw new Error(`FAIL: ${exp_json} != ${act_json}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(exp != actual){
|
||||||
|
throw new Error(`FAIL: ${exp} != ${actual}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const print_debug_ct_node = node => {
|
||||||
|
const do_print = node => {
|
||||||
|
const {id, fn, ok, value, error, args, has_more_children} = node
|
||||||
|
const res = {id, fn: fn?.name, ok, value, error, args, has_more_children}
|
||||||
|
if(node.children == null) {
|
||||||
|
return res
|
||||||
|
} else {
|
||||||
|
const next_children = node.children.map(do_print)
|
||||||
|
return {...res, children: next_children}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stringify(do_print(node))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const test = (message, test, only = false) => {
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
test: Object.defineProperty(test, 'name', {value: message}),
|
||||||
|
only,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const test_only = (message, t) => test(message, t, true)
|
||||||
|
|
||||||
|
// Wrap to Function constructor to hide from calltree view
|
||||||
|
// TODO in calltree view, hide fn which has special flag set (see
|
||||||
|
// filter_calltree)
|
||||||
|
export const run = Object.defineProperty(new Function('tests', `
|
||||||
|
const run_test = t => {
|
||||||
|
try {
|
||||||
|
t.test()
|
||||||
|
} catch(e) {
|
||||||
|
if(globalThis.process != null) {
|
||||||
|
// In node.js runner, fail fast
|
||||||
|
console.error('Failed: ' + t.message)
|
||||||
|
throw e
|
||||||
|
} else {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not run in node, then dont apply filter
|
||||||
|
const filter = globalThis.process && globalThis.process.argv[2]
|
||||||
|
|
||||||
|
if(filter == null) {
|
||||||
|
|
||||||
|
const only = tests.find(t => t.only)
|
||||||
|
const tests_to_run = only == null ? tests : [only]
|
||||||
|
|
||||||
|
// Exec each test. After all tests are done, we rethrow first failer if
|
||||||
|
// any. So we will mark root calltree node if one of tests failed
|
||||||
|
const failure = tests_to_run.reduce(
|
||||||
|
(failure, t) => {
|
||||||
|
const next_failure = run_test(t)
|
||||||
|
return failure ?? next_failure
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
if(failure != null) {
|
||||||
|
throw failure
|
||||||
|
} else {
|
||||||
|
if(globalThis.process != null) {
|
||||||
|
console.log('Ok')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const test = tests.find(t => t.message.includes(filter))
|
||||||
|
if(test == null) {
|
||||||
|
throw new Error('test not found')
|
||||||
|
} else {
|
||||||
|
run_test(test)
|
||||||
|
if(globalThis.process != null) {
|
||||||
|
console.log('Ok')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`), 'name', {value: 'run'})
|
||||||