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:
Megan Rogge
2025-02-08 09:40:33 -06:00
committed by GitHub
parent 18a64b37b0
commit 6dbde2a3ed
42 changed files with 19420 additions and 1447 deletions

View File

@@ -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

View File

@@ -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
}

View File

@@ -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\`)

View File

@@ -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": []
}
]
}
]
}
]
}
]
}

View File

@@ -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

View File

@@ -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)"

View File

@@ -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": []
}
]
}
]
}
]
}
]
}
]
}
]
}

View File

@@ -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

View File

@@ -22,7 +22,6 @@
"compile": "npx gulp compile-extension:terminal-suggest",
"watch": "npx gulp watch-extension:terminal-suggest"
},
"main": "./out/terminalSuggestMain",
"activationEvents": [
"onTerminalCompletionsRequested"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@ export const upstreamSpecs = [
'rm',
'rmdir',
'touch',
'git'
];

View 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.

View File

@@ -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);
// };

View File

@@ -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

View File

@@ -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) : [],
};
}

View File

@@ -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,
};

View File

@@ -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);
};

View File

@@ -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);
}

View File

@@ -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,
};

View File

@@ -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',
}

View File

@@ -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}`;
}
}
});
};

View 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 };

View 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
>;

View 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);
});
});

View 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.');
}

View 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),
);
};

View File

@@ -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');

View File

@@ -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';

View 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,
});

View 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 { 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", ""]);
});
});

View File

@@ -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);
});
}
}
});
}
});

View File

@@ -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;
}

View File

@@ -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 } },
]
};

View File

@@ -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 } },

View File

@@ -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 }
],
};

View File

@@ -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

View File

@@ -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');
}
});
}

View File

@@ -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,