mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 20:26:08 +00:00
leverage Fig's shell parser, add git spec (#240001)
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com> Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
This commit is contained in:
@@ -12,7 +12,8 @@
|
||||
**/extensions/markdown-math/notebook-out/**
|
||||
**/extensions/notebook-renderers/renderer-out/index.js
|
||||
**/extensions/simple-browser/media/index.js
|
||||
**/extensions/terminal-suggest/src/completions/**
|
||||
**/extensions/terminal-suggest/src/completions/upstream/**
|
||||
**/extensions/terminal-suggest/third_party/**
|
||||
**/extensions/typescript-language-features/test-workspace/**
|
||||
**/extensions/typescript-language-features/extension.webpack.config.js
|
||||
**/extensions/typescript-language-features/extension-browser.webpack.config.js
|
||||
|
||||
@@ -14,6 +14,75 @@
|
||||
"url": "https://github.com/withfig/autocomplete/blob/main/LICENSE.md"
|
||||
},
|
||||
"description": "IDE-style autocomplete for your existing terminal & shell from withfig/autocomplete."
|
||||
},
|
||||
{
|
||||
"component": {
|
||||
"type": "git",
|
||||
"git": {
|
||||
"name": "amazon-q-developer-cli",
|
||||
"repositoryUrl": "https://github.com/aws/amazon-q-developer-cli",
|
||||
"commitHash": "f66e0b0e917ab185eef528dc36eca56b78ca8b5d"
|
||||
}
|
||||
},
|
||||
"licenseDetail": [
|
||||
"MIT License",
|
||||
"",
|
||||
"Copyright (c) 2024 Amazon.com, Inc. or its affiliates.",
|
||||
"",
|
||||
"Permission is hereby granted, free of charge, to any person obtaining a copy",
|
||||
"of this software and associated documentation files (the \"Software\"), to deal",
|
||||
"in the Software without restriction, including without limitation the rights",
|
||||
"to use, copy, modify, merge, publish, distribute, sublicense, and/or sell",
|
||||
"copies of the Software, and to permit persons to whom the Software is",
|
||||
"furnished to do so, subject to the following conditions:",
|
||||
"",
|
||||
"The above copyright notice and this permission notice shall be included in all",
|
||||
"copies or substantial portions of the Software.",
|
||||
"",
|
||||
"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR",
|
||||
"IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,",
|
||||
"FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE",
|
||||
"AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER",
|
||||
"LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,",
|
||||
"OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE",
|
||||
"SOFTWARE."
|
||||
],
|
||||
"version": "f66e0b0e917ab185eef528dc36eca56b78ca8b5d"
|
||||
},
|
||||
{
|
||||
"component": {
|
||||
"type": "git",
|
||||
"git": {
|
||||
"name": "@fig/autocomplete-shared",
|
||||
"repositoryUrl": "https://github.com/withfig/autocomplete-tools/blob/main/shared",
|
||||
"commitHash": "104377c19a91ca8a312cb38c115a74468f6227cb"
|
||||
}
|
||||
},
|
||||
"licenseDetail": [
|
||||
"MIT License",
|
||||
"",
|
||||
"Copyright (c) 2021 Hercules Labs Inc. (Fig)",
|
||||
"",
|
||||
"Permission is hereby granted, free of charge, to any person obtaining a copy",
|
||||
"of this software and associated documentation files (the \"Software\"), to deal",
|
||||
"in the Software without restriction, including without limitation the rights",
|
||||
"to use, copy, modify, merge, publish, distribute, sublicense, and/or sell",
|
||||
"copies of the Software, and to permit persons to whom the Software is",
|
||||
"furnished to do so, subject to the following conditions:",
|
||||
"",
|
||||
"The above copyright notice and this permission notice shall be included in all",
|
||||
"copies or substantial portions of the Software.",
|
||||
"",
|
||||
"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR",
|
||||
"IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,",
|
||||
"FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE",
|
||||
"AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER",
|
||||
"LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,",
|
||||
"OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE",
|
||||
"SOFTWARE."
|
||||
],
|
||||
"version": "1.1.2"
|
||||
}
|
||||
]
|
||||
],
|
||||
"version": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
### Case 1
|
||||
a b\\ c
|
||||
|
||||
### Case 2
|
||||
a "b"
|
||||
|
||||
### Case 3
|
||||
a 'b'
|
||||
|
||||
### Case 4
|
||||
a $'b'
|
||||
|
||||
### Case 5
|
||||
a $commit
|
||||
|
||||
### Case 6
|
||||
a $$
|
||||
|
||||
### Case 7
|
||||
a $((b))
|
||||
|
||||
### Case 8
|
||||
a $(b)
|
||||
|
||||
### Case 9
|
||||
a \`b\`
|
||||
|
||||
### Case 10
|
||||
a $(\`b\`)
|
||||
@@ -0,0 +1,448 @@
|
||||
// Case 1
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 7,
|
||||
"text": "a b\\\\ c",
|
||||
"innerText": "a b\\\\ c",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 7,
|
||||
"text": "a b\\\\ c",
|
||||
"innerText": "a b\\\\ c",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "word",
|
||||
"endIndex": 5,
|
||||
"text": "b\\\\",
|
||||
"innerText": "b\\",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 6,
|
||||
"type": "word",
|
||||
"endIndex": 7,
|
||||
"text": "c",
|
||||
"innerText": "c",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 2
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 5,
|
||||
"text": "a \"b\"",
|
||||
"innerText": "a \"b\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 5,
|
||||
"text": "a \"b\"",
|
||||
"innerText": "a \"b\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "string",
|
||||
"endIndex": 5,
|
||||
"text": "\"b\"",
|
||||
"innerText": "b",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 3
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 5,
|
||||
"text": "a 'b'",
|
||||
"innerText": "a 'b'",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 5,
|
||||
"text": "a 'b'",
|
||||
"innerText": "a 'b'",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "raw_string",
|
||||
"endIndex": 5,
|
||||
"text": "'b'",
|
||||
"innerText": "b",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 4
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 6,
|
||||
"text": "a $'b'",
|
||||
"innerText": "a $'b'",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 6,
|
||||
"text": "a $'b'",
|
||||
"innerText": "a $'b'",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "ansi_c_string",
|
||||
"endIndex": 6,
|
||||
"text": "$'b'",
|
||||
"innerText": "b",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 5
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 9,
|
||||
"text": "a $commit",
|
||||
"innerText": "a $commit",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 9,
|
||||
"text": "a $commit",
|
||||
"innerText": "a $commit",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "simple_expansion",
|
||||
"endIndex": 9,
|
||||
"text": "$commit",
|
||||
"innerText": "$commit",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 6
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 4,
|
||||
"text": "a $$",
|
||||
"innerText": "a $$",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 4,
|
||||
"text": "a $$",
|
||||
"innerText": "a $$",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "special_expansion",
|
||||
"endIndex": 4,
|
||||
"text": "$$",
|
||||
"innerText": "$$",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 7
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 8,
|
||||
"text": "a $((b))",
|
||||
"innerText": "a $((b))",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 8,
|
||||
"text": "a $((b))",
|
||||
"innerText": "a $((b))",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "arithmetic_expansion",
|
||||
"endIndex": 8,
|
||||
"text": "$((b))",
|
||||
"innerText": "$((b))",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 8
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 6,
|
||||
"text": "a $(b)",
|
||||
"innerText": "a $(b)",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 6,
|
||||
"text": "a $(b)",
|
||||
"innerText": "a $(b)",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "command_substitution",
|
||||
"endIndex": 6,
|
||||
"text": "$(b)",
|
||||
"innerText": "$(b)",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 4,
|
||||
"type": "command",
|
||||
"endIndex": 5,
|
||||
"text": "b",
|
||||
"innerText": "b",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 4,
|
||||
"type": "word",
|
||||
"endIndex": 5,
|
||||
"text": "b",
|
||||
"innerText": "b",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 9
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 7,
|
||||
"text": "a \\`b\\`",
|
||||
"innerText": "a \\`b\\`",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 7,
|
||||
"text": "a \\`b\\`",
|
||||
"innerText": "a \\`b\\`",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 3,
|
||||
"type": "word",
|
||||
"endIndex": 7,
|
||||
"text": "`b\\`",
|
||||
"innerText": "`b`",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 10
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 10,
|
||||
"text": "a $(\\`b\\`)",
|
||||
"innerText": "a $(\\`b\\`)",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 10,
|
||||
"text": "a $(\\`b\\`)",
|
||||
"innerText": "a $(\\`b\\`)",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "command_substitution",
|
||||
"endIndex": 10,
|
||||
"text": "$(\\`b\\`)",
|
||||
"innerText": "$(\\`b\\`)",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 4,
|
||||
"type": "command",
|
||||
"endIndex": 9,
|
||||
"text": "\\`b\\`",
|
||||
"innerText": "\\`b\\`",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 5,
|
||||
"type": "word",
|
||||
"endIndex": 9,
|
||||
"text": "`b\\`",
|
||||
"innerText": "`b`",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
### Case 1
|
||||
a && b
|
||||
|
||||
### Case 2
|
||||
a || b
|
||||
|
||||
### Case 3
|
||||
a | b
|
||||
|
||||
### Case 4
|
||||
a |& b
|
||||
|
||||
### Case 5
|
||||
(a; b)
|
||||
|
||||
### Case 6
|
||||
(a; b;)
|
||||
|
||||
### Case 7
|
||||
{a; b}
|
||||
|
||||
### Case 8
|
||||
{a; b;}
|
||||
|
||||
### Case 9
|
||||
a; b
|
||||
|
||||
### Case 10
|
||||
a & b
|
||||
|
||||
### Case 11
|
||||
a &; b
|
||||
|
||||
### Case 12
|
||||
a ; b;
|
||||
|
||||
### Case 13
|
||||
a && b || c
|
||||
|
||||
### Case 14
|
||||
a && b | c
|
||||
|
||||
### Case 15
|
||||
a | b && c
|
||||
|
||||
### Case 16
|
||||
(a) | b && c
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
### Case 1
|
||||
a "\${b}"
|
||||
|
||||
### Case 2
|
||||
a "'b'"
|
||||
|
||||
### Case 3
|
||||
a "\${b:+"c"}"
|
||||
|
||||
### Case 4
|
||||
a b"c"
|
||||
|
||||
### Case 5
|
||||
a '\${b}'
|
||||
|
||||
### Case 6
|
||||
a $'\${b}'
|
||||
|
||||
### Case 7
|
||||
a $'b''c'd$$$e\${f}"g"
|
||||
|
||||
### Case 8
|
||||
a $'b\\'c'
|
||||
|
||||
### Case 9
|
||||
a 'b\\'c'
|
||||
|
||||
### Case 10
|
||||
a "b$"
|
||||
|
||||
### Case 11
|
||||
a "$b"
|
||||
|
||||
### Case 12
|
||||
a "$(b "c" && d)"
|
||||
@@ -0,0 +1,724 @@
|
||||
// Case 1
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 9,
|
||||
"text": "a \"\\${b}\"",
|
||||
"innerText": "a \"\\${b}\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 9,
|
||||
"text": "a \"\\${b}\"",
|
||||
"innerText": "a \"\\${b}\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "string",
|
||||
"endIndex": 9,
|
||||
"text": "\"\\${b}\"",
|
||||
"innerText": "${b}",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 2
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 7,
|
||||
"text": "a \"'b'\"",
|
||||
"innerText": "a \"'b'\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 7,
|
||||
"text": "a \"'b'\"",
|
||||
"innerText": "a \"'b'\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "string",
|
||||
"endIndex": 7,
|
||||
"text": "\"'b'\"",
|
||||
"innerText": "'b'",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 3
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 14,
|
||||
"text": "a \"\\${b:+\"c\"}\"",
|
||||
"innerText": "a \"\\${b:+\"c\"}\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 14,
|
||||
"text": "a \"\\${b:+\"c\"}\"",
|
||||
"innerText": "a \"\\${b:+\"c\"}\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "concatenation",
|
||||
"endIndex": 14,
|
||||
"text": "\"\\${b:+\"c\"}\"",
|
||||
"innerText": "${b:+c}",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "string",
|
||||
"endIndex": 10,
|
||||
"text": "\"\\${b:+\"",
|
||||
"innerText": "${b:+",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 10,
|
||||
"type": "word",
|
||||
"endIndex": 11,
|
||||
"text": "c",
|
||||
"innerText": "c",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 11,
|
||||
"type": "string",
|
||||
"endIndex": 14,
|
||||
"text": "\"}\"",
|
||||
"innerText": "}",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 4
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 6,
|
||||
"text": "a b\"c\"",
|
||||
"innerText": "a b\"c\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 6,
|
||||
"text": "a b\"c\"",
|
||||
"innerText": "a b\"c\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "concatenation",
|
||||
"endIndex": 6,
|
||||
"text": "b\"c\"",
|
||||
"innerText": "bc",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "word",
|
||||
"endIndex": 3,
|
||||
"text": "b",
|
||||
"innerText": "b",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 3,
|
||||
"type": "string",
|
||||
"endIndex": 6,
|
||||
"text": "\"c\"",
|
||||
"innerText": "c",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 5
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 9,
|
||||
"text": "a '\\${b}'",
|
||||
"innerText": "a '\\${b}'",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 9,
|
||||
"text": "a '\\${b}'",
|
||||
"innerText": "a '\\${b}'",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "raw_string",
|
||||
"endIndex": 9,
|
||||
"text": "'\\${b}'",
|
||||
"innerText": "\\${b}",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 6
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 10,
|
||||
"text": "a $'\\${b}'",
|
||||
"innerText": "a $'\\${b}'",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 10,
|
||||
"text": "a $'\\${b}'",
|
||||
"innerText": "a $'\\${b}'",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "ansi_c_string",
|
||||
"endIndex": 10,
|
||||
"text": "$'\\${b}'",
|
||||
"innerText": "\\${b}",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 7
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 22,
|
||||
"text": "a $'b''c'd$$$e\\${f}\"g\"",
|
||||
"innerText": "a $'b''c'd$$$e\\${f}\"g\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 22,
|
||||
"text": "a $'b''c'd$$$e\\${f}\"g\"",
|
||||
"innerText": "a $'b''c'd$$$e\\${f}\"g\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "concatenation",
|
||||
"endIndex": 22,
|
||||
"text": "$'b''c'd$$$e\\${f}\"g\"",
|
||||
"innerText": "bcd$$$e${f}g",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "ansi_c_string",
|
||||
"endIndex": 6,
|
||||
"text": "$'b'",
|
||||
"innerText": "b",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 6,
|
||||
"type": "raw_string",
|
||||
"endIndex": 9,
|
||||
"text": "'c'",
|
||||
"innerText": "c",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 9,
|
||||
"type": "word",
|
||||
"endIndex": 10,
|
||||
"text": "d",
|
||||
"innerText": "d",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 10,
|
||||
"type": "special_expansion",
|
||||
"endIndex": 12,
|
||||
"text": "$$",
|
||||
"innerText": "$$",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 12,
|
||||
"type": "simple_expansion",
|
||||
"endIndex": 14,
|
||||
"text": "$e",
|
||||
"innerText": "$e",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 15,
|
||||
"type": "word",
|
||||
"endIndex": 19,
|
||||
"text": "${f}",
|
||||
"innerText": "${f}",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 19,
|
||||
"type": "string",
|
||||
"endIndex": 22,
|
||||
"text": "\"g\"",
|
||||
"innerText": "g",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 8
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 10,
|
||||
"text": "a $'b\\\\'c'",
|
||||
"innerText": "a $'b\\\\'c'",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 10,
|
||||
"text": "a $'b\\\\'c'",
|
||||
"innerText": "a $'b\\\\'c'",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "concatenation",
|
||||
"endIndex": 10,
|
||||
"text": "$'b\\\\'c'",
|
||||
"innerText": "b\\\\c",
|
||||
"complete": false,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "ansi_c_string",
|
||||
"endIndex": 8,
|
||||
"text": "$'b\\\\'",
|
||||
"innerText": "b\\\\",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 8,
|
||||
"type": "word",
|
||||
"endIndex": 9,
|
||||
"text": "c",
|
||||
"innerText": "c",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 9,
|
||||
"type": "raw_string",
|
||||
"endIndex": 10,
|
||||
"text": "'",
|
||||
"innerText": "",
|
||||
"complete": false,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 9
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 9,
|
||||
"text": "a 'b\\\\'c'",
|
||||
"innerText": "a 'b\\\\'c'",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 9,
|
||||
"text": "a 'b\\\\'c'",
|
||||
"innerText": "a 'b\\\\'c'",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "concatenation",
|
||||
"endIndex": 9,
|
||||
"text": "'b\\\\'c'",
|
||||
"innerText": "b\\\\c",
|
||||
"complete": false,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "raw_string",
|
||||
"endIndex": 7,
|
||||
"text": "'b\\\\'",
|
||||
"innerText": "b\\\\",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 7,
|
||||
"type": "word",
|
||||
"endIndex": 8,
|
||||
"text": "c",
|
||||
"innerText": "c",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 8,
|
||||
"type": "raw_string",
|
||||
"endIndex": 9,
|
||||
"text": "'",
|
||||
"innerText": "",
|
||||
"complete": false,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 10
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 6,
|
||||
"text": "a \"b$\"",
|
||||
"innerText": "a \"b$\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 6,
|
||||
"text": "a \"b$\"",
|
||||
"innerText": "a \"b$\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "string",
|
||||
"endIndex": 6,
|
||||
"text": "\"b$\"",
|
||||
"innerText": "b$",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 11
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 6,
|
||||
"text": "a \"$b\"",
|
||||
"innerText": "a \"$b\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 6,
|
||||
"text": "a \"$b\"",
|
||||
"innerText": "a \"$b\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "string",
|
||||
"endIndex": 6,
|
||||
"text": "\"$b\"",
|
||||
"innerText": "$b",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 3,
|
||||
"type": "simple_expansion",
|
||||
"endIndex": 5,
|
||||
"text": "$b",
|
||||
"innerText": "$b",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Case 12
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "program",
|
||||
"endIndex": 17,
|
||||
"text": "a \"$(b \"c\" && d)\"",
|
||||
"innerText": "a \"$(b \"c\" && d)\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "command",
|
||||
"endIndex": 17,
|
||||
"text": "a \"$(b \"c\" && d)\"",
|
||||
"innerText": "a \"$(b \"c\" && d)\"",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 0,
|
||||
"type": "word",
|
||||
"endIndex": 1,
|
||||
"text": "a",
|
||||
"innerText": "a",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 2,
|
||||
"type": "string",
|
||||
"endIndex": 17,
|
||||
"text": "\"$(b \"c\" && d)\"",
|
||||
"innerText": "$(b \"c\" && d)",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 3,
|
||||
"type": "command_substitution",
|
||||
"endIndex": 16,
|
||||
"text": "$(b \"c\" && d)",
|
||||
"innerText": "$(b \"c\" && d)",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 5,
|
||||
"type": "list",
|
||||
"endIndex": 15,
|
||||
"text": "b \"c\" && d",
|
||||
"innerText": "b \"c\" && d",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 5,
|
||||
"type": "command",
|
||||
"endIndex": 11,
|
||||
"text": "b \"c\" ",
|
||||
"innerText": "b \"c\" ",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 5,
|
||||
"type": "word",
|
||||
"endIndex": 6,
|
||||
"text": "b",
|
||||
"innerText": "b",
|
||||
"complete": true,
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"startIndex": 7,
|
||||
"type": "string",
|
||||
"endIndex": 10,
|
||||
"text": "\"c\"",
|
||||
"innerText": "c",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"startIndex": 14,
|
||||
"type": "command",
|
||||
"endIndex": 15,
|
||||
"text": "d",
|
||||
"innerText": "d",
|
||||
"complete": true,
|
||||
"children": [
|
||||
{
|
||||
"startIndex": 14,
|
||||
"type": "word",
|
||||
"endIndex": 15,
|
||||
"text": "d",
|
||||
"innerText": "d",
|
||||
"complete": true,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
### Case 1
|
||||
ENV=a b
|
||||
|
||||
### Case 2
|
||||
ENV=a b c d --op=e
|
||||
|
||||
### Case 3
|
||||
ENV=a ENV=b a
|
||||
|
||||
### Case 4
|
||||
ENV=a ENV=b a && ENV=c c
|
||||
|
||||
### Case 5
|
||||
ENV="a b" c
|
||||
|
||||
### Case 6
|
||||
ENV='a b' c
|
||||
|
||||
### Case 7
|
||||
ENV=`cmd` a
|
||||
|
||||
### Case 8
|
||||
ENV+='100' b
|
||||
|
||||
### Case 9
|
||||
ENV+=a ENV=b
|
||||
|
||||
### Case 10
|
||||
ENV+=a ENV=b && foo
|
||||
|
||||
### Case 11
|
||||
ENV="a
|
||||
|
||||
### Case 12
|
||||
ENV='a
|
||||
|
||||
### Case 13
|
||||
ENV=a ENV=`b
|
||||
|
||||
### Case 14
|
||||
ENV=`ENV="a" b` && ENV="c" d
|
||||
|
||||
### Case 15
|
||||
c $(ENV=a foo)
|
||||
|
||||
### Case 16
|
||||
ENV=a; b
|
||||
|
||||
### Case 17
|
||||
ENV=a ; b
|
||||
|
||||
### Case 18
|
||||
ENV=a & b
|
||||
|
||||
### Case 19
|
||||
ENV=a|b
|
||||
|
||||
### Case 20
|
||||
ENV[0]=a b
|
||||
|
||||
### Case 21
|
||||
ENV[0]=a; b
|
||||
|
||||
### Case 22
|
||||
ENV[1]=`a b
|
||||
|
||||
### Case 23
|
||||
ENV[2]+="a b "
|
||||
|
||||
### Case 24
|
||||
MY_VAR='echo'hi$'quote'"command: $(ps | VAR=2 grep ps)"
|
||||
|
||||
### Case 25
|
||||
ENV="a"'b'c d
|
||||
|
||||
### Case 26
|
||||
ENV=a"b"'c'
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,6 @@
|
||||
"compile": "npx gulp compile-extension:terminal-suggest",
|
||||
"watch": "npx gulp watch-extension:terminal-suggest"
|
||||
},
|
||||
|
||||
"main": "./out/terminalSuggestMain",
|
||||
"activationEvents": [
|
||||
"onTerminalCompletionsRequested"
|
||||
|
||||
2486
extensions/terminal-suggest/src/completions/index.d.ts
vendored
2486
extensions/terminal-suggest/src/completions/index.d.ts
vendored
File diff suppressed because it is too large
Load Diff
9813
extensions/terminal-suggest/src/completions/upstream/git.ts
Normal file
9813
extensions/terminal-suggest/src/completions/upstream/git.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ export const upstreamSpecs = [
|
||||
'rm',
|
||||
'rmdir',
|
||||
'touch',
|
||||
'git'
|
||||
];
|
||||
|
||||
|
||||
|
||||
5
extensions/terminal-suggest/src/fig/README.md
Normal file
5
extensions/terminal-suggest/src/fig/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
This folder contains the `autocomplete-parser` project from https://github.com/aws/amazon-q-developer-cli/blob/main/packages/autocomplete-parser and its dependencies which were located in siblings folders and https://github.com/withfig/autocomplete-tools, both licenses under MIT. The fork was necessary for a few reasons:
|
||||
|
||||
- They ship as ESM modules which we're not ready to consume just yet.
|
||||
- We want the more complete `autocomplete-parser` that contains the important `parseArguments` function that does the bulk of the smarts in parsing the fig commands.
|
||||
- We needed to strip out all the implementation-specific parts from their `api-bindings` project that deals with settings, IPC, fuzzy sorting, etc.
|
||||
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Subcommand } from '../shared/internal';
|
||||
|
||||
const allCaches: Array<Map<string, unknown>> = [];
|
||||
|
||||
export const createCache = <T>() => {
|
||||
const cache = new Map<string, T>();
|
||||
allCaches.push(cache);
|
||||
return cache;
|
||||
};
|
||||
|
||||
export const resetCaches = () => {
|
||||
allCaches.forEach((cache) => {
|
||||
cache.clear();
|
||||
});
|
||||
};
|
||||
|
||||
// window.resetCaches = resetCaches;
|
||||
|
||||
export const specCache = createCache<Subcommand>();
|
||||
export const generateSpecCache = createCache<Subcommand>();
|
||||
|
||||
// window.listCache = () => {
|
||||
// console.log(specCache);
|
||||
// console.log(generateSpecCache);
|
||||
// };
|
||||
@@ -0,0 +1,21 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createErrorInstance } from '../shared/errors';
|
||||
|
||||
// LoadSpecErrors
|
||||
export const MissingSpecError = createErrorInstance('MissingSpecError');
|
||||
export const WrongDiffVersionedSpecError = createErrorInstance(
|
||||
'WrongDiffVersionedSpecError',
|
||||
);
|
||||
export const DisabledSpecError = createErrorInstance('DisabledSpecError');
|
||||
export const LoadLocalSpecError = createErrorInstance('LoadLocalSpecError');
|
||||
export const SpecCDNError = createErrorInstance('SpecCDNError');
|
||||
|
||||
// ParsingErrors
|
||||
export const ParsingHistoryError = createErrorInstance('ParsingHistoryError');
|
||||
|
||||
export const ParseArgumentsError = createErrorInstance('ParseArgumentsError');
|
||||
export const UpdateStateError = createErrorInstance('UpdateStateError');
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { makeArray } from './utils';
|
||||
|
||||
export type SuggestionType = Fig.SuggestionType | 'history' | 'auto-execute';
|
||||
|
||||
type Override<T, S> = Omit<T, keyof S> & S;
|
||||
export type Suggestion = Override<Fig.Suggestion, { type?: SuggestionType }>;
|
||||
|
||||
export type Option<ArgT, OptionT> = OptionT & {
|
||||
name: string[];
|
||||
args: ArgT[];
|
||||
};
|
||||
|
||||
export type Subcommand<ArgT, OptionT, SubcommandT> = SubcommandT & {
|
||||
name: string[];
|
||||
subcommands: Record<string, Subcommand<ArgT, OptionT, SubcommandT>>;
|
||||
options: Record<string, Option<ArgT, OptionT>>;
|
||||
persistentOptions: Record<string, Option<ArgT, OptionT>>;
|
||||
args: ArgT[];
|
||||
};
|
||||
|
||||
const makeNamedMap = <T extends { name: string[] }>(items: T[] | undefined): Record<string, T> => {
|
||||
const nameMapping: Record<string, T> = {};
|
||||
if (!items) {
|
||||
return nameMapping;
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
items[i].name.forEach((name) => {
|
||||
nameMapping[name] = items[i];
|
||||
});
|
||||
}
|
||||
return nameMapping;
|
||||
};
|
||||
|
||||
export type Initializer<ArgT, OptionT, SubcommandT> = {
|
||||
subcommand: (subcommand: Fig.Subcommand) => SubcommandT;
|
||||
option: (option: Fig.Option) => OptionT;
|
||||
arg: (arg: Fig.Arg) => ArgT;
|
||||
};
|
||||
|
||||
function convertOption<ArgT, OptionT>(
|
||||
option: Fig.Option,
|
||||
initialize: Omit<Initializer<ArgT, OptionT, never>, 'subcommand'>
|
||||
): Option<ArgT, OptionT> {
|
||||
return {
|
||||
...initialize.option(option),
|
||||
name: makeArray(option.name),
|
||||
args: option.args ? makeArray(option.args).map(initialize.arg) : [],
|
||||
};
|
||||
}
|
||||
|
||||
export function convertSubcommand<ArgT, OptionT, SubcommandT>(
|
||||
subcommand: Fig.Subcommand,
|
||||
initialize: Initializer<ArgT, OptionT, SubcommandT>
|
||||
): Subcommand<ArgT, OptionT, SubcommandT> {
|
||||
const { subcommands, options, args } = subcommand;
|
||||
return {
|
||||
...initialize.subcommand(subcommand),
|
||||
name: makeArray(subcommand.name),
|
||||
subcommands: makeNamedMap(subcommands?.map((s) => convertSubcommand(s, initialize))),
|
||||
options: makeNamedMap(
|
||||
options
|
||||
?.filter((option) => !option.isPersistent)
|
||||
?.map((option) => convertOption(option, initialize))
|
||||
),
|
||||
persistentOptions: makeNamedMap(
|
||||
options
|
||||
?.filter((option) => option.isPersistent)
|
||||
?.map((option) => convertOption(option, initialize))
|
||||
),
|
||||
args: args ? makeArray(args).map(initialize.arg) : [],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type * as Internal from './convert';
|
||||
import type * as Metadata from './specMetadata';
|
||||
import { revertSubcommand } from './revert';
|
||||
import { convertSubcommand } from './convert';
|
||||
import { convertLoadSpec, initializeDefault } from './specMetadata';
|
||||
import { SpecMixin, applyMixin, mergeSubcommands } from './mixins';
|
||||
import { SpecLocationSource, makeArray } from './utils';
|
||||
|
||||
export {
|
||||
Internal,
|
||||
revertSubcommand,
|
||||
convertSubcommand,
|
||||
Metadata,
|
||||
convertLoadSpec,
|
||||
initializeDefault,
|
||||
SpecMixin,
|
||||
applyMixin,
|
||||
mergeSubcommands,
|
||||
makeArray,
|
||||
SpecLocationSource,
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { makeArray } from './utils';
|
||||
|
||||
export type SpecMixin =
|
||||
| Fig.Subcommand
|
||||
| ((currentSpec: Fig.Subcommand, context: Fig.ShellContext) => Fig.Subcommand);
|
||||
|
||||
type NamedObject = { name: Fig.SingleOrArray<string> };
|
||||
|
||||
const concatArrays = <T>(a: T[] | undefined, b: T[] | undefined): T[] | undefined =>
|
||||
a && b ? [...a, ...b] : a || b;
|
||||
|
||||
const mergeNames = <T = string>(a: T | T[], b: T | T[]): T | T[] => [
|
||||
...new Set(concatArrays(makeArray(a), makeArray(b))),
|
||||
];
|
||||
|
||||
const mergeArrays = <T>(a: T[] | undefined, b: T[] | undefined): T[] | undefined =>
|
||||
a && b ? [...new Set(concatArrays(makeArray(a), makeArray(b)))] : a || b;
|
||||
|
||||
const mergeArgs = (arg: Fig.Arg, partial: Fig.Arg): Fig.Arg => ({
|
||||
...arg,
|
||||
...partial,
|
||||
suggestions: concatArrays<Fig.Suggestion | string>(arg.suggestions, partial.suggestions),
|
||||
generators:
|
||||
arg.generators && partial.generators
|
||||
? concatArrays(makeArray(arg.generators), makeArray(partial.generators))
|
||||
: arg.generators || partial.generators,
|
||||
template:
|
||||
arg.template && partial.template
|
||||
? mergeNames<Fig.TemplateStrings>(arg.template, partial.template)
|
||||
: arg.template || partial.template,
|
||||
});
|
||||
|
||||
const mergeArgArrays = (
|
||||
args: Fig.SingleOrArray<Fig.Arg> | undefined,
|
||||
partials: Fig.SingleOrArray<Fig.Arg> | undefined
|
||||
): Fig.SingleOrArray<Fig.Arg> | undefined => {
|
||||
if (!args || !partials) {
|
||||
return args || partials;
|
||||
}
|
||||
const argArray = makeArray(args);
|
||||
const partialArray = makeArray(partials);
|
||||
const result = [];
|
||||
for (let i = 0; i < Math.max(argArray.length, partialArray.length); i += 1) {
|
||||
const arg = argArray[i];
|
||||
const partial = partialArray[i];
|
||||
if (arg !== undefined && partial !== undefined) {
|
||||
result.push(mergeArgs(arg, partial));
|
||||
} else if (partial !== undefined || arg !== undefined) {
|
||||
result.push(arg || partial);
|
||||
}
|
||||
}
|
||||
return result.length === 1 ? result[0] : result;
|
||||
};
|
||||
|
||||
const mergeOptions = (option: Fig.Option, partial: Fig.Option): Fig.Option => ({
|
||||
...option,
|
||||
...partial,
|
||||
name: mergeNames(option.name, partial.name),
|
||||
args: mergeArgArrays(option.args, partial.args),
|
||||
exclusiveOn: mergeArrays(option.exclusiveOn, partial.exclusiveOn),
|
||||
dependsOn: mergeArrays(option.dependsOn, partial.dependsOn),
|
||||
});
|
||||
|
||||
const mergeNamedObjectArrays = <T extends NamedObject>(
|
||||
objects: T[] | undefined,
|
||||
partials: T[] | undefined,
|
||||
mergeItems: (a: T, b: T) => T
|
||||
): T[] | undefined => {
|
||||
if (!objects || !partials) {
|
||||
return objects || partials;
|
||||
}
|
||||
const mergedObjects = objects ? [...objects] : [];
|
||||
|
||||
const existingNameIndexMap: Record<string, number> = {};
|
||||
for (let i = 0; i < objects.length; i += 1) {
|
||||
makeArray(objects[i].name).forEach((name) => {
|
||||
existingNameIndexMap[name] = i;
|
||||
});
|
||||
}
|
||||
|
||||
for (let i = 0; i < partials.length; i += 1) {
|
||||
const partial = partials[i];
|
||||
if (!partial) {
|
||||
throw new Error('Invalid object passed to merge');
|
||||
}
|
||||
const existingNames = makeArray(partial.name).filter((name) => name in existingNameIndexMap);
|
||||
if (existingNames.length === 0) {
|
||||
mergedObjects.push(partial);
|
||||
} else {
|
||||
const index = existingNameIndexMap[existingNames[0]];
|
||||
if (existingNames.some((name) => existingNameIndexMap[name] !== index)) {
|
||||
throw new Error('Names provided for option matched multiple existing options');
|
||||
}
|
||||
mergedObjects[index] = mergeItems(mergedObjects[index], partial);
|
||||
}
|
||||
}
|
||||
return mergedObjects;
|
||||
};
|
||||
|
||||
function mergeOptionArrays(
|
||||
options: Fig.Option[] | undefined,
|
||||
partials: Fig.Option[] | undefined
|
||||
): Fig.Option[] | undefined {
|
||||
return mergeNamedObjectArrays(options, partials, mergeOptions);
|
||||
}
|
||||
|
||||
function mergeSubcommandArrays(
|
||||
subcommands: Fig.Subcommand[] | undefined,
|
||||
partials: Fig.Subcommand[] | undefined
|
||||
): Fig.Subcommand[] | undefined {
|
||||
return mergeNamedObjectArrays(subcommands, partials, mergeSubcommands);
|
||||
}
|
||||
|
||||
export function mergeSubcommands(
|
||||
subcommand: Fig.Subcommand,
|
||||
partial: Fig.Subcommand
|
||||
): Fig.Subcommand {
|
||||
return {
|
||||
...subcommand,
|
||||
...partial,
|
||||
name: mergeNames(subcommand.name, partial.name),
|
||||
args: mergeArgArrays(subcommand.args, partial.args),
|
||||
additionalSuggestions: concatArrays<Fig.Suggestion | string>(
|
||||
subcommand.additionalSuggestions,
|
||||
partial.additionalSuggestions
|
||||
),
|
||||
subcommands: mergeSubcommandArrays(subcommand.subcommands, partial.subcommands),
|
||||
options: mergeOptionArrays(subcommand.options, partial.options),
|
||||
parserDirectives:
|
||||
subcommand.parserDirectives && partial.parserDirectives
|
||||
? { ...subcommand.parserDirectives, ...partial.parserDirectives }
|
||||
: subcommand.parserDirectives || partial.parserDirectives,
|
||||
};
|
||||
}
|
||||
|
||||
export const applyMixin = (
|
||||
spec: Fig.Subcommand,
|
||||
context: Fig.ShellContext,
|
||||
mixin: SpecMixin
|
||||
): Fig.Subcommand => {
|
||||
if (typeof mixin === 'function') {
|
||||
return mixin(spec, context);
|
||||
}
|
||||
const partial = mixin;
|
||||
return mergeSubcommands(spec, partial);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Option, Subcommand } from './convert';
|
||||
|
||||
function makeSingleOrArray<T>(arr: T[]): Fig.SingleOrArray<T> {
|
||||
return arr.length === 1 ? (arr[0] as Fig.SingleOrArray<T>) : (arr as Fig.SingleOrArray<T>);
|
||||
}
|
||||
|
||||
function revertOption<ArgT extends Fig.Arg, OptionT>(option: Option<ArgT, OptionT>): Fig.Option {
|
||||
const { name, args } = option;
|
||||
|
||||
return {
|
||||
name: makeSingleOrArray(name),
|
||||
args,
|
||||
};
|
||||
}
|
||||
|
||||
export function revertSubcommand<ArgT extends Fig.Arg, OptionT, SubcommandT>(
|
||||
subcommand: Subcommand<ArgT, OptionT, SubcommandT>,
|
||||
postProcessingFn: (
|
||||
oldSub: Subcommand<ArgT, OptionT, SubcommandT>,
|
||||
newSub: Fig.Subcommand
|
||||
) => Fig.Subcommand
|
||||
): Fig.Subcommand {
|
||||
const { name, subcommands, options, persistentOptions, args } = subcommand;
|
||||
|
||||
const newSubcommand: Fig.Subcommand = {
|
||||
name: makeSingleOrArray(name),
|
||||
subcommands:
|
||||
Object.values(subcommands).length !== 0
|
||||
? Object.values(subcommands).map((sub) => revertSubcommand(sub, postProcessingFn))
|
||||
: undefined,
|
||||
options:
|
||||
Object.values(options).length !== 0
|
||||
? [
|
||||
...Object.values(options).map((option) => revertOption(option)),
|
||||
...Object.values(persistentOptions).map((option) => revertOption(option)),
|
||||
]
|
||||
: undefined,
|
||||
args: Object.values(args).length !== 0 ? makeSingleOrArray(Object.values(args)) : undefined,
|
||||
};
|
||||
return postProcessingFn(subcommand, newSubcommand);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Subcommand, convertSubcommand, Initializer } from './convert';
|
||||
import { makeArray, SpecLocationSource } from './utils';
|
||||
|
||||
type FigLoadSpecFn = Fig.LoadSpec extends infer U ? (U extends Function ? U : never) : never;
|
||||
export type LoadSpec<ArgT = ArgMeta, OptionT = OptionMeta, SubcommandT = SubcommandMeta> =
|
||||
| Fig.SpecLocation[]
|
||||
| Subcommand<ArgT, OptionT, SubcommandT>
|
||||
| ((
|
||||
...args: Parameters<FigLoadSpecFn>
|
||||
) => Promise<Fig.SpecLocation[] | Subcommand<ArgT, OptionT, SubcommandT>>);
|
||||
|
||||
export type OptionMeta = Omit<Fig.Option, 'args' | 'name'>;
|
||||
export type ArgMeta = Omit<Fig.Arg, 'template' | 'generators' | 'loadSpec'> & {
|
||||
generators: Fig.Generator[];
|
||||
loadSpec?: LoadSpec<ArgMeta, OptionMeta, SubcommandMeta>;
|
||||
};
|
||||
|
||||
type SubcommandMetaExcludes =
|
||||
| 'subcommands'
|
||||
| 'options'
|
||||
| 'loadSpec'
|
||||
| 'persistentOptions'
|
||||
| 'args'
|
||||
| 'name';
|
||||
export type SubcommandMeta = Omit<Fig.Subcommand, SubcommandMetaExcludes> & {
|
||||
loadSpec?: LoadSpec<ArgMeta, OptionMeta, SubcommandMeta>;
|
||||
};
|
||||
|
||||
export function convertLoadSpec<ArgT, OptionT, SubcommandT>(
|
||||
loadSpec: Fig.LoadSpec,
|
||||
initialize: Initializer<ArgT, OptionT, SubcommandT>
|
||||
): LoadSpec<ArgT, OptionT, SubcommandT> {
|
||||
if (typeof loadSpec === 'string') {
|
||||
return [{ name: loadSpec, type: SpecLocationSource.GLOBAL }];
|
||||
}
|
||||
|
||||
if (typeof loadSpec === 'function') {
|
||||
return (...args) =>
|
||||
loadSpec(...args).then((result) => {
|
||||
if (Array.isArray(result)) {
|
||||
return result;
|
||||
}
|
||||
if ('type' in result) {
|
||||
return [result];
|
||||
}
|
||||
return convertSubcommand(result, initialize);
|
||||
});
|
||||
}
|
||||
|
||||
return convertSubcommand(loadSpec, initialize);
|
||||
}
|
||||
|
||||
function initializeOptionMeta(option: Fig.Option): OptionMeta {
|
||||
return option;
|
||||
}
|
||||
|
||||
// Default initialization functions:
|
||||
function initializeArgMeta(arg: Fig.Arg): ArgMeta {
|
||||
const { template, ...rest } = arg;
|
||||
const generators = template ? [{ template }] : makeArray(arg.generators ?? []);
|
||||
return {
|
||||
...rest,
|
||||
loadSpec: arg.loadSpec
|
||||
? convertLoadSpec(arg.loadSpec, {
|
||||
option: initializeOptionMeta,
|
||||
subcommand: initializeSubcommandMeta,
|
||||
arg: initializeArgMeta,
|
||||
})
|
||||
: undefined,
|
||||
generators: generators.map((generator) => {
|
||||
let { trigger, getQueryTerm } = generator;
|
||||
if (generator.template) {
|
||||
const templates = makeArray(generator.template);
|
||||
if (templates.includes('folders') || templates.includes('filepaths')) {
|
||||
trigger = trigger ?? '/';
|
||||
getQueryTerm = getQueryTerm ?? '/';
|
||||
}
|
||||
}
|
||||
return { ...generator, trigger, getQueryTerm };
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function initializeSubcommandMeta(subcommand: Fig.Subcommand): SubcommandMeta {
|
||||
return {
|
||||
...subcommand,
|
||||
loadSpec: subcommand.loadSpec
|
||||
? convertLoadSpec(subcommand.loadSpec, {
|
||||
subcommand: initializeSubcommandMeta,
|
||||
option: initializeOptionMeta,
|
||||
arg: initializeArgMeta,
|
||||
})
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export const initializeDefault: Initializer<ArgMeta, OptionMeta, SubcommandMeta> = {
|
||||
subcommand: initializeSubcommandMeta,
|
||||
option: initializeOptionMeta,
|
||||
arg: initializeArgMeta,
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export function makeArray<T>(object: T | T[]): T[] {
|
||||
return Array.isArray(object) ? object : [object];
|
||||
}
|
||||
|
||||
export enum SpecLocationSource {
|
||||
GLOBAL = 'global',
|
||||
LOCAL = 'local',
|
||||
}
|
||||
@@ -3,23 +3,10 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withBrowserDefaults = require('../shared.webpack.config').browser;
|
||||
|
||||
module.exports = withBrowserDefaults({
|
||||
context: __dirname,
|
||||
entry: {
|
||||
extension: './src/terminalSuggestMain.ts'
|
||||
},
|
||||
output: {
|
||||
filename: 'terminalSuggestMain.js'
|
||||
},
|
||||
resolve: {
|
||||
fallback: {
|
||||
'child_process': false
|
||||
export const createErrorInstance = (name: string) =>
|
||||
class extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = `Fig.${name}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
10
extensions/terminal-suggest/src/fig/shared/index.ts
Normal file
10
extensions/terminal-suggest/src/fig/shared/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as Errors from './errors.js';
|
||||
import * as Internal from './internal.js';
|
||||
import * as Utils from './utils.js';
|
||||
|
||||
export { Errors, Internal, Utils };
|
||||
38
extensions/terminal-suggest/src/fig/shared/internal.ts
Normal file
38
extensions/terminal-suggest/src/fig/shared/internal.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Internal, Metadata } from '../fig-autocomplete-shared';
|
||||
|
||||
export type SpecLocation = Fig.SpecLocation & {
|
||||
diffVersionedFile?: string;
|
||||
};
|
||||
|
||||
type Override<T, S> = Omit<T, keyof S> & S;
|
||||
export type SuggestionType = Fig.SuggestionType | 'history' | 'auto-execute';
|
||||
export type Suggestion<ArgT = Metadata.ArgMeta> = Override<
|
||||
Fig.Suggestion,
|
||||
{
|
||||
type?: SuggestionType;
|
||||
// Whether or not to add a space after suggestion, e.g. if user completes a
|
||||
// subcommand that takes a mandatory arg.
|
||||
shouldAddSpace?: boolean;
|
||||
// Whether or not to add a separator after suggestion, e.g. for options with requiresSeparator
|
||||
separatorToAdd?: string;
|
||||
args?: ArgT[];
|
||||
// Generator information to determine whether suggestion should be filtered.
|
||||
generator?: Fig.Generator;
|
||||
getQueryTerm?: (x: string) => string;
|
||||
// fuzzyMatchData?: (Result | null)[];
|
||||
originalType?: SuggestionType;
|
||||
}
|
||||
>;
|
||||
|
||||
export type Arg = Metadata.ArgMeta;
|
||||
export type Option = Internal.Option<Metadata.ArgMeta, Metadata.OptionMeta>;
|
||||
export type Subcommand = Internal.Subcommand<
|
||||
Metadata.ArgMeta,
|
||||
Metadata.OptionMeta,
|
||||
Metadata.SubcommandMeta
|
||||
>;
|
||||
143
extensions/terminal-suggest/src/fig/shared/test/utils.test.ts
Normal file
143
extensions/terminal-suggest/src/fig/shared/test/utils.test.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { deepStrictEqual, ok } from 'node:assert';
|
||||
import {
|
||||
makeArray,
|
||||
makeArrayIfExists,
|
||||
longestCommonPrefix,
|
||||
compareNamedObjectsAlphabetically,
|
||||
fieldsAreEqual,
|
||||
} from '../utils';
|
||||
|
||||
function expect<T>(a: T): { toEqual: (b: T) => void } {
|
||||
return {
|
||||
toEqual: (b: T) => {
|
||||
deepStrictEqual(a, b);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
suite('fig/shared/ fieldsAreEqual', () => {
|
||||
test('should return immediately if two values are the same', () => {
|
||||
expect(fieldsAreEqual('hello', 'hello', [])).toEqual(true);
|
||||
expect(fieldsAreEqual('hello', 'hell', [])).toEqual(false);
|
||||
expect(fieldsAreEqual(1, 1, ['valueOf'])).toEqual(true);
|
||||
expect(fieldsAreEqual(null, null, [])).toEqual(true);
|
||||
expect(fieldsAreEqual(null, undefined, [])).toEqual(false);
|
||||
expect(fieldsAreEqual(undefined, undefined, [])).toEqual(true);
|
||||
expect(fieldsAreEqual(null, 'hello', [])).toEqual(false);
|
||||
expect(fieldsAreEqual(100, null, [])).toEqual(false);
|
||||
expect(fieldsAreEqual({}, {}, [])).toEqual(true);
|
||||
expect(
|
||||
fieldsAreEqual(
|
||||
() => { },
|
||||
() => { },
|
||||
[],
|
||||
),
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
test('should return true if fields are equal', () => {
|
||||
const fn = () => { };
|
||||
expect(
|
||||
fieldsAreEqual(
|
||||
{
|
||||
a: 'hello',
|
||||
b: 100,
|
||||
c: undefined,
|
||||
d: false,
|
||||
e: fn,
|
||||
f: { fa: true, fb: { fba: true } },
|
||||
g: null,
|
||||
},
|
||||
{
|
||||
a: 'hello',
|
||||
b: 100,
|
||||
c: undefined,
|
||||
d: false,
|
||||
e: fn,
|
||||
f: { fa: true, fb: { fba: true } },
|
||||
g: null,
|
||||
},
|
||||
['a', 'b', 'c', 'd', 'e', 'f', 'g'],
|
||||
),
|
||||
).toEqual(true);
|
||||
expect(fieldsAreEqual({ a: {} }, { a: {} }, ['a'])).toEqual(true);
|
||||
});
|
||||
|
||||
test('should return false if any field is not equal or fields are not specified', () => {
|
||||
expect(fieldsAreEqual({ a: null }, { a: {} }, ['a'])).toEqual(false);
|
||||
expect(fieldsAreEqual({ a: undefined }, { a: 'hello' }, ['a'])).toEqual(
|
||||
false,
|
||||
);
|
||||
expect(fieldsAreEqual({ a: false }, { a: true }, ['a'])).toEqual(false);
|
||||
expect(
|
||||
fieldsAreEqual(
|
||||
{ a: { b: { c: 'hello' } } },
|
||||
{ a: { b: { c: 'hell' } } },
|
||||
['a'],
|
||||
),
|
||||
).toEqual(false);
|
||||
expect(fieldsAreEqual({ a: 'true' }, { b: 'true' }, [])).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
suite('fig/shared/ makeArray', () => {
|
||||
test('should transform an object into an array', () => {
|
||||
expect(makeArray(true)).toEqual([true]);
|
||||
});
|
||||
|
||||
test('should not transform arrays with one value', () => {
|
||||
expect(makeArray([true])).toEqual([true]);
|
||||
});
|
||||
|
||||
test('should not transform arrays with multiple values', () => {
|
||||
expect(makeArray([true, false])).toEqual([true, false]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('fig/shared/ makeArrayIfExists', () => {
|
||||
test('works', () => {
|
||||
expect(makeArrayIfExists(null)).toEqual(null);
|
||||
expect(makeArrayIfExists(undefined)).toEqual(null);
|
||||
expect(makeArrayIfExists('a')).toEqual(['a']);
|
||||
expect(makeArrayIfExists(['a'])).toEqual(['a']);
|
||||
});
|
||||
});
|
||||
|
||||
suite('fig/shared/ longestCommonPrefix', () => {
|
||||
test('should return the shared match', () => {
|
||||
expect(longestCommonPrefix(['foo', 'foo bar', 'foo hello world'])).toEqual(
|
||||
'foo',
|
||||
);
|
||||
});
|
||||
|
||||
test('should return nothing if not all items starts by the same chars', () => {
|
||||
expect(longestCommonPrefix(['foo', 'foo bar', 'hello world'])).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
suite('fig/shared/ compareNamedObjectsAlphabetically', () => {
|
||||
test('should return 1 to sort alphabetically z against b for string', () => {
|
||||
ok(compareNamedObjectsAlphabetically('z', 'b') > 0);
|
||||
});
|
||||
|
||||
test('should return 1 to sort alphabetically z against b for object with name', () => {
|
||||
ok(compareNamedObjectsAlphabetically({ name: 'z' }, { name: 'b' }) > 0);
|
||||
});
|
||||
|
||||
test('should return 1 to sort alphabetically c against x for object with name', () => {
|
||||
ok(compareNamedObjectsAlphabetically({ name: 'c' }, { name: 'x' }) < 0);
|
||||
});
|
||||
|
||||
test('should return 1 to sort alphabetically z against b for object with name array', () => {
|
||||
ok(compareNamedObjectsAlphabetically({ name: ['z'] }, { name: ['b'] }) > 0);
|
||||
});
|
||||
|
||||
test('should return 1 to sort alphabetically c against x for object with name array', () => {
|
||||
ok(compareNamedObjectsAlphabetically({ name: ['c'] }, { name: ['x'] }) < 0);
|
||||
});
|
||||
});
|
||||
266
extensions/terminal-suggest/src/fig/shared/utils.ts
Normal file
266
extensions/terminal-suggest/src/fig/shared/utils.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { osIsWindows } from '../../helpers/os.js';
|
||||
import { createErrorInstance } from './errors.js';
|
||||
|
||||
// Use bitwise representation of suggestion flags.
|
||||
// See here: https://stackoverflow.com/questions/39359740/what-are-enum-flags-in-typescript/
|
||||
//
|
||||
// Given a number `flags` we can test `if (flags & Subcommands)` to see if we
|
||||
// should be suggesting subcommands.
|
||||
//
|
||||
// This is more maintainable in the future if we add more options (e.g. if we
|
||||
// distinguish between subcommand args and option args) as we can just add a
|
||||
// number here instead of passing 3+ boolean flags everywhere.
|
||||
export enum SuggestionFlag {
|
||||
None = 0,
|
||||
Subcommands = 1 << 0,
|
||||
Options = 1 << 1,
|
||||
Args = 1 << 2,
|
||||
Any = (1 << 2) | (1 << 1) | (1 << 0),
|
||||
}
|
||||
|
||||
// Combination of suggestion flags.
|
||||
export type SuggestionFlags = number;
|
||||
|
||||
export enum SpecLocationSource {
|
||||
GLOBAL = 'global',
|
||||
LOCAL = 'local',
|
||||
}
|
||||
|
||||
export function makeArray<T>(object: T | T[]): T[] {
|
||||
return Array.isArray(object) ? object : [object];
|
||||
}
|
||||
|
||||
export function firstMatchingToken(
|
||||
str: string,
|
||||
chars: Set<string>,
|
||||
): string | undefined {
|
||||
for (const char of str) {
|
||||
if (chars.has(char)) {
|
||||
return char;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function makeArrayIfExists<T>(
|
||||
obj: T | T[] | null | undefined,
|
||||
): T[] | null {
|
||||
return !obj ? null : makeArray(obj);
|
||||
}
|
||||
|
||||
export function isOrHasValue(
|
||||
obj: string | Array<string>,
|
||||
valueToMatch: string,
|
||||
) {
|
||||
return Array.isArray(obj) ? obj.includes(valueToMatch) : obj === valueToMatch;
|
||||
}
|
||||
|
||||
export const TimeoutError = createErrorInstance('TimeoutError');
|
||||
|
||||
export async function withTimeout<T>(
|
||||
time: number,
|
||||
promise: Promise<T>,
|
||||
): Promise<T> {
|
||||
return Promise.race<Promise<T>>([
|
||||
promise,
|
||||
new Promise<T>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new TimeoutError('Function timed out'));
|
||||
}, time);
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
export const longestCommonPrefix = (strings: string[]): string => {
|
||||
const sorted = strings.sort();
|
||||
|
||||
const { 0: firstItem, [sorted.length - 1]: lastItem } = sorted;
|
||||
const firstItemLength = firstItem.length;
|
||||
|
||||
let i = 0;
|
||||
|
||||
while (i < firstItemLength && firstItem.charAt(i) === lastItem.charAt(i)) {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return firstItem.slice(0, i);
|
||||
};
|
||||
|
||||
export function findLast<T>(
|
||||
values: T[],
|
||||
predicate: (v: T) => boolean,
|
||||
): T | undefined {
|
||||
for (let i = values.length - 1; i >= 0; i -= 1) {
|
||||
if (predicate(values[i])) {
|
||||
return values[i];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type NamedObject =
|
||||
| {
|
||||
name?: string[] | string;
|
||||
}
|
||||
| string;
|
||||
|
||||
export function compareNamedObjectsAlphabetically<
|
||||
A extends NamedObject,
|
||||
B extends NamedObject,
|
||||
>(a: A, b: B): number {
|
||||
const getName = (object: NamedObject): string =>
|
||||
typeof object === 'string' ? object : makeArray(object.name)[0] || '';
|
||||
return getName(a).localeCompare(getName(b));
|
||||
}
|
||||
|
||||
export const sleep = (ms: number): Promise<void> =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
||||
export type Func<S extends unknown[], T> = (...args: S) => T;
|
||||
type EqualFunc<T> = (args: T, newArgs: T) => boolean;
|
||||
|
||||
// Memoize a function (cache the most recent result based on the most recent args)
|
||||
// Optionally can pass an equals function to determine whether or not the old arguments
|
||||
// and new arguments are equal.
|
||||
//
|
||||
// e.g. let fn = (a, b) => a * 2
|
||||
//
|
||||
// If we memoize this then we recompute every time a or b changes. if we memoize with
|
||||
// isEqual = ([a, b], [newA, newB]) => newA === a
|
||||
// then we will only recompute when a changes.
|
||||
export function memoizeOne<S extends unknown[], T>(
|
||||
fn: Func<S, T>,
|
||||
isEqual?: EqualFunc<S>,
|
||||
): Func<S, T> {
|
||||
let lastArgs = [] as unknown[] as S;
|
||||
let lastResult: T;
|
||||
let hasBeenCalled = false;
|
||||
const areArgsEqual: EqualFunc<S> =
|
||||
isEqual || ((args, newArgs) => args.every((x, idx) => x === newArgs[idx]));
|
||||
return (...args: S): T => {
|
||||
if (!hasBeenCalled || !areArgsEqual(lastArgs, args)) {
|
||||
hasBeenCalled = true;
|
||||
lastArgs = [...args] as unknown[] as S;
|
||||
lastResult = fn(...args);
|
||||
}
|
||||
return lastResult;
|
||||
};
|
||||
}
|
||||
|
||||
function isNonNullObj(v: unknown): v is Record<string, unknown> {
|
||||
return typeof v === 'object' && v !== null;
|
||||
}
|
||||
|
||||
function isEmptyObject(v: unknown): v is Record<string, never> {
|
||||
return isNonNullObj(v) && Object.keys(v).length === 0;
|
||||
}
|
||||
|
||||
// TODO: to fix this we may want to have the default fields as Object.keys(A)
|
||||
/**
|
||||
* If no fields are specified and A,B are not equal primitives/empty objects, this returns false
|
||||
* even if the objects are actually equal.
|
||||
*/
|
||||
export function fieldsAreEqual<T>(A: T, B: T, fields: (keyof T)[]): boolean {
|
||||
if (A === B || (isEmptyObject(A) && isEmptyObject(B))) {
|
||||
return true;
|
||||
}
|
||||
if (!fields.length || !A || !B) {
|
||||
return false;
|
||||
}
|
||||
return fields.every((field) => {
|
||||
const aField = A[field];
|
||||
const bField = B[field];
|
||||
|
||||
if (typeof aField !== typeof bField) {
|
||||
return false;
|
||||
}
|
||||
if (isNonNullObj(aField) && isNonNullObj(bField)) {
|
||||
if (Object.keys(aField).length !== Object.keys(bField).length) {
|
||||
return false;
|
||||
}
|
||||
return fieldsAreEqual(aField, bField, Object.keys(aField) as never[]);
|
||||
}
|
||||
return aField === bField;
|
||||
});
|
||||
}
|
||||
|
||||
export const splitPath = (path: string): [string, string] => {
|
||||
const idx = path.lastIndexOf('/') + 1;
|
||||
return [path.slice(0, idx), path.slice(idx)];
|
||||
};
|
||||
|
||||
export const ensureTrailingSlash = (str: string) =>
|
||||
str.endsWith('/') ? str : `${str}/`;
|
||||
|
||||
// Outputs CWD with trailing `/`
|
||||
export const getCWDForFilesAndFolders = (
|
||||
cwd: string | null,
|
||||
searchTerm: string,
|
||||
): string => {
|
||||
if (cwd === null) {
|
||||
return '/';
|
||||
}
|
||||
const [dirname] = splitPath(searchTerm);
|
||||
|
||||
if (dirname === '') {
|
||||
return ensureTrailingSlash(cwd);
|
||||
}
|
||||
|
||||
return dirname.startsWith('~/') || dirname.startsWith('/')
|
||||
? dirname
|
||||
: `${cwd}/${dirname}`;
|
||||
};
|
||||
|
||||
export function localProtocol(domain: string, path: string) {
|
||||
let modifiedDomain;
|
||||
//TODO@meganrogge
|
||||
// if (domain === 'path' && !window.fig?.constants?.newUriFormat) {
|
||||
if (domain === 'path') {
|
||||
modifiedDomain = '';
|
||||
} else {
|
||||
modifiedDomain = domain;
|
||||
}
|
||||
|
||||
if (osIsWindows()) {
|
||||
return `https://fig.${modifiedDomain}/${path}`;
|
||||
}
|
||||
return `fig://${modifiedDomain}/${path}`;
|
||||
}
|
||||
|
||||
type ExponentialBackoffOptions = {
|
||||
attemptTimeout: number; // The maximum time in milliseconds to wait for a function to execute.
|
||||
baseDelay: number; // The initial delay in milliseconds.
|
||||
maxRetries: number; // The maximum number of retries.
|
||||
jitter: number; // A random factor to add to the delay on each retry.
|
||||
};
|
||||
|
||||
export async function exponentialBackoff<T>(
|
||||
options: ExponentialBackoffOptions,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
let retries = 0;
|
||||
let delay = options.baseDelay;
|
||||
|
||||
while (retries < options.maxRetries) {
|
||||
try {
|
||||
return await withTimeout(options.attemptTimeout, fn());
|
||||
} catch (_error) {
|
||||
retries += 1;
|
||||
delay *= 2;
|
||||
delay += Math.floor(Math.random() * options.jitter);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, delay);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Failed to execute function after all retries.');
|
||||
}
|
||||
241
extensions/terminal-suggest/src/fig/shell-parser/command.ts
Normal file
241
extensions/terminal-suggest/src/fig/shell-parser/command.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { NodeType, BaseNode, createTextNode, parse } from './parser.js';
|
||||
import { ConvertCommandError, SubstituteAliasError } from './errors.js';
|
||||
|
||||
export * from './errors.js';
|
||||
|
||||
export type Token = {
|
||||
text: string;
|
||||
node: BaseNode;
|
||||
originalNode: BaseNode;
|
||||
};
|
||||
|
||||
export type Command = {
|
||||
tokens: Token[];
|
||||
tree: BaseNode;
|
||||
|
||||
originalTree: BaseNode;
|
||||
};
|
||||
|
||||
export type AliasMap = Record<string, string>;
|
||||
|
||||
const descendantAtIndex = (
|
||||
node: BaseNode,
|
||||
index: number,
|
||||
type?: NodeType,
|
||||
): BaseNode | null => {
|
||||
if (node.startIndex <= index && index <= node.endIndex) {
|
||||
const descendant = node.children
|
||||
.map((child) => descendantAtIndex(child, index, type))
|
||||
.find(Boolean);
|
||||
if (descendant) {
|
||||
return descendant;
|
||||
}
|
||||
return !type || node.type === type ? node : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const createTextToken = (
|
||||
command: Command,
|
||||
index: number,
|
||||
text: string,
|
||||
originalNode?: BaseNode,
|
||||
): Token => {
|
||||
const { tree, originalTree, tokens } = command;
|
||||
|
||||
let indexDiff = 0;
|
||||
const tokenIndex = tokens.findIndex(
|
||||
(token) => index < token.originalNode.startIndex,
|
||||
);
|
||||
const token = tokens[tokenIndex];
|
||||
if (tokenIndex === 0) {
|
||||
indexDiff = token.node.startIndex - token.originalNode.startIndex;
|
||||
} else if (tokenIndex === -1) {
|
||||
indexDiff = tree.text.length - originalTree.text.length;
|
||||
} else {
|
||||
indexDiff = token.node.endIndex - token.originalNode.endIndex;
|
||||
}
|
||||
|
||||
return {
|
||||
originalNode:
|
||||
originalNode || createTextNode(originalTree.text, index, text),
|
||||
node: createTextNode(text, index + indexDiff, text),
|
||||
text,
|
||||
};
|
||||
};
|
||||
|
||||
const convertCommandNodeToCommand = (tree: BaseNode): Command => {
|
||||
if (tree.type !== NodeType.Command) {
|
||||
throw new ConvertCommandError('Cannot get tokens from non-command node');
|
||||
}
|
||||
|
||||
const command = {
|
||||
originalTree: tree,
|
||||
tree,
|
||||
tokens: tree.children.map((child) => ({
|
||||
originalNode: child,
|
||||
node: child,
|
||||
text: child.innerText,
|
||||
})),
|
||||
};
|
||||
|
||||
const { children, endIndex, text } = tree;
|
||||
if (
|
||||
+(children.length === 0 || children[children.length - 1].endIndex) <
|
||||
endIndex &&
|
||||
text.endsWith(' ')
|
||||
) {
|
||||
command.tokens.push(createTextToken(command, endIndex, ''));
|
||||
}
|
||||
return command;
|
||||
};
|
||||
|
||||
const shiftByAmount = (node: BaseNode, shift: number): BaseNode => ({
|
||||
...node,
|
||||
startIndex: node.startIndex + shift,
|
||||
endIndex: node.endIndex + shift,
|
||||
children: node.children.map((child) => shiftByAmount(child, shift)),
|
||||
});
|
||||
|
||||
export const substituteAlias = (
|
||||
command: Command,
|
||||
token: Token,
|
||||
alias: string,
|
||||
): Command => {
|
||||
if (command.tokens.find((t) => t === token) === undefined) {
|
||||
throw new SubstituteAliasError('Token not in command');
|
||||
}
|
||||
const { tree } = command;
|
||||
|
||||
const preAliasChars = token.node.startIndex - tree.startIndex;
|
||||
const postAliasChars = token.node.endIndex - tree.endIndex;
|
||||
|
||||
const preAliasText = `${tree.text.slice(0, preAliasChars)}`;
|
||||
const postAliasText = postAliasChars
|
||||
? `${tree.text.slice(postAliasChars)}`
|
||||
: '';
|
||||
|
||||
const commandBuffer = `${preAliasText}${alias}${postAliasText}`;
|
||||
|
||||
// Parse command and shift indices to align with original command.
|
||||
const parseTree = shiftByAmount(parse(commandBuffer), tree.startIndex);
|
||||
|
||||
if (parseTree.children.length !== 1) {
|
||||
throw new SubstituteAliasError('Invalid alias');
|
||||
}
|
||||
|
||||
const newCommand = convertCommandNodeToCommand(parseTree.children[0]);
|
||||
|
||||
const [aliasStart, aliasEnd] = [
|
||||
token.node.startIndex,
|
||||
token.node.startIndex + alias.length,
|
||||
];
|
||||
|
||||
let tokenIndexDiff = 0;
|
||||
let lastTokenInAlias = false;
|
||||
// Map tokens from new command back to old command to attributing the correct original nodes.
|
||||
const tokens = newCommand.tokens.map((newToken, index) => {
|
||||
const tokenInAlias =
|
||||
aliasStart < newToken.node.endIndex &&
|
||||
newToken.node.startIndex < aliasEnd;
|
||||
tokenIndexDiff += tokenInAlias && lastTokenInAlias ? 1 : 0;
|
||||
const { originalNode } = command.tokens[index - tokenIndexDiff];
|
||||
lastTokenInAlias = tokenInAlias;
|
||||
return { ...newToken, originalNode };
|
||||
});
|
||||
|
||||
if (newCommand.tokens.length - command.tokens.length !== tokenIndexDiff) {
|
||||
throw new SubstituteAliasError('Error substituting alias');
|
||||
}
|
||||
|
||||
return {
|
||||
originalTree: command.originalTree,
|
||||
tree: newCommand.tree,
|
||||
tokens,
|
||||
};
|
||||
};
|
||||
|
||||
export const expandCommand = (
|
||||
command: Command,
|
||||
_cursorIndex: number,
|
||||
aliases: AliasMap,
|
||||
): Command => {
|
||||
let expanded = command;
|
||||
const usedAliases = new Set();
|
||||
|
||||
// Check for aliases
|
||||
let [name] = expanded.tokens;
|
||||
while (
|
||||
expanded.tokens.length > 1 &&
|
||||
name &&
|
||||
aliases[name.text] &&
|
||||
!usedAliases.has(name.text)
|
||||
) {
|
||||
// Remove quotes
|
||||
const aliasValue = aliases[name.text].replace(/^'(.*)'$/g, '$1');
|
||||
try {
|
||||
expanded = substituteAlias(expanded, name, aliasValue);
|
||||
} catch (_err) {
|
||||
// TODO(refactoring): add logger again
|
||||
// console.error('Error substituting alias');
|
||||
}
|
||||
usedAliases.add(name.text);
|
||||
[name] = expanded.tokens;
|
||||
}
|
||||
|
||||
return expanded;
|
||||
};
|
||||
|
||||
export const getCommand = (
|
||||
buffer: string,
|
||||
aliases: AliasMap,
|
||||
cursorIndex?: number,
|
||||
): Command | null => {
|
||||
const index = cursorIndex === undefined ? buffer.length : cursorIndex;
|
||||
const parseTree = parse(buffer);
|
||||
const commandNode = descendantAtIndex(parseTree, index, NodeType.Command);
|
||||
if (commandNode === null) {
|
||||
return null;
|
||||
}
|
||||
const command = convertCommandNodeToCommand(commandNode);
|
||||
return expandCommand(command, index, aliases);
|
||||
};
|
||||
|
||||
const statements = [
|
||||
NodeType.Program,
|
||||
NodeType.CompoundStatement,
|
||||
NodeType.Subshell,
|
||||
NodeType.Pipeline,
|
||||
NodeType.List,
|
||||
NodeType.Command,
|
||||
];
|
||||
|
||||
export const getTopLevelCommands = (parseTree: BaseNode): Command[] => {
|
||||
if (parseTree.type === NodeType.Command) {
|
||||
return [convertCommandNodeToCommand(parseTree)];
|
||||
}
|
||||
if (!statements.includes(parseTree.type)) {
|
||||
return [];
|
||||
}
|
||||
const commands: Command[] = [];
|
||||
for (let i = 0; i < parseTree.children.length; i += 1) {
|
||||
commands.push(...getTopLevelCommands(parseTree.children[i]));
|
||||
}
|
||||
return commands;
|
||||
};
|
||||
|
||||
export const getAllCommandsWithAlias = (
|
||||
buffer: string,
|
||||
aliases: AliasMap,
|
||||
): Command[] => {
|
||||
const parseTree = parse(buffer);
|
||||
const commands = getTopLevelCommands(parseTree);
|
||||
return commands.map((command) =>
|
||||
expandCommand(command, command.tree.text.length, aliases),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createErrorInstance } from '../shared/errors';
|
||||
|
||||
export const SubstituteAliasError = createErrorInstance('SubstituteAliasError');
|
||||
export const ConvertCommandError = createErrorInstance('ConvertCommandError');
|
||||
@@ -0,0 +1,7 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export * from './parser.js';
|
||||
export * from './command.js';
|
||||
735
extensions/terminal-suggest/src/fig/shell-parser/parser.ts
Normal file
735
extensions/terminal-suggest/src/fig/shell-parser/parser.ts
Normal file
@@ -0,0 +1,735 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// Loosely follows the following grammar:
|
||||
// terminator = ";" | "&" | "&;"
|
||||
// literal = string | ansi_c_string | raw_string | expansion | simple_expansion | word
|
||||
// concatenation = literal literal
|
||||
// command = (concatenation | literal)+
|
||||
//
|
||||
// variable_name = word
|
||||
// subscript = variable_name"["literal"]"
|
||||
// assignment = (word | subscript)("=" | "+=")literal
|
||||
// assignment_list = assignment+ command?
|
||||
//
|
||||
// statement =
|
||||
// | "{" (statement terminator)+ "}"
|
||||
// | "(" statements ")"
|
||||
// | statement "||" statement
|
||||
// | statement "&&" statement
|
||||
// | statement "|" statement
|
||||
// | statement "|&" statement
|
||||
// | command
|
||||
// | assignment_list
|
||||
//
|
||||
// statements = (statement terminator)* statement terminator?
|
||||
// program = statements
|
||||
|
||||
export enum NodeType {
|
||||
Program = 'program',
|
||||
|
||||
AssignmentList = 'assignment_list',
|
||||
Assignment = 'assignment',
|
||||
VariableName = 'variable_name',
|
||||
Subscript = 'subscript',
|
||||
|
||||
CompoundStatement = 'compound_statement',
|
||||
Subshell = 'subshell',
|
||||
Command = 'command',
|
||||
Pipeline = 'pipeline',
|
||||
List = 'list',
|
||||
|
||||
// TODO: implement <(commands)
|
||||
ProcessSubstitution = 'process_substitution',
|
||||
|
||||
// Primary expressions
|
||||
Concatenation = 'concatenation',
|
||||
Word = 'word',
|
||||
String = 'string',
|
||||
Expansion = 'expansion',
|
||||
CommandSubstitution = 'command_substitution',
|
||||
|
||||
// Leaf Nodes
|
||||
RawString = 'raw_string',
|
||||
AnsiCString = 'ansi_c_string',
|
||||
SimpleExpansion = 'simple_expansion',
|
||||
SpecialExpansion = 'special_expansion',
|
||||
ArithmeticExpansion = 'arithmetic_expansion',
|
||||
}
|
||||
|
||||
export type LiteralNode =
|
||||
| BaseNode<NodeType.String>
|
||||
| BaseNode<NodeType.AnsiCString>
|
||||
| BaseNode<NodeType.RawString>
|
||||
| BaseNode<NodeType.CommandSubstitution>
|
||||
| BaseNode<NodeType.Concatenation>
|
||||
| BaseNode<NodeType.Expansion>
|
||||
| BaseNode<NodeType.ArithmeticExpansion>
|
||||
| BaseNode<NodeType.SimpleExpansion>
|
||||
| BaseNode<NodeType.SpecialExpansion>
|
||||
| BaseNode<NodeType.Word>;
|
||||
|
||||
export interface BaseNode<Type extends NodeType = NodeType> {
|
||||
text: string;
|
||||
// Unquoted text in node.
|
||||
innerText: string;
|
||||
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
|
||||
complete: boolean;
|
||||
|
||||
type: Type;
|
||||
children: BaseNode[];
|
||||
}
|
||||
|
||||
export interface ListNode extends BaseNode {
|
||||
type: NodeType.List;
|
||||
operator: '||' | '&&' | '|' | '|&';
|
||||
}
|
||||
|
||||
export interface AssignmentListNode extends BaseNode {
|
||||
type: NodeType.AssignmentList;
|
||||
children:
|
||||
| [...AssignmentNode[], BaseNode<NodeType.Command>]
|
||||
| AssignmentNode[];
|
||||
hasCommand: boolean;
|
||||
}
|
||||
|
||||
export interface AssignmentNode extends BaseNode {
|
||||
type: NodeType.Assignment;
|
||||
operator: '=' | '+=';
|
||||
name: BaseNode<NodeType.VariableName> | SubscriptNode;
|
||||
children: LiteralNode[];
|
||||
}
|
||||
|
||||
export interface SubscriptNode extends BaseNode {
|
||||
type: NodeType.Subscript;
|
||||
name: BaseNode<NodeType.VariableName>;
|
||||
index: LiteralNode;
|
||||
}
|
||||
|
||||
const operators = [';', '&', '&;', '|', '|&', '&&', '||'] as const;
|
||||
|
||||
type Operator = (typeof operators)[number];
|
||||
|
||||
const parseOperator = (str: string, index: number): Operator | null => {
|
||||
const c = str.charAt(index);
|
||||
if (['&', ';', '|'].includes(c)) {
|
||||
const op = str.slice(index, index + 2);
|
||||
return operators.includes(op as unknown as Operator)
|
||||
? (op as Operator)
|
||||
: (c as Operator);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const getInnerText = (node: BaseNode): string => {
|
||||
const { children, type, complete, text } = node;
|
||||
if (type === NodeType.Concatenation) {
|
||||
return children.reduce((current, child) => current + child.innerText, '');
|
||||
}
|
||||
|
||||
const terminalCharsMapping: { [key: string]: [string, string] | undefined } = {
|
||||
[NodeType.String]: ['"', '"'],
|
||||
[NodeType.RawString]: ['\'', '\''],
|
||||
[NodeType.AnsiCString]: ['$\'', '\''],
|
||||
};
|
||||
const terminalChars = terminalCharsMapping[type] ?? ['', ''];
|
||||
|
||||
const startChars = terminalChars[0];
|
||||
const endChars = !complete ? '' : terminalChars[1];
|
||||
|
||||
let innerText = '';
|
||||
for (let i = startChars.length; i < text.length - endChars.length; i += 1) {
|
||||
const c = text.charAt(i);
|
||||
const isWordEscape = c === '\\' && type === NodeType.Word;
|
||||
const isStringEscape =
|
||||
c === '\\' &&
|
||||
type === NodeType.String &&
|
||||
'$`"\\\n'.includes(text.charAt(i + 1));
|
||||
|
||||
if (isWordEscape || isStringEscape) {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
innerText += text.charAt(i);
|
||||
}
|
||||
return innerText;
|
||||
};
|
||||
|
||||
const createNode = <T extends BaseNode = BaseNode>(
|
||||
str: string,
|
||||
partial: Partial<T>,
|
||||
): T => {
|
||||
// eslint-disable-next-line local/code-no-dangerous-type-assertions
|
||||
const node = {
|
||||
startIndex: 0,
|
||||
type: NodeType.Word,
|
||||
endIndex: str.length,
|
||||
text: '',
|
||||
innerText: '',
|
||||
complete: true,
|
||||
children: [],
|
||||
...partial,
|
||||
} as BaseNode as T;
|
||||
const text = str.slice(node.startIndex, node.endIndex);
|
||||
const innerText = getInnerText({ ...node, text });
|
||||
return { ...node, text, innerText };
|
||||
};
|
||||
|
||||
export const createTextNode = (
|
||||
str: string,
|
||||
startIndex: number,
|
||||
text: string,
|
||||
): BaseNode =>
|
||||
createNode(str, { startIndex, text, endIndex: startIndex + text.length });
|
||||
|
||||
const nextWordIndex = (str: string, index: number) => {
|
||||
const firstChar = str.slice(index).search(/\S/);
|
||||
if (firstChar === -1) {
|
||||
return -1;
|
||||
}
|
||||
return index + firstChar;
|
||||
};
|
||||
|
||||
// Parse simple variable expansion ($foo or $$)
|
||||
const parseSimpleExpansion = (
|
||||
str: string,
|
||||
index: number,
|
||||
terminalChars: string[],
|
||||
):
|
||||
| BaseNode<NodeType.SimpleExpansion>
|
||||
| BaseNode<NodeType.SpecialExpansion>
|
||||
| null => {
|
||||
const node: Partial<BaseNode<NodeType.SimpleExpansion>> = {
|
||||
startIndex: index,
|
||||
type: NodeType.SimpleExpansion,
|
||||
};
|
||||
if (str.length > index + 1 && '*@?-$0_'.includes(str.charAt(index + 1))) {
|
||||
return createNode<BaseNode<NodeType.SpecialExpansion>>(str, {
|
||||
...node,
|
||||
type: NodeType.SpecialExpansion,
|
||||
endIndex: index + 2,
|
||||
});
|
||||
}
|
||||
const terminalSymbols = ['\t', ' ', '\n', '$', '\\', ...terminalChars];
|
||||
let i = index + 1;
|
||||
for (; i < str.length; i += 1) {
|
||||
if (terminalSymbols.includes(str.charAt(i))) {
|
||||
// Parse a literal $ if last token
|
||||
return i === index + 1
|
||||
? null
|
||||
: createNode<BaseNode<NodeType.SimpleExpansion>>(str, {
|
||||
...node,
|
||||
endIndex: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
return createNode<BaseNode<NodeType.SimpleExpansion>>(str, {
|
||||
...node,
|
||||
endIndex: i,
|
||||
});
|
||||
};
|
||||
|
||||
// Parse command substitution $(foo) or `foo`
|
||||
function parseCommandSubstitution(
|
||||
str: string,
|
||||
startIndex: number,
|
||||
terminalChar: string,
|
||||
): BaseNode<NodeType.CommandSubstitution> {
|
||||
const index =
|
||||
str.charAt(startIndex) === '`' ? startIndex + 1 : startIndex + 2;
|
||||
const { statements: children, terminatorIndex } = parseStatements(
|
||||
str,
|
||||
index,
|
||||
terminalChar,
|
||||
);
|
||||
const terminated = terminatorIndex !== -1;
|
||||
return createNode<BaseNode<NodeType.CommandSubstitution>>(str, {
|
||||
startIndex,
|
||||
type: NodeType.CommandSubstitution,
|
||||
complete: terminated && children.length !== 0,
|
||||
endIndex: terminated ? terminatorIndex + 1 : str.length,
|
||||
children,
|
||||
});
|
||||
}
|
||||
|
||||
const parseString = parseLiteral<NodeType.String>(NodeType.String, '"', '"');
|
||||
const parseRawString = parseLiteral<NodeType.RawString>(
|
||||
NodeType.RawString,
|
||||
'\'',
|
||||
'\'',
|
||||
);
|
||||
const parseExpansion = parseLiteral<NodeType.Expansion>(
|
||||
NodeType.Expansion,
|
||||
'${',
|
||||
'}',
|
||||
);
|
||||
const parseAnsiCString = parseLiteral<NodeType.AnsiCString>(
|
||||
NodeType.AnsiCString,
|
||||
'$\'',
|
||||
'\'',
|
||||
);
|
||||
const parseArithmeticExpansion = parseLiteral<NodeType.ArithmeticExpansion>(
|
||||
NodeType.ArithmeticExpansion,
|
||||
'$((',
|
||||
'))',
|
||||
);
|
||||
|
||||
function childAtIndex(
|
||||
str: string,
|
||||
index: number,
|
||||
inString: boolean,
|
||||
terminators: string[],
|
||||
): LiteralNode | null {
|
||||
const lookahead = [
|
||||
str.charAt(index),
|
||||
str.charAt(index + 1),
|
||||
str.charAt(index + 2),
|
||||
];
|
||||
switch (lookahead[0]) {
|
||||
case '$':
|
||||
if (lookahead[1] === '(') {
|
||||
return lookahead[2] === '('
|
||||
? parseArithmeticExpansion(str, index)
|
||||
: parseCommandSubstitution(str, index, ')');
|
||||
}
|
||||
if (lookahead[1] === '{') {
|
||||
return parseExpansion(str, index);
|
||||
}
|
||||
if (!inString && lookahead[1] === '\'') {
|
||||
return parseAnsiCString(str, index);
|
||||
}
|
||||
return parseSimpleExpansion(str, index, terminators);
|
||||
case '`':
|
||||
return parseCommandSubstitution(str, index, '`');
|
||||
case '\'':
|
||||
return inString ? null : parseRawString(str, index);
|
||||
case '"':
|
||||
return inString ? null : parseString(str, index);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function parseLiteral<T extends NodeType>(
|
||||
type: T,
|
||||
startChars: string,
|
||||
endChars: string,
|
||||
) {
|
||||
const canHaveChildren =
|
||||
type === NodeType.Expansion || type === NodeType.String;
|
||||
const isString = type === NodeType.String;
|
||||
return (str: string, startIndex: number): BaseNode<T> => {
|
||||
const children = [];
|
||||
for (let i = startIndex + startChars.length; i < str.length; i += 1) {
|
||||
const child = canHaveChildren
|
||||
? childAtIndex(str, i, isString, [endChars])
|
||||
: null;
|
||||
if (child !== null) {
|
||||
children.push(child);
|
||||
i = child.endIndex - 1;
|
||||
} else if (str.charAt(i) === '\\' && type !== NodeType.RawString) {
|
||||
i += 1;
|
||||
} else if (str.slice(i, i + endChars.length) === endChars) {
|
||||
return createNode<BaseNode<T>>(str, {
|
||||
startIndex,
|
||||
type,
|
||||
children,
|
||||
endIndex: i + endChars.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
return createNode<BaseNode<T>>(str, {
|
||||
startIndex,
|
||||
type,
|
||||
children,
|
||||
complete: false,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function parseStatements(
|
||||
str: string,
|
||||
index: number,
|
||||
terminalChar: string,
|
||||
mustTerminate = false,
|
||||
): {
|
||||
statements: BaseNode[];
|
||||
terminatorIndex: number;
|
||||
} {
|
||||
const statements = [];
|
||||
|
||||
let i = index;
|
||||
while (i < str.length) {
|
||||
// Will only exit on EOF, terminalChar or terminator symbol (;, &, &;)
|
||||
let statement = parseStatement(str, i, mustTerminate ? '' : terminalChar);
|
||||
|
||||
const opIndex = nextWordIndex(str, statement.endIndex);
|
||||
const reachedEnd = opIndex === -1;
|
||||
if (!mustTerminate && !reachedEnd && terminalChar === str.charAt(opIndex)) {
|
||||
statements.push(statement);
|
||||
return { statements, terminatorIndex: opIndex };
|
||||
}
|
||||
|
||||
if (reachedEnd) {
|
||||
statements.push(statement);
|
||||
break;
|
||||
}
|
||||
|
||||
const op = !reachedEnd && parseOperator(str, opIndex);
|
||||
if (op) {
|
||||
// Terminator symbol, ; | & | &;
|
||||
i = opIndex + op.length;
|
||||
const nextIndex = nextWordIndex(str, i);
|
||||
statements.push(statement);
|
||||
if (nextIndex !== -1 && str.charAt(nextIndex) === terminalChar) {
|
||||
return { statements, terminatorIndex: nextIndex };
|
||||
}
|
||||
} else {
|
||||
// Missing terminator but still have tokens left.
|
||||
// assignments do not require terminators
|
||||
statement = createNode(str, {
|
||||
...statement,
|
||||
complete:
|
||||
statement.type === NodeType.AssignmentList
|
||||
? statement.complete
|
||||
: false,
|
||||
});
|
||||
statements.push(statement);
|
||||
i = opIndex;
|
||||
}
|
||||
}
|
||||
return { statements, terminatorIndex: -1 };
|
||||
}
|
||||
|
||||
const parseConcatenationOrLiteralNode = (
|
||||
str: string,
|
||||
startIndex: number,
|
||||
terminalChar: string,
|
||||
): { children: LiteralNode[]; endIndex: number } => {
|
||||
const children: LiteralNode[] = [];
|
||||
|
||||
let argumentChildren: LiteralNode[] = [];
|
||||
let wordStart = -1;
|
||||
|
||||
const endWord = (endIndex: number) => {
|
||||
if (wordStart !== -1) {
|
||||
const word = createNode<BaseNode<NodeType.Word>>(str, {
|
||||
startIndex: wordStart,
|
||||
endIndex,
|
||||
});
|
||||
argumentChildren.push(word);
|
||||
}
|
||||
wordStart = -1;
|
||||
};
|
||||
|
||||
const endArgument = (endIndex: number) => {
|
||||
endWord(endIndex);
|
||||
let [argument] = argumentChildren;
|
||||
if (argumentChildren.length > 1) {
|
||||
const finalPart = argumentChildren[argumentChildren.length - 1];
|
||||
argument = createNode<BaseNode<NodeType.Concatenation>>(str, {
|
||||
startIndex: argumentChildren[0].startIndex,
|
||||
type: NodeType.Concatenation,
|
||||
endIndex: finalPart.endIndex,
|
||||
complete: finalPart.complete,
|
||||
children: argumentChildren,
|
||||
});
|
||||
}
|
||||
if (argument) {
|
||||
children.push(argument);
|
||||
}
|
||||
argumentChildren = [];
|
||||
};
|
||||
|
||||
const terminators = ['&', '|', ';', '\n', '\'', '"', '`'];
|
||||
if (terminalChar) {
|
||||
terminators.push(terminalChar);
|
||||
}
|
||||
|
||||
let i = startIndex;
|
||||
for (; i < str.length; i += 1) {
|
||||
const c = str.charAt(i);
|
||||
const op = parseOperator(str, i);
|
||||
if (op !== null || c === terminalChar) {
|
||||
// TODO: handle terminator like ; as first token.
|
||||
break;
|
||||
}
|
||||
const childNode = childAtIndex(str, i, false, terminators);
|
||||
if (childNode !== null) {
|
||||
endWord(i);
|
||||
argumentChildren.push(childNode);
|
||||
i = childNode.endIndex - 1;
|
||||
} else if ([' ', '\t'].includes(c)) {
|
||||
endArgument(i);
|
||||
} else {
|
||||
if (c === '\\') {
|
||||
i += 1;
|
||||
}
|
||||
if (wordStart === -1) {
|
||||
wordStart = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endArgument(i);
|
||||
|
||||
return { children, endIndex: i };
|
||||
};
|
||||
|
||||
function parseCommand(
|
||||
str: string,
|
||||
idx: number,
|
||||
terminalChar: string,
|
||||
): BaseNode<NodeType.Command> {
|
||||
const startIndex = Math.max(nextWordIndex(str, idx), idx);
|
||||
const { children, endIndex } = parseConcatenationOrLiteralNode(
|
||||
str,
|
||||
startIndex,
|
||||
terminalChar,
|
||||
);
|
||||
|
||||
return createNode<BaseNode<NodeType.Command>>(str, {
|
||||
startIndex,
|
||||
type: NodeType.Command,
|
||||
complete: children.length > 0,
|
||||
// Extend command up to separator.
|
||||
endIndex: children.length > 0 ? endIndex : str.length,
|
||||
children,
|
||||
});
|
||||
}
|
||||
|
||||
const parseAssignmentNode = (
|
||||
str: string,
|
||||
startIndex: number,
|
||||
): AssignmentNode => {
|
||||
const equalsIndex = str.indexOf('=', startIndex);
|
||||
const operator = str.charAt(equalsIndex - 1) === '+' ? '+=' : '=';
|
||||
const firstOperatorCharIndex =
|
||||
operator === '=' ? equalsIndex : equalsIndex - 1;
|
||||
const firstSquareBracketIndex = str
|
||||
.slice(startIndex, firstOperatorCharIndex)
|
||||
.indexOf('[');
|
||||
let nameNode: SubscriptNode | BaseNode<NodeType.VariableName>;
|
||||
|
||||
const variableName = createNode<BaseNode<NodeType.VariableName>>(str, {
|
||||
type: NodeType.VariableName,
|
||||
startIndex,
|
||||
endIndex:
|
||||
firstSquareBracketIndex !== -1
|
||||
? firstSquareBracketIndex
|
||||
: firstOperatorCharIndex,
|
||||
});
|
||||
|
||||
if (firstSquareBracketIndex !== -1) {
|
||||
const index = createNode<BaseNode<NodeType.Word>>(str, {
|
||||
type: NodeType.Word,
|
||||
startIndex: firstSquareBracketIndex + 1,
|
||||
endIndex: firstOperatorCharIndex - 1,
|
||||
});
|
||||
nameNode = createNode<SubscriptNode>(str, {
|
||||
type: NodeType.Subscript,
|
||||
name: variableName,
|
||||
startIndex,
|
||||
endIndex: index.endIndex + 1,
|
||||
children: [index],
|
||||
});
|
||||
} else {
|
||||
nameNode = variableName;
|
||||
}
|
||||
|
||||
const { children, endIndex } = parseConcatenationOrLiteralNode(
|
||||
str,
|
||||
equalsIndex + 1,
|
||||
' ',
|
||||
);
|
||||
return createNode<AssignmentNode>(str, {
|
||||
name: nameNode,
|
||||
startIndex,
|
||||
endIndex,
|
||||
type: NodeType.Assignment,
|
||||
operator,
|
||||
children,
|
||||
complete: children[children.length - 1].complete,
|
||||
});
|
||||
};
|
||||
|
||||
const parseAssignments = (str: string, index: number): AssignmentNode[] => {
|
||||
const variables: AssignmentNode[] = [];
|
||||
let lastVariableEnd = index;
|
||||
while (lastVariableEnd < str.length) {
|
||||
const nextTokenStart = nextWordIndex(str, lastVariableEnd);
|
||||
if (/^[\w[\]]+\+?=.*/.test(str.slice(nextTokenStart))) {
|
||||
const assignmentNode = parseAssignmentNode(str, nextTokenStart);
|
||||
variables.push(assignmentNode);
|
||||
lastVariableEnd = assignmentNode.endIndex;
|
||||
} else {
|
||||
return variables;
|
||||
}
|
||||
}
|
||||
return variables;
|
||||
};
|
||||
|
||||
const parseAssignmentListNodeOrCommandNode = (
|
||||
str: string,
|
||||
startIndex: number,
|
||||
terminalChar: string,
|
||||
): AssignmentListNode | BaseNode<NodeType.Command> => {
|
||||
const assignments = parseAssignments(str, startIndex);
|
||||
if (assignments.length > 0) {
|
||||
const lastAssignment = assignments[assignments.length - 1];
|
||||
const operator = parseOperator(
|
||||
str,
|
||||
nextWordIndex(str, lastAssignment.endIndex),
|
||||
);
|
||||
let command: BaseNode<NodeType.Command> | undefined;
|
||||
if (
|
||||
!operator &&
|
||||
lastAssignment.complete &&
|
||||
lastAssignment.endIndex !== str.length
|
||||
) {
|
||||
command = parseCommand(str, lastAssignment.endIndex, terminalChar);
|
||||
}
|
||||
// if it makes sense to parse a command here do it else return the list
|
||||
return createNode<AssignmentListNode>(str, {
|
||||
type: NodeType.AssignmentList,
|
||||
startIndex,
|
||||
endIndex: command ? command.endIndex : lastAssignment.endIndex,
|
||||
hasCommand: !!command,
|
||||
children: command ? [...assignments, command] : assignments,
|
||||
});
|
||||
}
|
||||
return parseCommand(str, startIndex, terminalChar);
|
||||
};
|
||||
|
||||
const reduceStatements = (
|
||||
str: string,
|
||||
lhs: BaseNode,
|
||||
rhs: BaseNode,
|
||||
type: NodeType,
|
||||
): BaseNode =>
|
||||
createNode(str, {
|
||||
type,
|
||||
startIndex: lhs.startIndex,
|
||||
children: rhs.type === type ? [lhs, ...rhs.children] : [lhs, rhs],
|
||||
endIndex: rhs.endIndex,
|
||||
complete: lhs.complete && rhs.complete,
|
||||
});
|
||||
|
||||
function parseStatement(
|
||||
str: string,
|
||||
index: number,
|
||||
terminalChar: string,
|
||||
): BaseNode {
|
||||
let i = nextWordIndex(str, index);
|
||||
i = i === -1 ? index : i;
|
||||
let statement = null;
|
||||
if (['{', '('].includes(str.charAt(i))) {
|
||||
// Parse compound statement or subshell
|
||||
const isCompound = str.charAt(i) === '{';
|
||||
const endChar = isCompound ? '}' : ')';
|
||||
|
||||
const { statements: children, terminatorIndex } = parseStatements(
|
||||
str,
|
||||
i + 1,
|
||||
endChar,
|
||||
isCompound,
|
||||
);
|
||||
const hasChildren = children.length > 0;
|
||||
const terminated = terminatorIndex !== -1;
|
||||
let endIndex = terminatorIndex + 1;
|
||||
if (!terminated) {
|
||||
endIndex = hasChildren
|
||||
? children[children.length - 1].endIndex
|
||||
: str.length;
|
||||
}
|
||||
statement = createNode(str, {
|
||||
startIndex: i,
|
||||
type: isCompound ? NodeType.CompoundStatement : NodeType.Subshell,
|
||||
endIndex,
|
||||
complete: terminated && hasChildren,
|
||||
children,
|
||||
});
|
||||
} else {
|
||||
// statement = parseAssignmentListNodeOrCommandNode(str, i, terminalChar)
|
||||
statement = parseAssignmentListNodeOrCommandNode(str, i, terminalChar);
|
||||
}
|
||||
|
||||
i = statement.endIndex;
|
||||
const opIndex = nextWordIndex(str, i);
|
||||
const op = opIndex !== -1 && parseOperator(str, opIndex);
|
||||
if (
|
||||
!op ||
|
||||
op === ';' ||
|
||||
op === '&' ||
|
||||
op === '&;' ||
|
||||
(opIndex !== -1 && terminalChar && str.charAt(opIndex) === terminalChar)
|
||||
) {
|
||||
return statement;
|
||||
}
|
||||
|
||||
// Recursively parse rightHandStatement if theres an operator.
|
||||
const rightHandStatement = parseStatement(
|
||||
str,
|
||||
opIndex + op.length,
|
||||
terminalChar,
|
||||
);
|
||||
if (op === '&&' || op === '||') {
|
||||
return reduceStatements(str, statement, rightHandStatement, NodeType.List);
|
||||
}
|
||||
|
||||
if (op === '|' || op === '|&') {
|
||||
if (rightHandStatement.type === NodeType.List) {
|
||||
const [oldFirstChild, ...otherChildren] = rightHandStatement.children;
|
||||
const newFirstChild = reduceStatements(
|
||||
str,
|
||||
statement,
|
||||
oldFirstChild,
|
||||
NodeType.Pipeline,
|
||||
);
|
||||
return createNode(str, {
|
||||
type: NodeType.List,
|
||||
startIndex: newFirstChild.startIndex,
|
||||
children: [newFirstChild, ...otherChildren],
|
||||
endIndex: rightHandStatement.endIndex,
|
||||
complete: newFirstChild.complete && rightHandStatement.complete,
|
||||
});
|
||||
}
|
||||
return reduceStatements(
|
||||
str,
|
||||
statement,
|
||||
rightHandStatement,
|
||||
NodeType.Pipeline,
|
||||
);
|
||||
}
|
||||
return statement;
|
||||
}
|
||||
|
||||
export const printTree = (root: BaseNode) => {
|
||||
const getNodeText = (node: BaseNode, level = 0) => {
|
||||
const indent = ' '.repeat(level);
|
||||
let nodeText = `${indent}${node.type} [${node.startIndex}, ${node.endIndex}] - ${node.text}`;
|
||||
const childrenText = node.children
|
||||
.map((child) => getNodeText(child, level + 1))
|
||||
.join('\n');
|
||||
if (childrenText) {
|
||||
nodeText += `\n${childrenText}`;
|
||||
}
|
||||
if (!node.complete) {
|
||||
nodeText += `\n${indent}INCOMPLETE`;
|
||||
}
|
||||
return nodeText;
|
||||
};
|
||||
console.log(getNodeText(root));
|
||||
};
|
||||
|
||||
export const parse = (str: string): BaseNode =>
|
||||
createNode<BaseNode<NodeType.Program>>(str, {
|
||||
startIndex: 0,
|
||||
type: NodeType.Program,
|
||||
children: parseStatements(str, 0, '').statements,
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { deepStrictEqual } from 'node:assert';
|
||||
import { getCommand, Command } from "../command";
|
||||
|
||||
suite("fig/shell-parser/ getCommand", () => {
|
||||
const aliases = {
|
||||
woman: "man",
|
||||
quote: "'q'",
|
||||
g: "git",
|
||||
};
|
||||
const getTokenText = (command: Command | null) => command?.tokens.map((token) => token.text) ?? [];
|
||||
|
||||
test("works without matching aliases", () => {
|
||||
deepStrictEqual(getTokenText(getCommand("git co ", {})), ["git", "co", ""]);
|
||||
deepStrictEqual(getTokenText(getCommand("git co ", aliases)), ["git", "co", ""]);
|
||||
deepStrictEqual(getTokenText(getCommand("woman ", {})), ["woman", ""]);
|
||||
deepStrictEqual(getTokenText(getCommand("another string ", aliases)), [
|
||||
"another",
|
||||
"string",
|
||||
"",
|
||||
]);
|
||||
});
|
||||
|
||||
test("works with regular aliases", () => {
|
||||
// Don't change a single token.
|
||||
deepStrictEqual(getTokenText(getCommand("woman", aliases)), ["woman"]);
|
||||
// Change first token if length > 1.
|
||||
deepStrictEqual(getTokenText(getCommand("woman ", aliases)), ["man", ""]);
|
||||
// Don't change later tokens.
|
||||
deepStrictEqual(getTokenText(getCommand("man woman ", aliases)), ["man", "woman", ""]);
|
||||
// Handle quotes
|
||||
deepStrictEqual(getTokenText(getCommand("quote ", aliases)), ["q", ""]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { parse } from '../parser';
|
||||
import { strictEqual } from 'node:assert';
|
||||
|
||||
function parseCommand(command: string): string {
|
||||
return JSON.stringify(parse(command), null, ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param filePath The path to the file to parse
|
||||
* @param nameComment The first character of each title line
|
||||
*/
|
||||
function getData(
|
||||
filePath: string,
|
||||
nameComment: string,
|
||||
): [name: string, value: string][] {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
fs.writeFileSync(filePath, '');
|
||||
return [];
|
||||
}
|
||||
return fs
|
||||
.readFileSync(filePath, { encoding: 'utf8' })
|
||||
.replaceAll('\r\n', '\n')
|
||||
.split('\n\n')
|
||||
.map((testCase) => {
|
||||
const firstNewline = testCase.indexOf('\n');
|
||||
const title = testCase.slice(0, firstNewline);
|
||||
const block = testCase.slice(firstNewline);
|
||||
return [title.slice(nameComment.length).trim(), block.trim()];
|
||||
});
|
||||
}
|
||||
|
||||
// function outputNewFile(
|
||||
// filePath: string,
|
||||
// nameComment: string,
|
||||
// data: [name: string, value: string][],
|
||||
// ) {
|
||||
// fs.writeFileSync(
|
||||
// filePath,
|
||||
// data.reduce(
|
||||
// (previous, current, index) =>
|
||||
// `${previous}${index > 0 ? '\n\n' : ''}${nameComment} ${current[0]}\n${current[1]
|
||||
// }`,
|
||||
// '',
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// function notIncludedIn<K>(setA: Set<K>, setB: Set<K>): K[] {
|
||||
// const notIncluded: K[] = [];
|
||||
// for (const v of setA) {
|
||||
// if (!setB.has(v)) notIncluded.push(v);
|
||||
// }
|
||||
// return notIncluded;
|
||||
// }
|
||||
|
||||
// function mapKeysDiff<K, V>(mapA: Map<K, V>, mapB: Map<K, V>) {
|
||||
// const keysA = new Set(mapA.keys());
|
||||
// const keysB = new Set(mapB.keys());
|
||||
// return [
|
||||
// notIncludedIn(keysA, keysB), // keys of A not included in B
|
||||
// notIncludedIn(keysB, keysA), // keys of B not included in A
|
||||
// ];
|
||||
// }
|
||||
|
||||
suite('fig/shell-parser/ fixtures', () => {
|
||||
const fixturesPath = path.join(__dirname, '../../../../fixtures/shell-parser');
|
||||
const fixtures = fs.readdirSync(fixturesPath);
|
||||
for (const fixture of fixtures) {
|
||||
// console.log('fixture', fixture);
|
||||
suite(fixture, () => {
|
||||
const inputFile = path.join(fixturesPath, fixture, 'input.sh');
|
||||
const outputFile = path.join(fixturesPath, fixture, 'output.txt');
|
||||
const inputData = new Map(getData(inputFile, '###'));
|
||||
const outputData = new Map(getData(outputFile, '//'));
|
||||
|
||||
// clean diffs and regenerate files if required.
|
||||
// if (!process.env.NO_FIXTURES_EDIT) {
|
||||
// const [newInputs, extraOutputs] = mapKeysDiff(inputData, outputData);
|
||||
// extraOutputs.forEach((v) => outputData.delete(v));
|
||||
// newInputs.forEach((v) =>
|
||||
// outputData.set(v, parseCommand(inputData.get(v) ?? '')),
|
||||
// );
|
||||
// if (extraOutputs.length || newInputs.length) {
|
||||
// outputNewFile(outputFile, '//', [...outputData.entries()]);
|
||||
// }
|
||||
// }
|
||||
|
||||
for (const [caseName, input] of inputData.entries()) {
|
||||
if (caseName) {
|
||||
test(caseName, () => {
|
||||
const output = outputData.get(caseName);
|
||||
strictEqual(parseCommand(input ?? ''), output);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -19,6 +19,10 @@ import { getPwshGlobals } from './shell/pwsh';
|
||||
import { getTokenType, TokenType } from './tokens';
|
||||
import { PathExecutableCache } from './env/pathExecutableCache';
|
||||
import { getFriendlyResourcePath } from './helpers/uri';
|
||||
import { ArgumentParserResult, parseArguments } from './fig/autocomplete-parser/parseArguments';
|
||||
import { getCommand, type Command } from './fig/shell-parser/command';
|
||||
import { SuggestionFlag } from './fig/shared/utils';
|
||||
import { spawnHelper } from './shell/common';
|
||||
|
||||
// TODO: remove once API is finalized
|
||||
export const enum TerminalShellType {
|
||||
@@ -102,11 +106,10 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
return;
|
||||
}
|
||||
const commands = [...commandsInPath.completionResources, ...shellGlobals];
|
||||
|
||||
const prefix = getPrefix(terminalContext.commandLine, terminalContext.cursorPosition);
|
||||
const pathSeparator = isWindows ? '\\' : '/';
|
||||
const tokenType = getTokenType(terminalContext, shellType);
|
||||
const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, tokenType, terminal.shellIntegration?.cwd, token);
|
||||
const result = await getCompletionItemsFromSpecs(availableSpecs, terminalContext, commands, prefix, tokenType, terminal.shellIntegration?.cwd, getEnvAsRecord(terminal.shellIntegration?.env), terminal.name, token);
|
||||
if (terminal.shellIntegration?.env) {
|
||||
const homeDirCompletion = result.items.find(i => i.label === '~');
|
||||
if (homeDirCompletion && terminal.shellIntegration.env.HOME) {
|
||||
@@ -226,13 +229,121 @@ export function asArray<T>(x: T | T[]): T[] {
|
||||
return Array.isArray(x) ? x : [x];
|
||||
}
|
||||
|
||||
export type SpecArg = Fig.Arg | Fig.Suggestion | Fig.Option | string;
|
||||
|
||||
export async function collectCompletionItemResult(command: Command, parsedArguments: ArgumentParserResult, prefix: string, terminalContext: any, items: vscode.TerminalCompletionItem[]): Promise<{ filesRequested: boolean; foldersRequested: boolean } | undefined> {
|
||||
let filesRequested = false;
|
||||
let foldersRequested = false;
|
||||
|
||||
const addSuggestions = async (specArgs: SpecArg[] | Record<string, SpecArg> | undefined, kind: vscode.TerminalCompletionItemKind, parsedArguments?: ArgumentParserResult) => {
|
||||
if (kind === vscode.TerminalCompletionItemKind.Argument && parsedArguments?.currentArg?.generators) {
|
||||
const generators = parsedArguments.currentArg.generators;
|
||||
for (const generator of generators) {
|
||||
if (generator.template) {
|
||||
const templates = Array.isArray(generator.template) ? generator.template : [generator.template];
|
||||
for (const template of templates) {
|
||||
if (template === 'filepaths') {
|
||||
filesRequested = true;
|
||||
} else if (template === 'folders') {
|
||||
foldersRequested = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (generator.script && Array.isArray(generator.script) && generator.postProcess) {
|
||||
let output;
|
||||
try {
|
||||
output = await spawnHelper(generator.script[0], generator.script.slice(1), { encoding: 'utf8' }); //, { options });
|
||||
} catch {
|
||||
//ignore
|
||||
}
|
||||
if (!output) {
|
||||
continue;
|
||||
}
|
||||
const generatedItems = generator.postProcess(output, command.tokens.map(e => e.text));
|
||||
for (const generatedItem of generatedItems) {
|
||||
if (!generatedItem) {
|
||||
continue;
|
||||
}
|
||||
const suggestionLabels = getLabel(generatedItem);
|
||||
if (!suggestionLabels) {
|
||||
continue;
|
||||
}
|
||||
for (const label of suggestionLabels) {
|
||||
items.push(createCompletionItem(
|
||||
terminalContext.cursorPosition,
|
||||
prefix,
|
||||
{ label },
|
||||
undefined,
|
||||
typeof generatedItem === 'string' ? generatedItem : generatedItem.description,
|
||||
kind
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!specArgs) {
|
||||
return { filesRequested, foldersRequested };
|
||||
}
|
||||
|
||||
if (Array.isArray(specArgs)) {
|
||||
for (const item of specArgs) {
|
||||
const suggestionLabels = getLabel(item);
|
||||
if (!suggestionLabels) {
|
||||
continue;
|
||||
}
|
||||
for (const label of suggestionLabels) {
|
||||
items.push(
|
||||
createCompletionItem(
|
||||
terminalContext.cursorPosition,
|
||||
prefix,
|
||||
{ label },
|
||||
undefined,
|
||||
typeof item === 'string' ? item : item.description,
|
||||
kind
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const [label, item] of Object.entries(specArgs)) {
|
||||
items.push(
|
||||
createCompletionItem(
|
||||
terminalContext.cursorPosition,
|
||||
prefix,
|
||||
{ label },
|
||||
undefined,
|
||||
typeof item === 'string' ? item : item.description,
|
||||
kind
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (parsedArguments.suggestionFlags & SuggestionFlag.Args) {
|
||||
await addSuggestions(parsedArguments.currentArg?.suggestions, vscode.TerminalCompletionItemKind.Argument, parsedArguments);
|
||||
}
|
||||
if (parsedArguments.suggestionFlags & SuggestionFlag.Subcommands) {
|
||||
await addSuggestions(parsedArguments.completionObj.subcommands, vscode.TerminalCompletionItemKind.Method);
|
||||
}
|
||||
if (parsedArguments.suggestionFlags & SuggestionFlag.Options) {
|
||||
await addSuggestions(parsedArguments.completionObj.options, vscode.TerminalCompletionItemKind.Flag);
|
||||
}
|
||||
|
||||
return { filesRequested, foldersRequested };
|
||||
}
|
||||
|
||||
export async function getCompletionItemsFromSpecs(
|
||||
specs: Fig.Spec[],
|
||||
terminalContext: { commandLine: string; cursorPosition: number },
|
||||
availableCommands: ICompletionResource[],
|
||||
prefix: string,
|
||||
tokenType: TokenType,
|
||||
shellIntegrationCwd?: vscode.Uri,
|
||||
shellIntegrationCwd: vscode.Uri | undefined,
|
||||
env: Record<string, string>,
|
||||
name: string,
|
||||
token?: vscode.CancellationToken
|
||||
): Promise<{ items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; cwd?: vscode.Uri }> {
|
||||
const items: vscode.TerminalCompletionItem[] = [];
|
||||
@@ -249,7 +360,6 @@ export async function getCompletionItemsFromSpecs(
|
||||
}
|
||||
}
|
||||
|
||||
let specificItemsProvided = false;
|
||||
for (const spec of specs) {
|
||||
const specLabels = getLabel(spec);
|
||||
|
||||
@@ -285,27 +395,24 @@ export async function getCompletionItemsFromSpecs(
|
||||
continue;
|
||||
}
|
||||
|
||||
const optionsCompletionResult = handleOptions(specLabel, spec, terminalContext, precedingText, prefix);
|
||||
if (optionsCompletionResult) {
|
||||
items.push(...optionsCompletionResult.items);
|
||||
filesRequested ||= optionsCompletionResult.filesRequested;
|
||||
foldersRequested ||= optionsCompletionResult.foldersRequested;
|
||||
specificItemsProvided ||= optionsCompletionResult.items.length > 0;
|
||||
const command = getCommand(terminalContext.commandLine, {}, terminalContext.cursorPosition);
|
||||
if (!command || !shellIntegrationCwd) {
|
||||
continue;
|
||||
}
|
||||
if (!optionsCompletionResult?.isOptionArg) {
|
||||
const argsCompletionResult = handleArguments(specLabel, spec, terminalContext, precedingText);
|
||||
if (argsCompletionResult) {
|
||||
items.push(...argsCompletionResult.items);
|
||||
filesRequested ||= argsCompletionResult.filesRequested;
|
||||
foldersRequested ||= argsCompletionResult.foldersRequested;
|
||||
specificItemsProvided ||= argsCompletionResult.items.length > 0;
|
||||
}
|
||||
const parsedArguments: ArgumentParserResult = await parseArguments(
|
||||
command,
|
||||
{ environmentVariables: env, currentWorkingDirectory: shellIntegrationCwd.fsPath, sshPrefix: '', currentProcess: name, /* TODO: pass in aliases */ },
|
||||
spec,
|
||||
);
|
||||
|
||||
const completionItemResult = await collectCompletionItemResult(command, parsedArguments, prefix, terminalContext, items);
|
||||
if (completionItemResult) {
|
||||
filesRequested ||= completionItemResult.filesRequested;
|
||||
foldersRequested ||= completionItemResult.foldersRequested;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (tokenType === TokenType.Command) {
|
||||
// Include builitin/available commands in the results
|
||||
const labels = new Set(items.map((i) => i.label));
|
||||
@@ -316,15 +423,10 @@ export async function getCompletionItemsFromSpecs(
|
||||
}
|
||||
filesRequested = true;
|
||||
foldersRequested = true;
|
||||
} else {
|
||||
const shouldShowResourceCompletions =
|
||||
!specificItemsProvided &&
|
||||
!filesRequested &&
|
||||
!foldersRequested;
|
||||
if (shouldShowResourceCompletions) {
|
||||
filesRequested = true;
|
||||
foldersRequested = true;
|
||||
}
|
||||
} else if (!items.length && !filesRequested && !foldersRequested) {
|
||||
// Not a command and no specific args or options were provided, so show resources
|
||||
filesRequested = true;
|
||||
foldersRequested = true;
|
||||
}
|
||||
|
||||
let cwd: vscode.Uri | undefined;
|
||||
@@ -335,133 +437,6 @@ export async function getCompletionItemsFromSpecs(
|
||||
return { items, filesRequested, foldersRequested, cwd };
|
||||
}
|
||||
|
||||
function handleArguments(specLabel: string, spec: Fig.Spec, terminalContext: { commandLine: string; cursorPosition: number }, precedingText: string): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean } | undefined {
|
||||
let args;
|
||||
if ('args' in spec && spec.args && asArray(spec.args)) {
|
||||
args = asArray(spec.args);
|
||||
}
|
||||
const expectedText = `${specLabel} `;
|
||||
|
||||
if (!precedingText.includes(expectedText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPrefix = precedingText.slice(precedingText.lastIndexOf(expectedText) + expectedText.length);
|
||||
const argsCompletions = getCompletionItemsFromArgs(args, currentPrefix, terminalContext);
|
||||
|
||||
if (!argsCompletions) {
|
||||
return;
|
||||
}
|
||||
|
||||
return argsCompletions;
|
||||
}
|
||||
|
||||
function handleOptions(specLabel: string, spec: Fig.Spec, terminalContext: { commandLine: string; cursorPosition: number }, precedingText: string, prefix: string): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean; isOptionArg: boolean } | undefined {
|
||||
let options;
|
||||
if ('options' in spec && spec.options) {
|
||||
options = spec.options;
|
||||
}
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optionItems: vscode.TerminalCompletionItem[] = [];
|
||||
|
||||
for (const option of options) {
|
||||
const optionLabels = getLabel(option);
|
||||
|
||||
if (!optionLabels) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const optionLabel of optionLabels) {
|
||||
if (
|
||||
// Already includes this option
|
||||
optionItems.find((i) => i.label === optionLabel)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
optionItems.push(
|
||||
createCompletionItem(
|
||||
terminalContext.cursorPosition,
|
||||
prefix,
|
||||
{ label: optionLabel },
|
||||
option.description,
|
||||
undefined,
|
||||
vscode.TerminalCompletionItemKind.Flag
|
||||
)
|
||||
);
|
||||
|
||||
const expectedText = `${specLabel} ${optionLabel} `;
|
||||
if (!precedingText.includes(expectedText)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentPrefix = precedingText.slice(precedingText.lastIndexOf(expectedText) + expectedText.length);
|
||||
const argsCompletions = getCompletionItemsFromArgs(option.args, currentPrefix, terminalContext);
|
||||
|
||||
if (argsCompletions) {
|
||||
return { items: argsCompletions.items, filesRequested: argsCompletions.filesRequested, foldersRequested: argsCompletions.foldersRequested, isOptionArg: true };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { items: optionItems, filesRequested: false, foldersRequested: false, isOptionArg: false };
|
||||
}
|
||||
|
||||
|
||||
function getCompletionItemsFromArgs(args: Fig.SingleOrArray<Fig.Arg> | undefined, currentPrefix: string, terminalContext: { commandLine: string; cursorPosition: number }): { items: vscode.TerminalCompletionItem[]; filesRequested: boolean; foldersRequested: boolean } | undefined {
|
||||
if (!args) {
|
||||
return;
|
||||
}
|
||||
|
||||
let items: vscode.TerminalCompletionItem[] = [];
|
||||
let filesRequested = false;
|
||||
let foldersRequested = false;
|
||||
for (const arg of asArray(args)) {
|
||||
if (!arg) {
|
||||
continue;
|
||||
}
|
||||
if (arg.template) {
|
||||
if (Array.isArray(arg.template) ? arg.template.includes('filepaths') : arg.template === 'filepaths') {
|
||||
filesRequested = true;
|
||||
}
|
||||
if (Array.isArray(arg.template) ? arg.template.includes('folders') : arg.template === 'folders') {
|
||||
foldersRequested = true;
|
||||
}
|
||||
}
|
||||
if (arg.suggestions?.length) {
|
||||
// there are specific suggestions to show
|
||||
items = [];
|
||||
for (const suggestion of arg.suggestions) {
|
||||
const suggestionLabels = getLabel(suggestion);
|
||||
if (!suggestionLabels) {
|
||||
continue;
|
||||
}
|
||||
const twoWordsBefore = terminalContext.commandLine.slice(0, terminalContext.cursorPosition).split(' ').at(-2);
|
||||
const wordBefore = terminalContext.commandLine.slice(0, terminalContext.cursorPosition).split(' ').at(-1);
|
||||
for (const suggestionLabel of suggestionLabels) {
|
||||
if (items.find(i => i.label === suggestionLabel)) {
|
||||
continue;
|
||||
}
|
||||
if (!arg.isVariadic && twoWordsBefore === suggestionLabel && wordBefore?.trim() === '') {
|
||||
return { items: [], filesRequested, foldersRequested };
|
||||
}
|
||||
if (suggestionLabel && suggestionLabel.startsWith(currentPrefix.trim())) {
|
||||
const description = typeof suggestion !== 'string' ? suggestion.description : '';
|
||||
items.push(createCompletionItem(terminalContext.cursorPosition, wordBefore ?? '', { label: suggestionLabel }, description, undefined, vscode.TerminalCompletionItemKind.Argument));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (items.length) {
|
||||
return { items, filesRequested, foldersRequested };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { items, filesRequested, foldersRequested };
|
||||
}
|
||||
|
||||
function getShell(shellType: TerminalShellType): string | undefined {
|
||||
switch (shellType) {
|
||||
case TerminalShellType.Bash:
|
||||
@@ -481,3 +456,15 @@ function getShell(shellType: TerminalShellType): string | undefined {
|
||||
function removeAnyFileExtension(label: string): string {
|
||||
return label.replace(/\.[a-zA-Z0-9!#\$%&'\(\)\-@\^_`{}~\+,;=\[\]]+$/, '');
|
||||
}
|
||||
|
||||
function getEnvAsRecord(shellIntegrationEnv: { [key: string]: string | undefined } | undefined): Record<string, string> {
|
||||
const env: Record<string, string> = {};
|
||||
if (shellIntegrationEnv) {
|
||||
for (const [key, value] of Object.entries(shellIntegrationEnv)) {
|
||||
if (typeof value === 'string') {
|
||||
env[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import 'mocha';
|
||||
import cdSpec from '../../completions/cd';
|
||||
import { testPaths, type ISuiteSpec } from '../helpers';
|
||||
|
||||
const expectedCompletions = ['~', '-'];
|
||||
|
||||
export const cdTestSuiteSpec: ISuiteSpec = {
|
||||
name: 'cd',
|
||||
completionSpecs: cdSpec,
|
||||
@@ -22,21 +24,21 @@ export const cdTestSuiteSpec: ISuiteSpec = {
|
||||
{ input: 'cd|', expectedCompletions: ['cd'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
|
||||
|
||||
// Basic arguments
|
||||
{ input: 'cd |', expectedCompletions: ['~', '-'], expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd -|', expectedCompletions: ['-'], expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd ~|', expectedCompletions: ['~'], expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd |', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd -|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd ~|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
|
||||
// Relative paths
|
||||
{ input: 'cd c|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd child|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd .|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd ./|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd ./child|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd ..|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd c|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd child|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd .|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd ./|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd ./child|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
{ input: 'cd ..|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
|
||||
// Relative directories (changes cwd due to /)
|
||||
{ input: 'cd child/|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdChild } },
|
||||
{ input: 'cd ../|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdParent } },
|
||||
{ input: 'cd ../sibling|', expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdParent } },
|
||||
{ input: 'cd child/|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdChild } },
|
||||
{ input: 'cd ../|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdParent } },
|
||||
{ input: 'cd ../sibling|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdParent } },
|
||||
]
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ export function createCodeTestSpecs(executable: string): ITestSpec[] {
|
||||
{ input: `${executable} --list-extensions |`, expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
|
||||
{ input: `${executable} --show-versions |`, expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
|
||||
{ input: `${executable} --category |`, expectedCompletions: categoryOptions },
|
||||
{ input: `${executable} --category a|`, expectedCompletions: categoryOptions.filter(c => c.startsWith('a')) },
|
||||
{ input: `${executable} --category a|`, expectedCompletions: categoryOptions },
|
||||
|
||||
// Middle of command
|
||||
{ input: `${executable} | --locale`, expectedCompletions: codeSpecOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'mocha';
|
||||
import { testPaths, type ISuiteSpec } from '../../helpers';
|
||||
import gitSpec from '../../../completions/upstream/git';
|
||||
|
||||
// const gitSubcommandAndArgs = ['--bare', '--exec-path', '--git-dir', '--help', '--html-path', '--info-path', '--man-path', '--namespace', '--no-optional-locks', '--no-pager', '--no-replace-objects', '--paginate', '--version', '--work-tree', '-C', '-c', '-p', 'add', 'apply', 'archive', 'bisect', 'blame', 'branch', 'checkout', 'cherry-pick', 'clean', 'clone', 'commit', 'config', 'daemon', 'diff', 'fetch', 'grep', 'init', 'log', 'ls-remote', 'merge', 'mergetool', 'mv', 'pull', 'push', 'rebase', 'reflog', 'remote', 'reset', 'restore', 'revert', 'rm', 'show', 'stage', 'stash', 'status', 'submodule', 'switch', 'tag', 'worktree'];
|
||||
// const gitCommitArgs = ['--', '--all', '--allow-empty', '--allow-empty-message', '--amend', '--author', '--branch', '--cleanup', '--date', '--dry-run', '--edit', '--file', '--fixup', '--gpg-sign', '--include', '--long', '--message', '--no-edit', '--no-gpg-sign', '--no-post-rewrite', '--no-signoff', '--no-status', '--no-verify', '--null', '--only', '--patch', '--pathspec-file-nul', '--pathspec-from-file', '--porcelain', '--quiet', '--reedit-message', '--reset-author', '--reuse-message', '--short', '--signoff', '--squash', '--status', '--template', '--untracked-files', '--verbose', '-C', '-F', '-S', '-a', '-am', '-c', '-e', '-i', '-m', '-n', '-o', '-p', '-q', '-s', '-t', '-u', '-v', '-z'];
|
||||
// const gitMergeArgs = ['-', '--abort', '--allow-unrelated-histories', '--autostash', '--cleanup', '--commit', '--continue', '--edit', '--ff', '--ff-only', '--file', '--gpg-sign', '--log', '--no-autostash', '--no-commit', '--no-edit', '--no-ff', '--no-gpg-sign', '--no-log', '--no-overwrite-ignore', '--no-progress', '--no-rerere-autoupdate', '--no-signoff', '--no-squash', '--no-stat', '--no-summary', '--no-verify', '--no-verify-signatures', '--overwrite-ignore', '--progress', '--quiet', '--quit', '--rerere-autoupdate', '--signoff', '--squash', '--stat', '--strategy', '--strategy-option', '--summary', '--verbose', '--verify-signatures', '-F', '-S', '-X', '-e', '-m', '-n', '-q', '-s'];
|
||||
// const gitAddArgs = ['--', '--all', '--chmod', '--dry-run', '--edit', '--force', '--ignore-errors', '--ignore-missing', '--ignore-removal', '--intent-to-add', '--interactive', '--no-all', '--no-ignore-removal', '--no-warn-embedded-repo', '--patch', '--pathspec-file-nul', '--pathspec-from-file', '--refresh', '--renormalize', '--update', '--verbose', '-A', '-N', '-e', '-f', '-i', '-n', '-p', '-u', '-v'];
|
||||
export const gitTestSuiteSpec: ISuiteSpec = {
|
||||
name: 'git',
|
||||
completionSpecs: gitSpec,
|
||||
availableCommands: 'git',
|
||||
testSpecs: [
|
||||
// Empty input
|
||||
{ input: '|', expectedCompletions: ['git'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
|
||||
|
||||
// Typing the command
|
||||
{ input: 'g|', expectedCompletions: ['git'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
|
||||
{ input: 'gi|', expectedCompletions: ['git'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
|
||||
{ input: 'git|', expectedCompletions: ['git'], expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
|
||||
|
||||
// TODO: These currently read .gitconfig and end up returning different results depending on the system
|
||||
// Basic options
|
||||
// { input: 'git |', expectedCompletions: gitSubcommandAndArgs },
|
||||
|
||||
// Complex options
|
||||
// { input: 'git add |', expectedCompletions: gitAddArgs, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwd } },
|
||||
// { input: 'git commit |', expectedCompletions: gitCommitArgs },
|
||||
// { input: 'git merge |', expectedCompletions: gitMergeArgs }
|
||||
],
|
||||
};
|
||||
@@ -66,12 +66,10 @@ export const lsTestSuiteSpec: ISuiteSpec = {
|
||||
|
||||
// Basic options
|
||||
// TODO: The spec wants file paths and folders (which seems like it should only be folders),
|
||||
// but neither are requested https://github.com/microsoft/vscode/issues/239606
|
||||
{ input: 'ls |', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
|
||||
{ input: 'ls -|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
|
||||
|
||||
// Filtering options should request all options so client side can filter
|
||||
{ input: 'ls -a|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwd } },
|
||||
{ input: 'ls -a|', expectedCompletions: allOptions },
|
||||
|
||||
// Duplicate option
|
||||
// TODO: Duplicate options should not be presented https://github.com/microsoft/vscode/issues/239607
|
||||
|
||||
@@ -18,6 +18,7 @@ import { mkdirTestSuiteSpec } from './completions/upstream/mkdir.test';
|
||||
import { rmTestSuiteSpec } from './completions/upstream/rm.test';
|
||||
import { rmdirTestSuiteSpec } from './completions/upstream/rmdir.test';
|
||||
import { touchTestSuiteSpec } from './completions/upstream/touch.test';
|
||||
import { gitTestSuiteSpec } from './completions/upstream/git.test';
|
||||
import { osIsWindows } from '../helpers/os';
|
||||
import codeCompletionSpec from '../completions/code';
|
||||
import { figGenericTestSuites } from './fig.test';
|
||||
@@ -49,6 +50,7 @@ const testSpecs2: ISuiteSpec[] = [
|
||||
rmTestSuiteSpec,
|
||||
rmdirTestSuiteSpec,
|
||||
touchTestSuiteSpec,
|
||||
gitTestSuiteSpec,
|
||||
];
|
||||
|
||||
if (osIsWindows()) {
|
||||
@@ -97,13 +99,15 @@ suite('Terminal Suggest', () => {
|
||||
availableCommands.map(c => { return { label: c }; }),
|
||||
prefix,
|
||||
getTokenType(terminalContext, undefined),
|
||||
testPaths.cwd
|
||||
testPaths.cwd,
|
||||
{},
|
||||
'testName'
|
||||
);
|
||||
deepStrictEqual(result.items.map(i => i.label).sort(), (testSpec.expectedCompletions ?? []).sort());
|
||||
strictEqual(result.filesRequested, filesRequested);
|
||||
strictEqual(result.foldersRequested, foldersRequested);
|
||||
strictEqual(result.filesRequested, filesRequested, 'Files requested different than expected, got: ' + result.filesRequested);
|
||||
strictEqual(result.foldersRequested, foldersRequested, 'Folders requested different than expected, got: ' + result.foldersRequested);
|
||||
if (testSpec.expectedResourceRequests?.cwd) {
|
||||
strictEqual(result.cwd?.fsPath, testSpec.expectedResourceRequests.cwd.fsPath);
|
||||
strictEqual(result.cwd?.fsPath, testSpec.expectedResourceRequests.cwd.fsPath, 'Non matching cwd');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,15 +2,11 @@
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out",
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"target": "es2020",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
// Needed to suppress warnings in upstream completions
|
||||
"noImplicitReturns": false,
|
||||
|
||||
Reference in New Issue
Block a user