+4
.gitignore
+4
.gitignore
+128
-105
Cargo.lock
+128
-105
Cargo.lock
···
74
74
75
75
[[package]]
76
76
name = "alphanumeric-sort"
77
-
version = "1.5.4"
77
+
version = "1.5.5"
78
78
source = "registry+https://github.com/rust-lang/crates.io-index"
79
-
checksum = "ab31f79a61d3c25cae1d6734a386662e5cf1b6ba5a9720c135b609273c058c15"
79
+
checksum = "774ffdfeac16e9b4d75e41225dc2545d9c2082a0634b5d7f6f70e168546eecb1"
80
80
81
81
[[package]]
82
82
name = "android_system_properties"
···
197
197
"futures-core",
198
198
"pin-project-lite",
199
199
"tokio",
200
+
]
201
+
202
+
[[package]]
203
+
name = "async-lock"
204
+
version = "3.4.1"
205
+
source = "registry+https://github.com/rust-lang/crates.io-index"
206
+
checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc"
207
+
dependencies = [
208
+
"event-listener",
209
+
"event-listener-strategy",
210
+
"pin-project-lite",
200
211
]
201
212
202
213
[[package]]
···
403
414
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
404
415
dependencies = [
405
416
"memchr",
406
-
"regex-automata",
407
417
"serde",
408
418
]
409
419
···
706
716
version = "1.0.4"
707
717
source = "registry+https://github.com/rust-lang/crates.io-index"
708
718
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
719
+
720
+
[[package]]
721
+
name = "compile-time"
722
+
version = "0.2.0"
723
+
source = "registry+https://github.com/rust-lang/crates.io-index"
724
+
checksum = "e55ede5279d4d7c528906853743abeb26353ae1e6c440fcd6d18316c2c2dd903"
725
+
dependencies = [
726
+
"once_cell",
727
+
"proc-macro2",
728
+
"quote",
729
+
"rustc_version",
730
+
"semver",
731
+
"time",
732
+
]
709
733
710
734
[[package]]
711
735
name = "compression-codecs"
···
725
749
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
726
750
727
751
[[package]]
752
+
name = "concurrent-queue"
753
+
version = "2.5.0"
754
+
source = "registry+https://github.com/rust-lang/crates.io-index"
755
+
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
756
+
dependencies = [
757
+
"crossbeam-utils",
758
+
]
759
+
760
+
[[package]]
728
761
name = "console"
729
762
version = "0.15.11"
730
763
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1313
1346
]
1314
1347
1315
1348
[[package]]
1349
+
name = "event-listener"
1350
+
version = "5.4.1"
1351
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1352
+
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
1353
+
dependencies = [
1354
+
"concurrent-queue",
1355
+
"parking",
1356
+
"pin-project-lite",
1357
+
]
1358
+
1359
+
[[package]]
1360
+
name = "event-listener-strategy"
1361
+
version = "0.5.4"
1362
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1363
+
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
1364
+
dependencies = [
1365
+
"event-listener",
1366
+
"pin-project-lite",
1367
+
]
1368
+
1369
+
[[package]]
1316
1370
name = "fancy-regex"
1317
1371
version = "0.16.2"
1318
1372
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1341
1395
dependencies = [
1342
1396
"ansi_term",
1343
1397
"anyhow",
1398
+
"async-lock",
1399
+
"compile-time",
1344
1400
"console_error_panic_hook",
1345
1401
"futures",
1346
1402
"getrandom 0.3.4",
···
1348
1404
"jacquard-repo",
1349
1405
"js-sys",
1350
1406
"miette",
1407
+
"nu-cmd-base",
1351
1408
"nu-cmd-extra",
1352
1409
"nu-cmd-lang",
1353
1410
"nu-command",
1354
1411
"nu-engine",
1412
+
"nu-glob",
1355
1413
"nu-parser",
1414
+
"nu-path",
1356
1415
"nu-protocol",
1357
1416
"rapidhash",
1358
1417
"reqwest",
1418
+
"rust-embed",
1359
1419
"scc",
1360
1420
"serde",
1361
1421
"serde_ipld_dagcbor",
···
1684
1744
version = "0.3.3"
1685
1745
source = "registry+https://github.com/rust-lang/crates.io-index"
1686
1746
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
1747
+
1748
+
[[package]]
1749
+
name = "globset"
1750
+
version = "0.4.18"
1751
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1752
+
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
1753
+
dependencies = [
1754
+
"aho-corasick",
1755
+
"bstr",
1756
+
"log",
1757
+
"regex-automata",
1758
+
"regex-syntax",
1759
+
]
1687
1760
1688
1761
[[package]]
1689
1762
name = "gloo-storage"
···
2971
3044
2972
3045
[[package]]
2973
3046
name = "nu-cmd-base"
2974
-
version = "0.109.1"
2975
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2976
-
checksum = "2460ee389a43b935aa18ef5ed9fa8275bdf617e8c05eba7c2b82f92effd2132b"
3047
+
version = "0.109.2"
3048
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
2977
3049
dependencies = [
2978
3050
"indexmap 2.12.1",
2979
3051
"miette",
···
2985
3057
2986
3058
[[package]]
2987
3059
name = "nu-cmd-extra"
2988
-
version = "0.109.1"
2989
-
source = "registry+https://github.com/rust-lang/crates.io-index"
2990
-
checksum = "96f1ac416cd5182ca427a1007c9d0193feaa746aeb54b5320251faad41481cfe"
3060
+
version = "0.109.2"
3061
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
2991
3062
dependencies = [
2992
3063
"fancy-regex",
2993
3064
"heck 0.5.0",
···
3010
3081
3011
3082
[[package]]
3012
3083
name = "nu-cmd-lang"
3013
-
version = "0.109.1"
3014
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3015
-
checksum = "5b266674d87b816264f6aff8cca351e6ebb156f34faab45d7d728c2aba005495"
3084
+
version = "0.109.2"
3085
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3016
3086
dependencies = [
3017
3087
"itertools 0.14.0",
3018
3088
"nu-cmd-base",
···
3026
3096
3027
3097
[[package]]
3028
3098
name = "nu-color-config"
3029
-
version = "0.109.1"
3030
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3031
-
checksum = "440a59265caf5468af6ff186e656bc6e6d349c08662ee959b116375597864206"
3099
+
version = "0.109.2"
3100
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3032
3101
dependencies = [
3033
3102
"nu-ansi-term",
3034
3103
"nu-engine",
···
3039
3108
3040
3109
[[package]]
3041
3110
name = "nu-command"
3042
-
version = "0.109.1"
3043
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3044
-
checksum = "5380a9cf6ef0f482280b71d6e78fa8d4bc1b2b43aa8f0b1fa786f96087c2d500"
3111
+
version = "0.109.2"
3112
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3045
3113
dependencies = [
3046
3114
"alphanumeric-sort",
3047
3115
"base64",
···
3098
3166
"pathdiff",
3099
3167
"percent-encoding",
3100
3168
"print-positions",
3101
-
"procfs 0.17.0",
3169
+
"procfs",
3102
3170
"quick-xml 0.38.4",
3103
3171
"rayon",
3104
3172
"rmp",
···
3129
3197
3130
3198
[[package]]
3131
3199
name = "nu-derive-value"
3132
-
version = "0.109.1"
3133
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3134
-
checksum = "1465d2d3ada6004cb6689f269a08c70ba81056231e2b5392d1e0ccf5825f81cb"
3200
+
version = "0.109.2"
3201
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3135
3202
dependencies = [
3136
3203
"heck 0.5.0",
3137
3204
"proc-macro-error2",
···
3142
3209
3143
3210
[[package]]
3144
3211
name = "nu-engine"
3145
-
version = "0.109.1"
3146
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3147
-
checksum = "b3b777faf7c5180fe5d7f67d83c44fd14138d91f2938a36494ed6ac66b7160f3"
3212
+
version = "0.109.2"
3213
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3148
3214
dependencies = [
3149
3215
"fancy-regex",
3150
3216
"log",
···
3157
3223
3158
3224
[[package]]
3159
3225
name = "nu-experimental"
3160
-
version = "0.109.1"
3161
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3162
-
checksum = "73dd212a1afdad646a38c00579a0988264880aeb97fee820b349a28cdcc04df2"
3226
+
version = "0.109.2"
3227
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3163
3228
dependencies = [
3164
3229
"itertools 0.14.0",
3165
3230
"thiserror 2.0.17",
···
3167
3232
3168
3233
[[package]]
3169
3234
name = "nu-glob"
3170
-
version = "0.109.1"
3171
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3172
-
checksum = "15aa2c17078926f14e393b4b708e69f228cb6fd4c81136839bde82772bdde1b5"
3235
+
version = "0.109.2"
3236
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3173
3237
3174
3238
[[package]]
3175
3239
name = "nu-json"
3176
-
version = "0.109.1"
3177
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3178
-
checksum = "7ca63927a3c1c4fb889e80dc5cfbe754daed822a7b503cc74e600627c2aa8435"
3240
+
version = "0.109.2"
3241
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3179
3242
dependencies = [
3180
3243
"linked-hash-map",
3181
3244
"nu-utils",
···
3186
3249
3187
3250
[[package]]
3188
3251
name = "nu-parser"
3189
-
version = "0.109.1"
3190
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3191
-
checksum = "237172636312c3566272511a00c1dc355202406c376e1546a45a33c65e81babe"
3252
+
version = "0.109.2"
3253
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3192
3254
dependencies = [
3193
3255
"bytesize",
3194
3256
"chrono",
···
3203
3265
3204
3266
[[package]]
3205
3267
name = "nu-path"
3206
-
version = "0.109.1"
3207
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3208
-
checksum = "dde9d8ba26f62c07176c0237a36f38ce964ab3a0dcfb6aab1feea7515d1c6594"
3268
+
version = "0.109.2"
3269
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3209
3270
dependencies = [
3210
3271
"dirs",
3211
3272
"omnipath",
···
3215
3276
3216
3277
[[package]]
3217
3278
name = "nu-pretty-hex"
3218
-
version = "0.109.1"
3219
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3220
-
checksum = "02561546604ac4c443bad65d9485ab3965154cad0873340e2e9ebe72d4a18aef"
3279
+
version = "0.109.2"
3280
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3221
3281
dependencies = [
3222
3282
"nu-ansi-term",
3223
3283
]
3224
3284
3225
3285
[[package]]
3226
3286
name = "nu-protocol"
3227
-
version = "0.109.1"
3228
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3229
-
checksum = "038943300ca9de0924fef1c795a7dd16ffc67105629477cf163e8ee6bad95ea6"
3287
+
version = "0.109.2"
3288
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3230
3289
dependencies = [
3231
3290
"bytes",
3232
3291
"chrono",
···
3261
3320
3262
3321
[[package]]
3263
3322
name = "nu-system"
3264
-
version = "0.109.1"
3265
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3266
-
checksum = "46be734cc9b19e09a9665769e14360e13e6978490056ba5c8bfad7dd0537ea83"
3323
+
version = "0.109.2"
3324
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3267
3325
dependencies = [
3268
3326
"chrono",
3269
3327
"itertools 0.14.0",
···
3273
3331
"mach2",
3274
3332
"nix",
3275
3333
"ntapi",
3276
-
"procfs 0.17.0",
3334
+
"procfs",
3277
3335
"sysinfo",
3278
3336
"web-time",
3279
3337
"windows 0.62.2",
···
3281
3339
3282
3340
[[package]]
3283
3341
name = "nu-table"
3284
-
version = "0.109.1"
3285
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3286
-
checksum = "aa96502adbb69c838d8469715327ba2dacf2c4f5254a4cdee7536e2c6849de1d"
3342
+
version = "0.109.2"
3343
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3287
3344
dependencies = [
3288
3345
"fancy-regex",
3289
3346
"nu-ansi-term",
···
3296
3353
3297
3354
[[package]]
3298
3355
name = "nu-term-grid"
3299
-
version = "0.109.1"
3300
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3301
-
checksum = "2b6545b361413e88bea37c4b9c7aa97a7fd7a11d84a5d330a72242367fd1d2df"
3356
+
version = "0.109.2"
3357
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3302
3358
dependencies = [
3303
3359
"nu-utils",
3304
3360
"unicode-width 0.2.2",
···
3306
3362
3307
3363
[[package]]
3308
3364
name = "nu-utils"
3309
-
version = "0.109.1"
3310
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3311
-
checksum = "3f8eb43c29cc5bce85f87defdadc2cca964fa434d808af37036a7cb78f3c68e9"
3365
+
version = "0.109.2"
3366
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3312
3367
dependencies = [
3313
3368
"byteyarn",
3314
3369
"crossterm_winapi",
···
3398
3453
]
3399
3454
3400
3455
[[package]]
3401
-
name = "number_prefix"
3402
-
version = "0.4.0"
3403
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3404
-
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
3405
-
3406
-
[[package]]
3407
3456
name = "nuon"
3408
-
version = "0.109.1"
3409
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3410
-
checksum = "f8c4f2e4460a6cf00e50cddf0840f954874d645be3af5196c5858c70c069d8c0"
3457
+
version = "0.109.2"
3458
+
source = "git+https://github.com/90-008/nushell#db3f8b1979bc9bfc6b4b54046804db9c1709eaa0"
3411
3459
dependencies = [
3412
3460
"nu-engine",
3413
3461
"nu-parser",
···
3963
4011
"chrono",
3964
4012
"flate2",
3965
4013
"hex",
3966
-
"procfs-core 0.17.0",
4014
+
"procfs-core",
3967
4015
"rustix 0.38.44",
3968
4016
]
3969
4017
3970
4018
[[package]]
3971
-
name = "procfs"
3972
-
version = "0.18.0"
3973
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3974
-
checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7"
3975
-
dependencies = [
3976
-
"bitflags",
3977
-
"chrono",
3978
-
"flate2",
3979
-
"procfs-core 0.18.0",
3980
-
"rustix 1.1.2",
3981
-
]
3982
-
3983
-
[[package]]
3984
4019
name = "procfs-core"
3985
4020
version = "0.17.0"
3986
4021
source = "registry+https://github.com/rust-lang/crates.io-index"
3987
4022
checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec"
3988
-
dependencies = [
3989
-
"bitflags",
3990
-
"chrono",
3991
-
"hex",
3992
-
]
3993
-
3994
-
[[package]]
3995
-
name = "procfs-core"
3996
-
version = "0.18.0"
3997
-
source = "registry+https://github.com/rust-lang/crates.io-index"
3998
-
checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405"
3999
4023
dependencies = [
4000
4024
"bitflags",
4001
4025
"chrono",
···
4278
4302
4279
4303
[[package]]
4280
4304
name = "reqwest"
4281
-
version = "0.12.25"
4305
+
version = "0.12.26"
4282
4306
source = "registry+https://github.com/rust-lang/crates.io-index"
4283
-
checksum = "b6eff9328d40131d43bd911d42d79eb6a47312002a4daefc9e37f17e74a7701a"
4307
+
checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f"
4284
4308
dependencies = [
4285
4309
"base64",
4286
4310
"bytes",
···
4408
4432
source = "registry+https://github.com/rust-lang/crates.io-index"
4409
4433
checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475"
4410
4434
dependencies = [
4435
+
"globset",
4411
4436
"sha2",
4412
4437
"walkdir",
4413
4438
]
···
4471
4496
4472
4497
[[package]]
4473
4498
name = "rustls"
4474
-
version = "0.23.28"
4499
+
version = "0.23.35"
4475
4500
source = "registry+https://github.com/rust-lang/crates.io-index"
4476
-
checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
4501
+
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
4477
4502
dependencies = [
4478
4503
"once_cell",
4479
4504
"ring",
···
5736
5761
5737
5762
[[package]]
5738
5763
name = "uucore"
5739
-
version = "0.4.0"
5764
+
version = "0.5.0"
5740
5765
source = "registry+https://github.com/rust-lang/crates.io-index"
5741
-
checksum = "2003164a38a7f39da1de103a70fa66b745f572f0045ec261481539516c0a8a0e"
5766
+
checksum = "b5eddd390f3fdef74f104a948559e6de29203f60f8f563c8c9f528cd4c88ee78"
5742
5767
dependencies = [
5743
-
"bstr",
5744
5768
"clap",
5745
5769
"fluent",
5746
5770
"fluent-bundle",
5747
5771
"fluent-syntax",
5748
5772
"libc",
5749
5773
"nix",
5750
-
"number_prefix",
5751
5774
"os_display",
5752
5775
"phf 0.13.1",
5753
-
"procfs 0.18.0",
5754
5776
"thiserror 2.0.17",
5755
5777
"unic-langid",
5756
5778
"uucore_procs",
···
5759
5781
5760
5782
[[package]]
5761
5783
name = "uucore_procs"
5762
-
version = "0.4.0"
5784
+
version = "0.5.0"
5763
5785
source = "registry+https://github.com/rust-lang/crates.io-index"
5764
-
checksum = "c76f0308f7810d915246a39748e7f5d64e43e6bb9d6c8107224f9d741aefc375"
5786
+
checksum = "47148309a1f7a989d165dabbbc7f2bf156d7ff6affe7d69c1c5bfb822e663ae6"
5765
5787
dependencies = [
5766
5788
"proc-macro2",
5767
5789
"quote",
···
5788
5810
[[package]]
5789
5811
name = "vfs"
5790
5812
version = "0.12.1"
5791
-
source = "git+https://github.com/landaire/rust-vfs?branch=fix%2Fwasm#c4341e8e7c16a019c1a1415fc8a413bf883a08d5"
5813
+
source = "git+https://github.com/90-008/rust-vfs?branch=fix%2Fwasm#547b30641d8f329614fb29e44c1c7360ef57ded9"
5792
5814
dependencies = [
5793
5815
"filetime",
5816
+
"rust-embed",
5794
5817
]
5795
5818
5796
5819
[[package]]
+15
-8
Cargo.toml
+15
-8
Cargo.toml
···
11
11
wasm-bindgen-futures = "0.4"
12
12
getrandom = { version = "0.3", features = ["wasm_js"] }
13
13
web-sys = { version = "0.3", features = ["console", "Window"] }
14
-
vfs = { version = "0.12" }
15
-
nu-cmd-lang = { version = "0.109.1", default-features = false }
16
-
nu-command = { version = "0.109.1", default-features = false }
17
-
nu-engine = { version = "0.109.1", default-features = false }
18
-
nu-parser = { version = "0.109.1", default-features = false }
19
-
nu-protocol = { version = "0.109.1", default-features = false }
20
-
nu-cmd-extra = { version = "0.109.1", default-features = false }
14
+
vfs = { version = "0.12", features = ["embedded-fs"] }
15
+
nu-command = { git = "https://github.com/90-008/nushell", default-features = false }
16
+
nu-engine = { git = "https://github.com/90-008/nushell", default-features = false }
17
+
nu-parser = { git = "https://github.com/90-008/nushell", default-features = false }
18
+
nu-protocol = { git = "https://github.com/90-008/nushell", default-features = false }
19
+
nu-path = { git = "https://github.com/90-008/nushell", default-features = false }
20
+
nu-glob = { git = "https://github.com/90-008/nushell", default-features = false }
21
+
nu-cmd-base = { git = "https://github.com/90-008/nushell", default-features = false }
22
+
nu-cmd-lang = { git = "https://github.com/90-008/nushell", default-features = false }
23
+
nu-cmd-extra = { git = "https://github.com/90-008/nushell", default-features = false }
21
24
serde = { version = "1.0", features = ["derive"] }
22
25
serde_json = "1"
23
26
miette = { version = "7.6", features = ["fancy"] }
···
35
38
36
39
scc = "3"
37
40
rapidhash = { version = "4", features = ["unsafe"] }
41
+
async-lock = "3.4.1"
42
+
compile-time = "0.2.0"
43
+
rust-embed = { version = "8.9.0", features = ["debug-embed", "include-exclude"] }
38
44
39
45
[patch.crates-io]
40
-
vfs = { git = "https://github.com/landaire/rust-vfs", branch = "fix/wasm" }
46
+
vfs = { git = "https://github.com/90-008/rust-vfs", branch = "fix/wasm" }
41
47
42
48
[profile.release]
43
49
opt-level = 3
44
50
lto = true
45
51
codegen-units = 1
46
52
strip = true
53
+
debug-assertions = false
+5
README.md
+5
README.md
···
1
1
nushell, but on the web, with a virtual environment for working in with various commands to interact with the environment, trying to emulate the nushell CLI from native OSes. faux + nu = faunu.
2
2
3
3
see [dysnomia.ptr.pet](https://dysnomia.ptr.pet) to try it out.
4
+
5
+
# TODOS
6
+
7
+
- add a separate worker for running commands in
8
+
- add more commands
embedded/.gitkeep
embedded/.gitkeep
This is a binary file and will not be displayed.
+1
-1
flake.nix
+1
-1
flake.nix
···
23
23
nodejs-slim_latest deno
24
24
nodePackages.svelte-language-server
25
25
nodePackages.typescript-language-server
26
-
rustc rust-analyzer cargo wasm-pack wasm-bindgen-cli lld rustfmt binaryen
26
+
rustc rust-analyzer cargo wasm-pack wasm-bindgen-cli_0_2_104 lld rustfmt binaryen
27
27
];
28
28
shellHook = ''
29
29
export PATH="$PATH:$PWD/node_modules/.bin"
+5
-3
nix/wasm.nix
+5
-3
nix/wasm.nix
···
3
3
lib,
4
4
wasm-pack,
5
5
binaryen,
6
-
wasm-bindgen-cli,
6
+
wasm-bindgen-cli_0_2_104,
7
7
lld,
8
8
stdenv,
9
9
...
···
21
21
../Cargo.lock
22
22
../src
23
23
../.cargo
24
+
../embedded
24
25
];
25
26
};
26
27
27
28
cargoLock = {
28
29
lockFile = ../Cargo.lock;
29
30
outputHashes = {
30
-
"vfs-0.12.1" = "sha256-d249sIYhICdqqb7uoTRyhXAZTCF5zgjfItM4DE7b/gQ=";
31
+
"vfs-0.12.1" = "sha256-arpgwVsBhnn/2qawTR+NeyWRJOipr0kafg7VaiISufM=";
31
32
"jacquard-0.9.4" = "sha256-TEu4coueWzzkmFCkGb610Xrly7n8LUGMa5tdde/OElg=";
33
+
"nu-cmd-base-0.109.2" = "sha256-Q+6PxSmeiV/K6QP0I9xCiqZM37+p+CRLs7LMBUWurPo=";
32
34
};
33
35
};
34
36
35
-
nativeBuildInputs = [wasm-pack wasm-bindgen-cli lld];
37
+
nativeBuildInputs = [wasm-pack wasm-bindgen-cli_0_2_104 lld];
36
38
37
39
phases = ["unpackPhase" "buildPhase"];
38
40
+13
-5
src/cmd/cd.rs
+13
-5
src/cmd/cd.rs
···
1
-
use crate::globals::{get_pwd, get_vfs, set_pwd, to_shell_err};
1
+
use crate::{
2
+
error::to_shell_err,
3
+
globals::{get_pwd, get_vfs, set_pwd},
4
+
};
2
5
use nu_engine::CallExt;
3
6
use nu_protocol::{
4
-
Category, PipelineData, ShellError, Signature, SyntaxShape,
7
+
Category, IntoValue, PipelineData, ShellError, Signature, SyntaxShape, Type,
5
8
engine::{Command, EngineState, Stack},
6
9
};
7
10
use std::sync::Arc;
···
17
20
18
21
fn signature(&self) -> Signature {
19
22
Signature::build("cd")
20
-
.optional("path", SyntaxShape::String, "the path to change into")
23
+
.optional("path", SyntaxShape::Filepath, "the path to change into")
24
+
.input_output_type(Type::Nothing, Type::Nothing)
21
25
.category(Category::FileSystem)
22
26
}
23
27
···
28
32
fn run(
29
33
&self,
30
34
engine_state: &EngineState,
31
-
_stack: &mut Stack,
35
+
stack: &mut Stack,
32
36
call: &nu_protocol::engine::Call,
33
37
_input: PipelineData,
34
38
) -> Result<PipelineData, ShellError> {
35
-
let path_arg: Option<String> = call.opt(engine_state, _stack, 0)?;
39
+
let path_arg: Option<String> = call.opt(engine_state, stack, 0)?;
36
40
let path = path_arg.unwrap_or_else(|| "/".to_string());
37
41
38
42
let base: Arc<vfs::VfsPath> = if path.starts_with('/') {
···
49
53
let metadata = target.metadata().map_err(to_shell_err(call.head))?;
50
54
match metadata.file_type {
51
55
VfsFileType::Directory => {
56
+
stack.add_env_var(
57
+
"PWD".to_string(),
58
+
target.as_str().into_value(call.arguments_span()),
59
+
);
52
60
set_pwd(Arc::new(target));
53
61
Ok(PipelineData::Empty)
54
62
}
+61
src/cmd/eval.rs
+61
src/cmd/eval.rs
···
1
+
use crate::globals::print_to_console;
2
+
use nu_engine::CallExt;
3
+
use nu_protocol::engine::Call;
4
+
use nu_protocol::{
5
+
Category, PipelineData, ShellError, Signature,
6
+
engine::{Command, EngineState, Stack},
7
+
};
8
+
use nu_protocol::{SyntaxShape, Type};
9
+
10
+
#[derive(Clone)]
11
+
pub struct Eval;
12
+
13
+
impl Command for Eval {
14
+
fn name(&self) -> &str {
15
+
"eval"
16
+
}
17
+
18
+
fn signature(&self) -> Signature {
19
+
Signature::build(self.name())
20
+
.optional("code", SyntaxShape::String, "code to evaluate")
21
+
.input_output_type(Type::one_of([Type::Nothing, Type::String]), Type::Nothing)
22
+
.category(Category::FileSystem)
23
+
}
24
+
25
+
fn description(&self) -> &str {
26
+
"evaluates a string as nushell code."
27
+
}
28
+
29
+
fn run(
30
+
&self,
31
+
engine_state: &EngineState,
32
+
stack: &mut Stack,
33
+
call: &Call,
34
+
input: PipelineData,
35
+
) -> Result<PipelineData, ShellError> {
36
+
let code: Option<String> = call.opt(engine_state, stack, 0)?;
37
+
38
+
let (span, code) = match code {
39
+
Some(c) => (Some(call.arguments_span()), c),
40
+
None => (
41
+
input.span(),
42
+
input.collect_string("\n", &engine_state.config)?,
43
+
),
44
+
};
45
+
46
+
match super::source_file::eval(engine_state, stack, &code, None) {
47
+
Ok(d) => Ok(d),
48
+
Err(err) => {
49
+
let msg: String = err.into();
50
+
print_to_console(&msg, true);
51
+
Err(ShellError::GenericError {
52
+
error: "source error".into(),
53
+
msg: "can't source string".into(),
54
+
span,
55
+
help: None,
56
+
inner: vec![],
57
+
})
58
+
}
59
+
}
60
+
}
61
+
}
+5
-3
src/cmd/fetch.rs
+5
-3
src/cmd/fetch.rs
···
1
-
use crate::globals::{get_pwd, print_to_console, register_task, remove_task, to_shell_err};
1
+
use crate::error::to_shell_err;
2
+
use crate::globals::{get_pwd, print_to_console, register_task, remove_task};
2
3
use anyhow::{Result, anyhow};
3
4
use futures::future::{AbortHandle, Abortable};
4
5
use jacquard::types::aturi::AtUri;
···
20
21
storage::{BlockStore, MemoryBlockStore},
21
22
};
22
23
use nu_engine::CallExt;
23
-
use nu_protocol::IntoPipelineData;
24
24
use nu_protocol::{
25
25
Category, PipelineData, ShellError, Signature, SyntaxShape, Value,
26
26
engine::{Command, EngineState, Stack},
27
27
};
28
+
use nu_protocol::{IntoPipelineData, Type};
28
29
use std::io::Write;
29
30
use std::str::FromStr;
30
31
use std::sync::Arc;
···
47
48
"HTTP URI or AT URI (at://identifier[/collection[/rkey]])",
48
49
)
49
50
.named("output", SyntaxShape::Filepath, "output path", Some('o'))
51
+
.input_output_type(Type::Nothing, Type::Nothing)
50
52
.category(Category::Network)
51
53
}
52
54
···
180
182
match result {
181
183
Ok(_) => {}
182
184
Err(e) => {
183
-
print_to_console(
185
+
let _ = print_to_console(
184
186
&format!("\x1b[31mโ\x1b[0m ({task_desc}) error: {e}"),
185
187
false,
186
188
);
+313
src/cmd/glob.rs
+313
src/cmd/glob.rs
···
1
+
use std::sync::Arc;
2
+
3
+
use crate::globals::{get_pwd, get_vfs};
4
+
use nu_engine::CallExt;
5
+
use nu_glob::Pattern;
6
+
use nu_protocol::{
7
+
Category, ListStream, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
8
+
engine::{Command, EngineState, Stack},
9
+
};
10
+
use vfs::VfsFileType;
11
+
12
+
/// Options for glob matching
13
+
pub struct GlobOptions {
14
+
pub max_depth: Option<usize>,
15
+
pub no_dirs: bool,
16
+
pub no_files: bool,
17
+
}
18
+
19
+
impl Default for GlobOptions {
20
+
fn default() -> Self {
21
+
Self {
22
+
max_depth: None,
23
+
no_dirs: false,
24
+
no_files: false,
25
+
}
26
+
}
27
+
}
28
+
29
+
/// Expand a path (glob pattern or regular path) into a list of matching paths.
30
+
/// If the path is not a glob pattern, returns a single-item list.
31
+
/// Returns a vector of relative paths (relative to the base path).
32
+
pub fn expand_path(
33
+
path_str: &str,
34
+
base_path: Arc<vfs::VfsPath>,
35
+
options: GlobOptions,
36
+
) -> Result<Vec<String>, ShellError> {
37
+
// Check if it's a glob pattern
38
+
let is_glob = path_str.contains('*')
39
+
|| path_str.contains('?')
40
+
|| path_str.contains('[')
41
+
|| path_str.contains("**");
42
+
43
+
if is_glob {
44
+
glob_match(path_str, base_path, options)
45
+
} else {
46
+
// Single path: return as single-item list
47
+
Ok(vec![path_str.trim_start_matches('/').to_string()])
48
+
}
49
+
}
50
+
51
+
/// Match files and directories using a glob pattern.
52
+
/// Returns a vector of relative paths (relative to the base path) that match the pattern.
53
+
pub fn glob_match(
54
+
pattern_str: &str,
55
+
base_path: Arc<vfs::VfsPath>,
56
+
options: GlobOptions,
57
+
) -> Result<Vec<String>, ShellError> {
58
+
if pattern_str.is_empty() {
59
+
return Err(ShellError::GenericError {
60
+
error: "glob pattern must not be empty".into(),
61
+
msg: "glob pattern is empty".into(),
62
+
span: None,
63
+
help: Some("add characters to the glob pattern".into()),
64
+
inner: vec![],
65
+
});
66
+
}
67
+
68
+
// Parse the pattern
69
+
let pattern = Pattern::new(pattern_str).map_err(|e| ShellError::GenericError {
70
+
error: "error with glob pattern".into(),
71
+
msg: format!("{}", e),
72
+
span: None,
73
+
help: None,
74
+
inner: vec![],
75
+
})?;
76
+
77
+
// Determine max depth
78
+
let max_depth = if let Some(d) = options.max_depth {
79
+
d
80
+
} else if pattern_str.contains("**") {
81
+
usize::MAX
82
+
} else {
83
+
// Count number of / in pattern to determine depth
84
+
pattern_str.split('/').count()
85
+
};
86
+
87
+
// Normalize pattern: remove leading / for relative matching
88
+
let normalized_pattern = pattern_str.trim_start_matches('/');
89
+
let is_recursive = normalized_pattern.contains("**");
90
+
91
+
// Collect matching paths
92
+
let mut matches = Vec::new();
93
+
94
+
fn walk_directory(
95
+
current_path: Arc<vfs::VfsPath>,
96
+
current_relative_path: String,
97
+
pattern: &Pattern,
98
+
normalized_pattern: &str,
99
+
current_depth: usize,
100
+
max_depth: usize,
101
+
matches: &mut Vec<String>,
102
+
no_dirs: bool,
103
+
no_files: bool,
104
+
is_recursive: bool,
105
+
) -> Result<(), ShellError> {
106
+
if current_depth > max_depth {
107
+
return Ok(());
108
+
}
109
+
110
+
// Walk through directory entries
111
+
if let Ok(entries) = current_path.read_dir() {
112
+
for entry in entries {
113
+
let filename = entry.filename();
114
+
let entry_path =
115
+
current_path
116
+
.join(&filename)
117
+
.map_err(|e| ShellError::GenericError {
118
+
error: "path error".into(),
119
+
msg: e.to_string(),
120
+
span: None,
121
+
help: None,
122
+
inner: vec![],
123
+
})?;
124
+
125
+
// Build relative path from base
126
+
let new_relative = if current_relative_path.is_empty() {
127
+
filename.clone()
128
+
} else {
129
+
format!("{}/{}", current_relative_path, filename)
130
+
};
131
+
132
+
let metadata = entry_path
133
+
.metadata()
134
+
.map_err(|e| ShellError::GenericError {
135
+
error: "path error".into(),
136
+
msg: e.to_string(),
137
+
span: None,
138
+
help: None,
139
+
inner: vec![],
140
+
})?;
141
+
142
+
// Check if this path matches the pattern
143
+
// For patterns without path separators, match just the filename
144
+
// For patterns with path separators, match the full relative path
145
+
let path_to_match = if normalized_pattern.contains('/') {
146
+
&new_relative
147
+
} else {
148
+
&filename
149
+
};
150
+
151
+
if pattern.matches(path_to_match) {
152
+
let should_include = match metadata.file_type {
153
+
VfsFileType::Directory => !no_dirs,
154
+
VfsFileType::File => !no_files,
155
+
};
156
+
if should_include {
157
+
matches.push(new_relative.clone());
158
+
}
159
+
}
160
+
161
+
// Recursively walk into subdirectories
162
+
if metadata.file_type == VfsFileType::Directory {
163
+
// Only recurse if:
164
+
// 1. Pattern contains ** (recursive wildcard), OR
165
+
// 2. Pattern has path separators and we haven't matched all components yet
166
+
let has_path_separator = normalized_pattern.contains('/');
167
+
let pattern_component_count = if has_path_separator {
168
+
normalized_pattern.split('/').count()
169
+
} else {
170
+
1
171
+
};
172
+
173
+
let should_recurse = is_recursive
174
+
|| (has_path_separator && current_depth + 1 < pattern_component_count);
175
+
176
+
if should_recurse {
177
+
walk_directory(
178
+
Arc::new(entry_path),
179
+
new_relative,
180
+
pattern,
181
+
normalized_pattern,
182
+
current_depth + 1,
183
+
max_depth,
184
+
matches,
185
+
no_dirs,
186
+
no_files,
187
+
is_recursive,
188
+
)?;
189
+
}
190
+
}
191
+
}
192
+
}
193
+
194
+
Ok(())
195
+
}
196
+
197
+
// Start walking from base path
198
+
walk_directory(
199
+
base_path,
200
+
String::new(),
201
+
&pattern,
202
+
normalized_pattern,
203
+
0,
204
+
max_depth,
205
+
&mut matches,
206
+
options.no_dirs,
207
+
options.no_files,
208
+
is_recursive,
209
+
)?;
210
+
211
+
Ok(matches)
212
+
}
213
+
214
+
#[derive(Clone)]
215
+
pub struct Glob;
216
+
217
+
impl Command for Glob {
218
+
fn name(&self) -> &str {
219
+
"glob"
220
+
}
221
+
222
+
fn signature(&self) -> Signature {
223
+
Signature::build("glob")
224
+
.required(
225
+
"pattern",
226
+
SyntaxShape::OneOf(vec![SyntaxShape::String, SyntaxShape::GlobPattern]),
227
+
"the glob expression.",
228
+
)
229
+
.named(
230
+
"depth",
231
+
SyntaxShape::Int,
232
+
"directory depth to search",
233
+
Some('d'),
234
+
)
235
+
.switch(
236
+
"no-dir",
237
+
"whether to filter out directories from the returned paths",
238
+
Some('D'),
239
+
)
240
+
.switch(
241
+
"no-file",
242
+
"whether to filter out files from the returned paths",
243
+
Some('F'),
244
+
)
245
+
.input_output_type(Type::Nothing, Type::List(Box::new(Type::String)))
246
+
.category(Category::FileSystem)
247
+
}
248
+
249
+
fn description(&self) -> &str {
250
+
"creates a list of paths based on the glob pattern provided."
251
+
}
252
+
253
+
fn run(
254
+
&self,
255
+
engine_state: &EngineState,
256
+
stack: &mut Stack,
257
+
call: &nu_protocol::engine::Call,
258
+
_input: PipelineData,
259
+
) -> Result<PipelineData, ShellError> {
260
+
let span = call.head;
261
+
let pattern_value: Value = call.req(engine_state, stack, 0)?;
262
+
let pattern_span = pattern_value.span();
263
+
let depth: Option<i64> = call.get_flag(engine_state, stack, "depth")?;
264
+
let no_dirs = call.has_flag(engine_state, stack, "no-dir")?;
265
+
let no_files = call.has_flag(engine_state, stack, "no-file")?;
266
+
267
+
let pattern_str = match pattern_value {
268
+
Value::String { val, .. } | Value::Glob { val, .. } => val,
269
+
_ => {
270
+
return Err(ShellError::IncorrectValue {
271
+
msg: "incorrect glob pattern supplied to glob. use string or glob only."
272
+
.to_string(),
273
+
val_span: pattern_span,
274
+
call_span: pattern_span,
275
+
});
276
+
}
277
+
};
278
+
279
+
if pattern_str.is_empty() {
280
+
return Err(ShellError::GenericError {
281
+
error: "glob pattern must not be empty".into(),
282
+
msg: "glob pattern is empty".into(),
283
+
span: Some(pattern_span),
284
+
help: Some("add characters to the glob pattern".into()),
285
+
inner: vec![],
286
+
});
287
+
}
288
+
289
+
// Determine if pattern is absolute (starts with /)
290
+
let is_absolute = pattern_str.starts_with('/');
291
+
let base_path = if is_absolute { get_vfs() } else { get_pwd() };
292
+
293
+
// Use the glob_match function
294
+
let options = GlobOptions {
295
+
max_depth: depth.map(|d| d as usize),
296
+
no_dirs,
297
+
no_files,
298
+
};
299
+
300
+
let matches = glob_match(&pattern_str, base_path, options)?;
301
+
302
+
// Convert matches to Value stream
303
+
let signals = engine_state.signals().clone();
304
+
let values = matches
305
+
.into_iter()
306
+
.map(move |path| Value::string(path, span));
307
+
308
+
Ok(PipelineData::list_stream(
309
+
ListStream::new(values, span, signals.clone()),
310
+
None,
311
+
))
312
+
}
313
+
}
+2
-1
src/cmd/job_kill.rs
+2
-1
src/cmd/job_kill.rs
···
1
1
use crate::globals::kill_task_by_id;
2
2
use nu_engine::CallExt;
3
3
use nu_protocol::{
4
-
Category, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Value,
4
+
Category, IntoPipelineData, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
5
5
engine::{Call, Command, EngineState, Stack},
6
6
};
7
7
···
16
16
fn signature(&self) -> Signature {
17
17
Signature::build("job kill")
18
18
.required("id", SyntaxShape::Int, "id of job to kill")
19
+
.input_output_type(Type::Nothing, Type::Nothing)
19
20
.category(Category::System)
20
21
}
21
22
+4
-2
src/cmd/job_list.rs
+4
-2
src/cmd/job_list.rs
···
1
1
use crate::globals::get_all_tasks;
2
2
use nu_protocol::{
3
-
Category, ListStream, PipelineData, Record, ShellError, Signature, Value,
3
+
Category, ListStream, PipelineData, Record, ShellError, Signature, Type, Value,
4
4
engine::{Call, Command, EngineState, Stack},
5
5
};
6
6
···
13
13
}
14
14
15
15
fn signature(&self) -> Signature {
16
-
Signature::build("job list").category(Category::System)
16
+
Signature::build("job list")
17
+
.input_output_type(Type::Nothing, Type::table())
18
+
.category(Category::System)
17
19
}
18
20
19
21
fn description(&self) -> &str {
+111
-52
src/cmd/ls.rs
+111
-52
src/cmd/ls.rs
···
1
-
use std::{
2
-
sync::Arc,
3
-
time::{SystemTime, UNIX_EPOCH},
1
+
use std::time::{SystemTime, UNIX_EPOCH};
2
+
3
+
use crate::{
4
+
cmd::glob::{GlobOptions, expand_path},
5
+
error::to_shell_err,
6
+
globals::{get_pwd, get_vfs},
4
7
};
5
-
6
-
use crate::globals::{get_pwd, to_shell_err};
7
8
use jacquard::chrono;
8
9
use nu_engine::CallExt;
9
10
use nu_protocol::{
10
-
Category, ListStream, PipelineData, Record, ShellError, Signature, SyntaxShape, Value,
11
+
Category, ListStream, PipelineData, Record, ShellError, Signature, SyntaxShape, Type, Value,
11
12
engine::{Command, EngineState, Stack},
12
13
};
14
+
use std::sync::Arc;
13
15
14
16
#[derive(Clone)]
15
17
pub struct Ls;
···
21
23
22
24
fn signature(&self) -> Signature {
23
25
Signature::build("ls")
24
-
.optional("path", SyntaxShape::String, "the path to list")
26
+
.optional(
27
+
"path",
28
+
SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]),
29
+
"the path to list",
30
+
)
25
31
.switch(
26
32
"all",
27
33
"include hidden paths (that start with a dot)",
···
32
38
"show detailed information about each file",
33
39
Some('l'),
34
40
)
41
+
.switch("full-paths", "display paths as absolute paths", Some('f'))
42
+
.input_output_type(Type::Nothing, Type::table())
35
43
.category(Category::FileSystem)
36
44
}
37
45
···
46
54
call: &nu_protocol::engine::Call,
47
55
_input: PipelineData,
48
56
) -> Result<PipelineData, ShellError> {
49
-
let path_arg: Option<String> = call.opt(engine_state, stack, 0)?;
57
+
let path_arg: Option<Value> = call.opt(engine_state, stack, 0)?;
50
58
let all = call.has_flag(engine_state, stack, "all")?;
51
59
let long = call.has_flag(engine_state, stack, "long")?;
60
+
let full_paths = call.has_flag(engine_state, stack, "full-paths")?;
52
61
53
-
let mut target_dir = get_pwd();
54
-
if let Some(path) = path_arg {
55
-
target_dir = Arc::new(
56
-
target_dir
57
-
.join(path.trim_end_matches('/'))
58
-
.map_err(to_shell_err(call.arguments_span()))?,
59
-
);
60
-
}
62
+
let pwd = get_pwd();
63
+
let span = call.head;
64
+
65
+
// If no path provided, list current directory
66
+
let (matches, base_path) = if let Some(path_val) = &path_arg {
67
+
let path_str = match path_val {
68
+
Value::String { val, .. } | Value::Glob { val, .. } => val,
69
+
_ => {
70
+
return Err(ShellError::GenericError {
71
+
error: "invalid path".into(),
72
+
msg: "path must be a string or glob pattern".into(),
73
+
span: Some(call.arguments_span()),
74
+
help: None,
75
+
inner: vec![],
76
+
});
77
+
}
78
+
};
79
+
80
+
let is_absolute = path_str.starts_with('/');
81
+
let base_path: Arc<vfs::VfsPath> = if is_absolute { get_vfs() } else { pwd.clone() };
82
+
83
+
// Check if it's a glob pattern
84
+
let is_glob = path_str.contains('*')
85
+
|| path_str.contains('?')
86
+
|| path_str.contains('[')
87
+
|| path_str.contains("**");
88
+
89
+
if is_glob {
90
+
// Glob pattern: expand and list matching paths
91
+
let options = GlobOptions {
92
+
max_depth: None,
93
+
no_dirs: false,
94
+
no_files: false,
95
+
};
96
+
let matches = expand_path(path_str, base_path.clone(), options)?;
97
+
(matches, base_path)
98
+
} else {
99
+
// Non-glob path: check if it's a directory and list its contents
100
+
let normalized_path = path_str.trim_start_matches('/').trim_end_matches('/');
101
+
let target_path = base_path
102
+
.join(normalized_path)
103
+
.map_err(to_shell_err(call.arguments_span()))?;
104
+
105
+
let metadata = target_path.metadata().map_err(to_shell_err(span))?;
106
+
match metadata.file_type {
107
+
vfs::VfsFileType::Directory => {
108
+
// List directory contents
109
+
let entries = target_path.read_dir().map_err(to_shell_err(span))?;
110
+
let matches: Vec<String> = entries
111
+
.map(|e| {
112
+
// Build relative path from base_path
113
+
let entry_name = e.filename();
114
+
if normalized_path.is_empty() || normalized_path == "." {
115
+
entry_name
116
+
} else {
117
+
format!("{}/{}", normalized_path, entry_name)
118
+
}
119
+
})
120
+
.collect();
121
+
(matches, base_path)
122
+
}
123
+
vfs::VfsFileType::File => {
124
+
// Single file: return just this file (normalized, relative to base_path)
125
+
(vec![normalized_path.to_string()], base_path)
126
+
}
127
+
}
128
+
}
129
+
} else {
130
+
// No path: list current directory entries
131
+
let entries = pwd.read_dir().map_err(to_shell_err(span))?;
132
+
let matches: Vec<String> = entries.map(|e| e.filename()).collect();
133
+
(matches, pwd.clone())
134
+
};
135
+
136
+
let make_record = move |rel_path: &str| {
137
+
let full_path = base_path.join(rel_path).map_err(to_shell_err(span))?;
138
+
let metadata = full_path.metadata().map_err(to_shell_err(span))?;
61
139
62
-
let span = call.head;
63
-
let entries = target_dir.read_dir().map_err(to_shell_err(span))?;
140
+
// Filter hidden files if --all is not set
141
+
let filename = rel_path.split('/').last().unwrap_or(rel_path);
142
+
if filename.starts_with('.') && !all {
143
+
return Ok(None);
144
+
}
64
145
65
-
let make_record = move |name: &str, metadata: &vfs::VfsMetadata| {
66
146
let type_str = match metadata.file_type {
67
147
vfs::VfsFileType::Directory => "dir",
68
148
vfs::VfsFileType::File => "file",
69
149
};
70
150
71
151
let mut record = Record::new();
72
-
record.push("name", Value::string(name, span));
152
+
let display_name = if full_paths {
153
+
full_path.as_str().to_string()
154
+
} else {
155
+
rel_path.to_string()
156
+
};
157
+
record.push("name", Value::string(display_name, span));
73
158
record.push("type", Value::string(type_str, span));
74
159
record.push("size", Value::filesize(metadata.len as i64, span));
75
160
let mut add_timestamp = |field: &str, timestamp: Option<SystemTime>| {
···
91
176
add_timestamp("created", metadata.created);
92
177
add_timestamp("accessed", metadata.accessed);
93
178
}
94
-
Value::record(record, span)
95
-
};
96
-
const DIR_METADATA: vfs::VfsMetadata = vfs::VfsMetadata {
97
-
file_type: vfs::VfsFileType::Directory,
98
-
len: 0,
99
-
modified: None,
100
-
created: None,
101
-
accessed: None,
179
+
Ok(Some(Value::record(record, span)))
102
180
};
103
181
104
-
let dots = if all {
105
-
vec![
106
-
make_record(".", &DIR_METADATA),
107
-
make_record("..", &DIR_METADATA),
108
-
]
109
-
} else {
110
-
Vec::new()
111
-
};
112
-
let entries = dots
113
-
.into_iter()
114
-
.chain(entries.into_iter().flat_map(move |entry| {
115
-
let do_map = move || {
116
-
let name = entry.filename();
117
-
if name.starts_with('.') && !all {
118
-
return Ok(None);
119
-
}
120
-
let metadata = entry.metadata().map_err(to_shell_err(span))?;
121
-
122
-
Ok(Some(make_record(&name, &metadata)))
123
-
};
124
-
do_map()
125
-
.transpose()
126
-
.map(|res| res.unwrap_or_else(|err| Value::error(err, span)))
127
-
}));
182
+
let entries = matches.into_iter().flat_map(move |rel_path| {
183
+
make_record(&rel_path)
184
+
.transpose()
185
+
.map(|res| res.unwrap_or_else(|err| Value::error(err, span)))
186
+
});
128
187
129
188
let signals = engine_state.signals().clone();
130
189
Ok(PipelineData::list_stream(
+4
-3
src/cmd/mkdir.rs
+4
-3
src/cmd/mkdir.rs
···
1
-
use crate::globals::{get_pwd, to_shell_err};
1
+
use crate::{error::to_shell_err, globals::get_pwd};
2
2
use nu_engine::CallExt;
3
3
use nu_protocol::{
4
-
Category, PipelineData, ShellError, Signature, SyntaxShape,
4
+
Category, PipelineData, ShellError, Signature, SyntaxShape, Type,
5
5
engine::{Command, EngineState, Stack},
6
6
};
7
7
···
17
17
Signature::build("mkdir")
18
18
.required(
19
19
"path",
20
-
SyntaxShape::String,
20
+
SyntaxShape::Filepath,
21
21
"path of the directory(s) to create",
22
22
)
23
+
.input_output_type(Type::Nothing, Type::Nothing)
23
24
.category(Category::FileSystem)
24
25
}
25
26
+10
-4
src/cmd/mod.rs
+10
-4
src/cmd/mod.rs
···
1
1
pub mod cd;
2
+
pub mod eval;
2
3
pub mod fetch;
4
+
pub mod glob;
3
5
pub mod job;
4
6
pub mod job_kill;
5
7
pub mod job_list;
6
8
pub mod ls;
7
9
pub mod mkdir;
10
+
pub mod mv;
8
11
pub mod open;
12
+
pub mod print;
9
13
pub mod pwd;
10
14
pub mod random;
11
15
pub mod rm;
12
16
pub mod save;
13
-
pub mod source;
17
+
pub mod source_file;
14
18
pub mod sys;
15
-
pub mod version;
16
19
17
20
pub use cd::Cd;
21
+
pub use eval::Eval;
18
22
pub use fetch::Fetch;
23
+
pub use glob::Glob;
19
24
pub use job::Job;
20
25
pub use job_kill::JobKill;
21
26
pub use job_list::JobList;
22
27
pub use ls::Ls;
23
28
pub use mkdir::Mkdir;
29
+
pub use mv::Mv;
24
30
pub use open::Open;
31
+
pub use print::Print;
25
32
pub use pwd::Pwd;
26
33
pub use random::Random;
27
34
pub use rm::Rm;
28
35
pub use save::Save;
29
-
pub use source::Source;
36
+
pub use source_file::SourceFile;
30
37
pub use sys::Sys;
31
-
pub use version::Version;
+210
src/cmd/mv.rs
+210
src/cmd/mv.rs
···
1
+
use std::io::{Read, Write};
2
+
3
+
use crate::{
4
+
cmd::glob::{GlobOptions, expand_path},
5
+
error::to_shell_err,
6
+
globals::{get_pwd, get_vfs},
7
+
};
8
+
use nu_engine::CallExt;
9
+
use nu_protocol::{
10
+
Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
11
+
engine::{Command, EngineState, Stack},
12
+
};
13
+
use std::sync::Arc;
14
+
use vfs::{VfsError, VfsFileType};
15
+
16
+
#[derive(Clone)]
17
+
pub struct Mv;
18
+
19
+
impl Command for Mv {
20
+
fn name(&self) -> &str {
21
+
"mv"
22
+
}
23
+
24
+
fn signature(&self) -> Signature {
25
+
Signature::build("mv")
26
+
.required(
27
+
"source",
28
+
SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]),
29
+
"path to the file or directory to move",
30
+
)
31
+
.required(
32
+
"destination",
33
+
SyntaxShape::Filepath,
34
+
"path to the destination",
35
+
)
36
+
.input_output_type(Type::Nothing, Type::Nothing)
37
+
.category(Category::FileSystem)
38
+
}
39
+
40
+
fn description(&self) -> &str {
41
+
"move a file or directory in the virtual filesystem."
42
+
}
43
+
44
+
fn run(
45
+
&self,
46
+
engine_state: &EngineState,
47
+
stack: &mut Stack,
48
+
call: &nu_protocol::engine::Call,
49
+
_input: PipelineData,
50
+
) -> Result<PipelineData, ShellError> {
51
+
let source_value: Value = call.req(engine_state, stack, 0)?;
52
+
let dest_path: String = call.req(engine_state, stack, 1)?;
53
+
54
+
let source_str = match source_value {
55
+
Value::String { val, .. } | Value::Glob { val, .. } => val,
56
+
_ => {
57
+
return Err(ShellError::GenericError {
58
+
error: "invalid source path".into(),
59
+
msg: "source must be a string or glob pattern".into(),
60
+
span: Some(call.arguments_span()),
61
+
help: None,
62
+
inner: vec![],
63
+
});
64
+
}
65
+
};
66
+
67
+
// Prevent moving root
68
+
if source_str == "/" {
69
+
return Err(ShellError::GenericError {
70
+
error: "cannot move root".to_string(),
71
+
msg: "refusing to move root directory".to_string(),
72
+
span: Some(call.arguments_span()),
73
+
help: None,
74
+
inner: vec![],
75
+
});
76
+
}
77
+
78
+
// Expand source path (glob or single) into list of paths
79
+
let is_absolute = source_str.starts_with('/');
80
+
let base_path: Arc<vfs::VfsPath> = if is_absolute { get_vfs() } else { get_pwd() };
81
+
82
+
let options = GlobOptions {
83
+
max_depth: None,
84
+
no_dirs: false,
85
+
no_files: false,
86
+
};
87
+
88
+
let matches = expand_path(&source_str, base_path.clone(), options)?;
89
+
let is_glob = matches.len() > 1
90
+
|| source_str.contains('*')
91
+
|| source_str.contains('?')
92
+
|| source_str.contains('[')
93
+
|| source_str.contains("**");
94
+
95
+
// Resolve destination
96
+
let dest = get_pwd()
97
+
.join(dest_path.trim_end_matches('/'))
98
+
.map_err(to_shell_err(call.arguments_span()))?;
99
+
100
+
// For glob patterns, destination must be a directory
101
+
if is_glob {
102
+
let dest_meta = dest
103
+
.metadata()
104
+
.map_err(to_shell_err(call.arguments_span()))?;
105
+
if dest_meta.file_type != VfsFileType::Directory {
106
+
return Err(ShellError::GenericError {
107
+
error: "destination must be a directory".to_string(),
108
+
msg: "when using glob patterns, destination must be a directory".to_string(),
109
+
span: Some(call.arguments_span()),
110
+
help: None,
111
+
inner: vec![],
112
+
});
113
+
}
114
+
}
115
+
116
+
// Move each matching file/directory
117
+
for rel_path in matches {
118
+
let source = base_path
119
+
.join(&rel_path)
120
+
.map_err(to_shell_err(call.arguments_span()))?;
121
+
let source_meta = source
122
+
.metadata()
123
+
.map_err(to_shell_err(call.arguments_span()))?;
124
+
125
+
// Determine destination path
126
+
let dest_entry = if is_glob {
127
+
// For glob patterns, use filename in destination directory
128
+
let filename = rel_path.split('/').last().unwrap_or(&rel_path);
129
+
dest.join(filename)
130
+
.map_err(to_shell_err(call.arguments_span()))?
131
+
} else {
132
+
// For single path, use destination as-is
133
+
dest.clone()
134
+
};
135
+
136
+
match source_meta.file_type {
137
+
VfsFileType::File => move_file(&source, &dest_entry, call.arguments_span())?,
138
+
VfsFileType::Directory => {
139
+
move_directory(&source, &dest_entry, call.arguments_span())?
140
+
}
141
+
}
142
+
}
143
+
144
+
Ok(PipelineData::Empty)
145
+
}
146
+
}
147
+
148
+
fn move_file(
149
+
source: &vfs::VfsPath,
150
+
dest: &vfs::VfsPath,
151
+
span: nu_protocol::Span,
152
+
) -> Result<(), ShellError> {
153
+
// Read source file content
154
+
let mut source_file = source.open_file().map_err(to_shell_err(span))?;
155
+
156
+
let mut contents = Vec::new();
157
+
source_file
158
+
.read_to_end(&mut contents)
159
+
.map_err(|e| ShellError::GenericError {
160
+
error: "io error".to_string(),
161
+
msg: format!("failed to read source file: {}", e),
162
+
span: Some(span),
163
+
help: None,
164
+
inner: vec![],
165
+
})?;
166
+
167
+
// Create destination file and write content
168
+
dest.create_file()
169
+
.map_err(to_shell_err(span))
170
+
.and_then(|mut f| {
171
+
f.write_all(&contents)
172
+
.map_err(VfsError::from)
173
+
.map_err(to_shell_err(span))
174
+
})?;
175
+
176
+
// Remove source file
177
+
source.remove_file().map_err(to_shell_err(span))?;
178
+
179
+
Ok(())
180
+
}
181
+
182
+
fn move_directory(
183
+
source: &vfs::VfsPath,
184
+
dest: &vfs::VfsPath,
185
+
span: nu_protocol::Span,
186
+
) -> Result<(), ShellError> {
187
+
// Try to create destination directory (create_dir_all handles parent creation)
188
+
// If it already exists, that's fine - we'll move entries into it
189
+
let _ = dest.create_dir_all().map_err(to_shell_err(span));
190
+
191
+
// Recursively move all entries
192
+
let entries = source.read_dir().map_err(to_shell_err(span))?;
193
+
for entry_name in entries {
194
+
let source_entry = source
195
+
.join(entry_name.as_str())
196
+
.map_err(to_shell_err(span))?;
197
+
let dest_entry = dest.join(entry_name.as_str()).map_err(to_shell_err(span))?;
198
+
199
+
let entry_meta = source_entry.metadata().map_err(to_shell_err(span))?;
200
+
match entry_meta.file_type {
201
+
VfsFileType::File => move_file(&source_entry, &dest_entry, span)?,
202
+
VfsFileType::Directory => move_directory(&source_entry, &dest_entry, span)?,
203
+
}
204
+
}
205
+
206
+
// Remove source directory
207
+
source.remove_dir_all().map_err(to_shell_err(span))?;
208
+
209
+
Ok(())
210
+
}
+128
-30
src/cmd/open.rs
+128
-30
src/cmd/open.rs
···
1
1
use std::ops::Not;
2
2
3
-
use crate::globals::{get_pwd, to_shell_err};
3
+
use crate::{
4
+
cmd::glob::{GlobOptions, expand_path},
5
+
globals::{get_pwd, get_vfs},
6
+
};
4
7
use nu_command::{FromCsv, FromJson, FromOds, FromToml, FromTsv, FromXlsx, FromXml, FromYaml};
5
8
use nu_engine::CallExt;
6
9
use nu_protocol::{
7
-
ByteStream, Category, PipelineData, ShellError, Signature, SyntaxShape,
10
+
ByteStream, Category, ListStream, PipelineData, ShellError, Signature, SyntaxShape, Type,
11
+
Value,
8
12
engine::{Command, EngineState, Stack},
9
13
};
14
+
use std::sync::Arc;
10
15
11
16
#[derive(Clone)]
12
17
pub struct Open;
···
18
23
19
24
fn signature(&self) -> Signature {
20
25
Signature::build("open")
21
-
.required("path", SyntaxShape::String, "path to the file")
26
+
.required(
27
+
"path",
28
+
SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]),
29
+
"path to the file",
30
+
)
22
31
.switch(
23
32
"raw",
24
33
"output content as raw string/binary without parsing",
25
34
Some('r'),
26
35
)
36
+
.input_output_type(Type::Nothing, Type::one_of([Type::String, Type::Binary]))
27
37
.category(Category::FileSystem)
28
38
}
29
39
···
38
48
call: &nu_protocol::engine::Call,
39
49
_input: PipelineData,
40
50
) -> Result<PipelineData, ShellError> {
41
-
let path: String = call.req(engine_state, stack, 0)?;
51
+
let path_value: Value = call.req(engine_state, stack, 0)?;
42
52
let raw_flag = call.has_flag(engine_state, stack, "raw")?;
43
53
44
-
let target_file = get_pwd().join(&path).map_err(to_shell_err(call.head))?;
54
+
let path_str = match path_value {
55
+
Value::String { val, .. } | Value::Glob { val, .. } => val,
56
+
_ => {
57
+
return Err(ShellError::GenericError {
58
+
error: "invalid path".into(),
59
+
msg: "path must be a string or glob pattern".into(),
60
+
span: Some(call.head),
61
+
help: None,
62
+
inner: vec![],
63
+
});
64
+
}
65
+
};
66
+
67
+
// Expand path (glob or single) into list of paths
68
+
let is_absolute = path_str.starts_with('/');
69
+
let base_path: Arc<vfs::VfsPath> = if is_absolute { get_vfs() } else { get_pwd() };
70
+
71
+
let options = GlobOptions {
72
+
max_depth: None,
73
+
no_dirs: true, // Only open files, not directories
74
+
no_files: false,
75
+
};
45
76
46
-
let parse_cmd = raw_flag
47
-
.not()
48
-
.then(|| {
49
-
target_file
50
-
.extension()
51
-
.and_then(|ext| get_cmd_for_ext(&ext))
52
-
})
53
-
.flatten();
77
+
let matches = expand_path(&path_str, base_path.clone(), options)?;
54
78
55
-
target_file
56
-
.open_file()
57
-
.map_err(to_shell_err(call.head))
58
-
.and_then(|f| {
59
-
let data = PipelineData::ByteStream(
60
-
ByteStream::read(
61
-
f,
62
-
call.head,
63
-
engine_state.signals().clone(),
64
-
nu_protocol::ByteStreamType::String,
65
-
),
66
-
None,
67
-
);
68
-
if let Some(cmd) = parse_cmd {
69
-
return cmd.run(engine_state, stack, call, data);
79
+
let span = call.head;
80
+
let signals = engine_state.signals().clone();
81
+
82
+
// Open each matching file
83
+
let mut results = Vec::new();
84
+
for rel_path in matches {
85
+
let target_file = match base_path.join(&rel_path) {
86
+
Ok(p) => p,
87
+
Err(e) => {
88
+
results.push(Value::error(
89
+
ShellError::GenericError {
90
+
error: "path error".into(),
91
+
msg: e.to_string(),
92
+
span: Some(span),
93
+
help: None,
94
+
inner: vec![],
95
+
},
96
+
span,
97
+
));
98
+
continue;
70
99
}
71
-
Ok(data)
72
-
})
100
+
};
101
+
102
+
let parse_cmd = raw_flag
103
+
.not()
104
+
.then(|| {
105
+
target_file
106
+
.extension()
107
+
.and_then(|ext| get_cmd_for_ext(&ext))
108
+
})
109
+
.flatten();
110
+
111
+
match target_file.open_file() {
112
+
Ok(f) => {
113
+
let data = PipelineData::ByteStream(
114
+
ByteStream::read(
115
+
f,
116
+
span,
117
+
signals.clone(),
118
+
nu_protocol::ByteStreamType::String,
119
+
),
120
+
None,
121
+
);
122
+
123
+
let value = if let Some(cmd) = parse_cmd {
124
+
match cmd.run(engine_state, stack, call, data) {
125
+
Ok(pipeline_data) => {
126
+
// Convert pipeline data to value
127
+
pipeline_data
128
+
.into_value(span)
129
+
.unwrap_or_else(|e| Value::error(e, span))
130
+
}
131
+
Err(e) => Value::error(e, span),
132
+
}
133
+
} else {
134
+
data.into_value(span)
135
+
.unwrap_or_else(|e| Value::error(e, span))
136
+
};
137
+
results.push(value);
138
+
}
139
+
Err(e) => {
140
+
results.push(Value::error(
141
+
ShellError::GenericError {
142
+
error: "io error".into(),
143
+
msg: format!("failed to open file {}: {}", rel_path, e),
144
+
span: Some(span),
145
+
help: None,
146
+
inner: vec![],
147
+
},
148
+
span,
149
+
));
150
+
}
151
+
}
152
+
}
153
+
154
+
// If single file, return the single result directly (for backward compatibility)
155
+
if results.len() == 1
156
+
&& !path_str.contains('*')
157
+
&& !path_str.contains('?')
158
+
&& !path_str.contains('[')
159
+
&& !path_str.contains("**")
160
+
{
161
+
match results.into_iter().next().unwrap() {
162
+
Value::Error { error, .. } => Err(*error),
163
+
val => Ok(PipelineData::Value(val, None)),
164
+
}
165
+
} else {
166
+
Ok(PipelineData::list_stream(
167
+
ListStream::new(results.into_iter(), span, signals.clone()),
168
+
None,
169
+
))
170
+
}
73
171
}
74
172
}
75
173
+46
src/cmd/print.rs
+46
src/cmd/print.rs
···
1
+
use crate::globals::print_to_console;
2
+
use nu_engine::CallExt;
3
+
use nu_protocol::{
4
+
Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
5
+
engine::{Command, EngineState, Stack},
6
+
};
7
+
8
+
#[derive(Clone)]
9
+
pub struct Print;
10
+
11
+
impl Command for Print {
12
+
fn name(&self) -> &str {
13
+
"print"
14
+
}
15
+
16
+
fn signature(&self) -> Signature {
17
+
Signature::build("print")
18
+
.rest("rest", SyntaxShape::Any, "values to print")
19
+
.input_output_type(Type::Nothing, Type::Nothing)
20
+
.category(Category::Strings)
21
+
}
22
+
23
+
fn description(&self) -> &str {
24
+
"print values to the console."
25
+
}
26
+
27
+
fn run(
28
+
&self,
29
+
engine_state: &EngineState,
30
+
stack: &mut Stack,
31
+
call: &nu_protocol::engine::Call,
32
+
_input: PipelineData,
33
+
) -> Result<PipelineData, ShellError> {
34
+
let rest: Vec<Value> = call.rest(engine_state, stack, 0)?;
35
+
36
+
let mut parts = Vec::new();
37
+
for value in rest {
38
+
let s = value.to_expanded_string(" ", &engine_state.config);
39
+
parts.push(s);
40
+
}
41
+
let output = parts.join(" ");
42
+
print_to_console(&output, true);
43
+
44
+
Ok(PipelineData::Empty)
45
+
}
46
+
}
+4
-1
src/cmd/pwd.rs
+4
-1
src/cmd/pwd.rs
···
1
1
use crate::globals::get_pwd;
2
+
use nu_protocol::Type;
2
3
use nu_protocol::engine::Call;
3
4
use nu_protocol::{
4
5
Category, IntoPipelineData, PipelineData, ShellError, Signature, Value,
···
14
15
}
15
16
16
17
fn signature(&self) -> Signature {
17
-
Signature::build("pwd").category(Category::FileSystem)
18
+
Signature::build("pwd")
19
+
.input_output_type(Type::Nothing, Type::String)
20
+
.category(Category::FileSystem)
18
21
}
19
22
20
23
fn description(&self) -> &str {
+53
-24
src/cmd/rm.rs
+53
-24
src/cmd/rm.rs
···
1
-
use crate::globals::{get_pwd, to_shell_err};
1
+
use crate::{
2
+
cmd::glob::{GlobOptions, expand_path},
3
+
error::to_shell_err,
4
+
globals::{get_pwd, get_vfs},
5
+
};
2
6
use nu_engine::CallExt;
3
7
use nu_protocol::{
4
-
Category, PipelineData, ShellError, Signature, SyntaxShape,
8
+
Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
5
9
engine::{Command, EngineState, Stack},
6
10
};
11
+
use std::sync::Arc;
7
12
use vfs::VfsFileType;
8
13
9
14
#[derive(Clone)]
···
18
23
Signature::build("rm")
19
24
.required(
20
25
"path",
21
-
SyntaxShape::String,
26
+
SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]),
22
27
"path to file or directory to remove",
23
28
)
24
29
.switch(
···
26
31
"remove directories and their contents recursively",
27
32
Some('r'),
28
33
)
34
+
.input_output_type(Type::Nothing, Type::Nothing)
29
35
.category(Category::FileSystem)
30
36
}
31
37
···
40
46
call: &nu_protocol::engine::Call,
41
47
_input: PipelineData,
42
48
) -> Result<PipelineData, ShellError> {
43
-
let path: String = call.req(engine_state, stack, 0)?;
49
+
let path_value: Value = call.req(engine_state, stack, 0)?;
44
50
let recursive = call.has_flag(engine_state, stack, "recursive")?;
45
51
52
+
let path_str = match path_value {
53
+
Value::String { val, .. } | Value::Glob { val, .. } => val,
54
+
_ => {
55
+
return Err(ShellError::GenericError {
56
+
error: "invalid path".into(),
57
+
msg: "path must be a string or glob pattern".into(),
58
+
span: Some(call.head),
59
+
help: None,
60
+
inner: vec![],
61
+
});
62
+
}
63
+
};
64
+
46
65
// Prevent removing root
47
-
if path == "/" {
66
+
if path_str == "/" {
48
67
return Err(ShellError::GenericError {
49
68
error: "cannot remove root".to_string(),
50
69
msg: "refusing to remove root directory".to_string(),
···
54
73
});
55
74
}
56
75
57
-
// Resolve target relative to PWD (or absolute if path starts with '/')
58
-
let target = get_pwd()
59
-
.join(path.trim_end_matches('/'))
60
-
.map_err(to_shell_err(call.head))?;
76
+
// Expand path (glob or single) into list of paths
77
+
let is_absolute = path_str.starts_with('/');
78
+
let base_path: Arc<vfs::VfsPath> = if is_absolute { get_vfs() } else { get_pwd() };
79
+
80
+
let options = GlobOptions {
81
+
max_depth: None,
82
+
no_dirs: false,
83
+
no_files: false,
84
+
};
85
+
86
+
let matches = expand_path(&path_str, base_path.clone(), options)?;
61
87
62
-
let meta = target.metadata().map_err(to_shell_err(call.head))?;
63
-
match meta.file_type {
64
-
VfsFileType::File => {
65
-
target.remove_file().map_err(to_shell_err(call.head))?;
66
-
Ok(PipelineData::Empty)
67
-
}
68
-
VfsFileType::Directory => {
69
-
(if recursive {
70
-
target.remove_dir_all()
71
-
} else {
72
-
// non-recursive: attempt to remove directory (will fail if not empty)
73
-
target.remove_dir()
74
-
})
75
-
.map_err(to_shell_err(call.head))
76
-
.map(|_| PipelineData::Empty)
88
+
// Remove all matching paths
89
+
for rel_path in matches {
90
+
let target = base_path.join(&rel_path).map_err(to_shell_err(call.head))?;
91
+
let meta = target.metadata().map_err(to_shell_err(call.head))?;
92
+
match meta.file_type {
93
+
VfsFileType::File => {
94
+
target.remove_file().map_err(to_shell_err(call.head))?;
95
+
}
96
+
VfsFileType::Directory => {
97
+
(if recursive {
98
+
target.remove_dir_all()
99
+
} else {
100
+
target.remove_dir()
101
+
})
102
+
.map_err(to_shell_err(call.head))?;
103
+
}
77
104
}
78
105
}
106
+
107
+
Ok(PipelineData::Empty)
79
108
}
80
109
}
+2
-2
src/cmd/save.rs
+2
-2
src/cmd/save.rs
···
1
-
use crate::globals::{get_pwd, to_shell_err};
1
+
use crate::{error::to_shell_err, globals::get_pwd};
2
2
use nu_engine::CallExt;
3
3
use nu_protocol::{
4
4
Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
···
16
16
17
17
fn signature(&self) -> Signature {
18
18
Signature::build("save")
19
-
.required("path", SyntaxShape::String, "path to write the data to")
19
+
.required("path", SyntaxShape::Filepath, "path to write the data to")
20
20
.input_output_types(vec![(Type::Any, Type::Nothing)])
21
21
.category(Category::FileSystem)
22
22
}
-89
src/cmd/source.rs
-89
src/cmd/source.rs
···
1
-
use crate::globals::{get_pwd, queue_delta, to_shell_err};
2
-
use nu_engine::{CallExt, command_prelude::IoError, eval_block};
3
-
use nu_parser::parse;
4
-
use nu_protocol::{
5
-
Category, PipelineData, ShellError, Signature, SyntaxShape,
6
-
debugger::WithoutDebug,
7
-
engine::{Command, EngineState, Stack, StateWorkingSet},
8
-
};
9
-
use std::{io::Read, path::PathBuf, str::FromStr};
10
-
11
-
#[derive(Clone)]
12
-
pub struct Source;
13
-
14
-
impl Command for Source {
15
-
fn name(&self) -> &str {
16
-
"source"
17
-
}
18
-
19
-
fn signature(&self) -> Signature {
20
-
Signature::build(self.name())
21
-
.required("filename", SyntaxShape::String, "the file to source")
22
-
.category(Category::Core)
23
-
}
24
-
25
-
fn description(&self) -> &str {
26
-
"source a file from the virtual filesystem."
27
-
}
28
-
29
-
fn run(
30
-
&self,
31
-
engine_state: &EngineState,
32
-
stack: &mut Stack,
33
-
call: &nu_protocol::engine::Call,
34
-
input: PipelineData,
35
-
) -> Result<PipelineData, ShellError> {
36
-
let filename: String = call.req(engine_state, stack, 0)?;
37
-
38
-
// 1. Read file from VFS
39
-
let path = get_pwd().join(&filename).map_err(to_shell_err(call.head))?;
40
-
let mut file = path.open_file().map_err(to_shell_err(call.head))?;
41
-
let mut contents = String::new();
42
-
file.read_to_string(&mut contents).map_err(|e| {
43
-
ShellError::Io(IoError::new(
44
-
e,
45
-
call.head,
46
-
PathBuf::from_str(path.as_str()).unwrap(),
47
-
))
48
-
})?;
49
-
50
-
// 2. Parse the content
51
-
// We create a new working set based on the CURRENT engine state.
52
-
let mut working_set = StateWorkingSet::new(engine_state);
53
-
54
-
// We must add the file to the working set so the parser can track spans correctly
55
-
let _file_id = working_set.add_file(filename.clone(), contents.as_bytes());
56
-
57
-
// Parse the block
58
-
let block = parse(
59
-
&mut working_set,
60
-
Some(&filename),
61
-
contents.as_bytes(),
62
-
false,
63
-
);
64
-
65
-
if let Some(err) = working_set.parse_errors.first() {
66
-
return Err(ShellError::GenericError {
67
-
error: "Parse error".into(),
68
-
msg: err.to_string(),
69
-
span: Some(call.head),
70
-
help: None,
71
-
inner: vec![],
72
-
});
73
-
}
74
-
75
-
// 3. Prepare execution context
76
-
// We clone the engine state to merge the new definitions (delta) locally.
77
-
// This ensures the script can call its own defined functions immediately.
78
-
let mut local_state = engine_state.clone();
79
-
local_state.merge_delta(working_set.delta.clone())?;
80
-
81
-
// 4. Queue the delta for the global engine state
82
-
// This allows definitions to be available in the next command execution cycle (REPL behavior).
83
-
queue_delta(working_set.delta);
84
-
85
-
// 5. Evaluate the block
86
-
// We pass the MUTABLE stack, so environment variable changes (PWD, load-env) WILL persist.
87
-
eval_block::<WithoutDebug>(&local_state, stack, &block, input).map(|data| data.body)
88
-
}
89
-
}
+161
src/cmd/source_file.rs
+161
src/cmd/source_file.rs
···
1
+
use crate::{
2
+
cmd::glob::glob_match,
3
+
error::{CommandError, to_shell_err},
4
+
globals::{get_pwd, get_vfs, print_to_console, set_pwd},
5
+
};
6
+
use nu_engine::{CallExt, get_eval_block_with_early_return};
7
+
use nu_parser::parse;
8
+
use nu_protocol::{
9
+
Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
10
+
engine::{Command, EngineState, Stack, StateWorkingSet},
11
+
};
12
+
use std::sync::Arc;
13
+
14
+
#[derive(Clone)]
15
+
pub struct SourceFile;
16
+
17
+
impl Command for SourceFile {
18
+
fn name(&self) -> &str {
19
+
"eval file"
20
+
}
21
+
22
+
fn signature(&self) -> Signature {
23
+
Signature::build(self.name())
24
+
.required(
25
+
"path",
26
+
SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::GlobPattern]),
27
+
"the file to source",
28
+
)
29
+
.input_output_type(Type::Nothing, Type::Nothing)
30
+
.category(Category::Core)
31
+
}
32
+
33
+
fn description(&self) -> &str {
34
+
"sources a file from the virtual filesystem."
35
+
}
36
+
37
+
fn run(
38
+
&self,
39
+
engine_state: &EngineState,
40
+
stack: &mut Stack,
41
+
call: &nu_protocol::engine::Call,
42
+
_input: PipelineData,
43
+
) -> Result<PipelineData, ShellError> {
44
+
let span = call.arguments_span();
45
+
let path: Value = call.req(engine_state, stack, 0)?;
46
+
47
+
// Check if path is a glob pattern
48
+
let path_str = match &path {
49
+
Value::String { val, .. } | Value::Glob { val, .. } => val.clone(),
50
+
_ => {
51
+
return Err(ShellError::GenericError {
52
+
error: "not a path or glob pattern".into(),
53
+
msg: String::new(),
54
+
span: Some(span),
55
+
help: None,
56
+
inner: vec![],
57
+
});
58
+
}
59
+
};
60
+
61
+
let pwd = get_pwd();
62
+
let is_absolute = path_str.starts_with('/');
63
+
let base_path: Arc<vfs::VfsPath> = if is_absolute { get_vfs() } else { pwd.clone() };
64
+
65
+
// Check if it's a glob pattern (contains *, ?, [, or **)
66
+
let is_glob = path_str.contains('*')
67
+
|| path_str.contains('?')
68
+
|| path_str.contains('[')
69
+
|| path_str.contains("**");
70
+
71
+
let paths_to_source = if is_glob {
72
+
// Expand glob pattern
73
+
let options = crate::cmd::glob::GlobOptions {
74
+
max_depth: None,
75
+
no_dirs: true, // Only source files, not directories
76
+
no_files: false,
77
+
};
78
+
glob_match(&path_str, base_path.clone(), options)?
79
+
} else {
80
+
// Single file path
81
+
vec![path_str]
82
+
};
83
+
84
+
// Source each matching file
85
+
for rel_path in paths_to_source {
86
+
let full_path = base_path.join(&rel_path).map_err(to_shell_err(span))?;
87
+
88
+
let metadata = full_path.metadata().map_err(to_shell_err(span))?;
89
+
if metadata.file_type != vfs::VfsFileType::File {
90
+
continue;
91
+
}
92
+
93
+
let contents = full_path.read_to_string().map_err(to_shell_err(span))?;
94
+
95
+
set_pwd(full_path.parent().into());
96
+
let res = eval(engine_state, stack, &contents, Some(&full_path.filename()));
97
+
set_pwd(pwd.clone());
98
+
99
+
match res {
100
+
Ok(p) => {
101
+
print_to_console(&p.collect_string("\n", &engine_state.config)?, true);
102
+
}
103
+
Err(err) => {
104
+
let msg: String = err.into();
105
+
print_to_console(&msg, true);
106
+
return Err(ShellError::GenericError {
107
+
error: "source error".into(),
108
+
msg: format!("can't source file: {}", rel_path),
109
+
span: Some(span),
110
+
help: None,
111
+
inner: vec![],
112
+
});
113
+
}
114
+
}
115
+
}
116
+
117
+
Ok(PipelineData::Empty)
118
+
}
119
+
}
120
+
121
+
pub fn eval(
122
+
engine_state: &EngineState,
123
+
stack: &mut Stack,
124
+
contents: &str,
125
+
filename: Option<&str>,
126
+
) -> Result<PipelineData, CommandError> {
127
+
let filename = filename.unwrap_or("<piped data>");
128
+
let mut working_set = StateWorkingSet::new(engine_state);
129
+
let start_offset = working_set.next_span_start();
130
+
let _ = working_set.add_file(filename.into(), contents.as_bytes());
131
+
132
+
let block = parse(&mut working_set, Some(filename), contents.as_bytes(), false);
133
+
134
+
if let Some(err) = working_set.parse_errors.into_iter().next() {
135
+
web_sys::console::error_1(&err.to_string().into());
136
+
return Err(CommandError::new(err, contents).with_start_offset(start_offset));
137
+
}
138
+
if let Some(err) = working_set.compile_errors.into_iter().next() {
139
+
web_sys::console::error_1(&err.to_string().into());
140
+
return Err(CommandError::new(err, contents).with_start_offset(start_offset));
141
+
}
142
+
143
+
// uhhhhh this is safe prolly cuz we are single threaded
144
+
// i mean still shouldnt do this but i lowkey dont care so :3
145
+
let engine_state = unsafe {
146
+
std::ptr::from_ref(engine_state)
147
+
.cast_mut()
148
+
.as_mut()
149
+
.unwrap()
150
+
};
151
+
engine_state
152
+
.merge_delta(working_set.delta)
153
+
.map_err(|err| CommandError::new(err, contents).with_start_offset(start_offset))?;
154
+
155
+
// queue_delta(working_set.delta.clone());
156
+
157
+
let eval_block_with_early_return = get_eval_block_with_early_return(&engine_state);
158
+
eval_block_with_early_return(&engine_state, stack, &block, PipelineData::Empty)
159
+
.map(|d| d.body)
160
+
.map_err(|err| CommandError::new(err, contents).with_start_offset(start_offset))
161
+
}
+10
-1
src/cmd/sys.rs
+10
-1
src/cmd/sys.rs
···
1
1
use js_sys::Reflect;
2
2
use js_sys::global;
3
+
use nu_protocol::Type;
3
4
use nu_protocol::{
4
5
Category, IntoPipelineData, PipelineData, Record, ShellError, Signature, Value,
5
6
engine::{Command, EngineState, Stack},
···
15
16
}
16
17
17
18
fn signature(&self) -> Signature {
18
-
Signature::build("sys").category(Category::System)
19
+
Signature::build("sys")
20
+
.input_output_type(Type::Nothing, Type::record())
21
+
.category(Category::System)
19
22
}
20
23
21
24
fn description(&self) -> &str {
···
176
179
Value::string("not running in a browser environment", head),
177
180
);
178
181
}
182
+
183
+
let date = compile_time::unix!();
184
+
let rustc = compile_time::rustc_version_str!();
185
+
186
+
rec.push("build_time", Value::int(date, head));
187
+
rec.push("rustc_version", Value::string(rustc, head));
179
188
180
189
Ok(Value::record(rec, head).into_pipeline_data())
181
190
}
-32
src/cmd/version.rs
-32
src/cmd/version.rs
···
1
-
use nu_protocol::engine::Call;
2
-
use nu_protocol::{
3
-
Category, IntoPipelineData, PipelineData, ShellError, Signature, Value,
4
-
engine::{Command, EngineState, Stack},
5
-
};
6
-
7
-
#[derive(Clone)]
8
-
pub struct Version;
9
-
10
-
impl Command for Version {
11
-
fn name(&self) -> &str {
12
-
"version"
13
-
}
14
-
15
-
fn signature(&self) -> Signature {
16
-
Signature::build(self.name()).category(Category::System)
17
-
}
18
-
19
-
fn description(&self) -> &str {
20
-
"print the version of dysnomia."
21
-
}
22
-
23
-
fn run(
24
-
&self,
25
-
_engine_state: &EngineState,
26
-
_stack: &mut Stack,
27
-
call: &Call,
28
-
_input: PipelineData,
29
-
) -> Result<PipelineData, ShellError> {
30
-
Ok(Value::string("dysnomia.v099.t1765660500", call.head).into_pipeline_data())
31
-
}
32
-
}
+1163
src/completion/context.rs
+1163
src/completion/context.rs
···
1
+
use crate::completion::helpers::*;
2
+
use crate::completion::types::{CompletionContext, CompletionKind};
3
+
use crate::console_log;
4
+
use nu_parser::FlatShape;
5
+
use nu_protocol::engine::{EngineState, StateWorkingSet};
6
+
use nu_protocol::{Signature, Span};
7
+
8
+
pub fn find_command_and_arg_index(
9
+
input: &str,
10
+
shapes: &[(Span, FlatShape)],
11
+
current_idx: usize,
12
+
current_local_span: Span,
13
+
global_offset: usize,
14
+
) -> Option<(String, usize)> {
15
+
let mut command_name: Option<String> = None;
16
+
let mut arg_count = 0;
17
+
18
+
// Look backwards through shapes to find the command
19
+
for i in (0..current_idx).rev() {
20
+
if let Some((prev_span, prev_shape)) = shapes.get(i) {
21
+
let prev_local_span = to_local_span(*prev_span, global_offset);
22
+
23
+
// Check if there's a separator between this shape and the next one
24
+
let next_shape_start = if i + 1 < shapes.len() {
25
+
to_local_span(shapes[i + 1].0, global_offset).start
26
+
} else {
27
+
current_local_span.start
28
+
};
29
+
30
+
if has_separator_between(input, prev_local_span.end, next_shape_start) {
31
+
break; // Stop at separator
32
+
}
33
+
34
+
if is_command_shape(input, prev_shape, prev_local_span) {
35
+
// Found the command
36
+
let cmd_text = safe_slice(input, prev_local_span);
37
+
let cmd_name = extract_command_name(cmd_text);
38
+
command_name = Some(cmd_name.to_string());
39
+
break;
40
+
} else {
41
+
// This is an argument - count it if it's not a flag
42
+
let arg_text = safe_slice(input, prev_local_span);
43
+
let trimmed_arg = arg_text.trim();
44
+
// Don't count flags (starting with -) or empty arguments
45
+
if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') {
46
+
arg_count += 1;
47
+
}
48
+
}
49
+
}
50
+
}
51
+
52
+
command_name.map(|name| (name, arg_count))
53
+
}
54
+
55
+
pub fn build_command_prefix(
56
+
input: &str,
57
+
shapes: &[(Span, FlatShape)],
58
+
current_idx: usize,
59
+
current_local_span: Span,
60
+
current_prefix: &str,
61
+
global_offset: usize,
62
+
) -> (String, Span) {
63
+
let mut span_start = current_local_span.start;
64
+
65
+
// Look backwards through shapes to find previous command words
66
+
for i in (0..current_idx).rev() {
67
+
if let Some((prev_span, prev_shape)) = shapes.get(i) {
68
+
let prev_local_span = to_local_span(*prev_span, global_offset);
69
+
70
+
if is_command_shape(input, prev_shape, prev_local_span) {
71
+
// Check if there's a separator between this shape and the next one
72
+
let next_shape_start = if i + 1 < shapes.len() {
73
+
to_local_span(shapes[i + 1].0, global_offset).start
74
+
} else {
75
+
current_local_span.start
76
+
};
77
+
78
+
// Check if there's a separator (pipe, semicolon, etc.) between shapes
79
+
// Whitespace is fine, but separators indicate a new command
80
+
if has_separator_between(input, prev_local_span.end, next_shape_start) {
81
+
break; // Stop at separator
82
+
}
83
+
84
+
// Update span start to include this command word
85
+
span_start = prev_local_span.start;
86
+
} else {
87
+
// Not a command shape, stop looking backwards
88
+
break;
89
+
}
90
+
}
91
+
}
92
+
93
+
// Extract the full prefix from the input, preserving exact spacing
94
+
let span_end = current_local_span.end;
95
+
let full_prefix = if span_start < input.len() {
96
+
safe_slice(input, Span::new(span_start, span_end)).to_string()
97
+
} else {
98
+
current_prefix.to_string()
99
+
};
100
+
101
+
(full_prefix, Span::new(span_start, span_end))
102
+
}
103
+
104
+
pub fn get_command_signature(engine_guard: &EngineState, cmd_name: &str) -> Option<Signature> {
105
+
engine_guard
106
+
.find_decl(cmd_name.as_bytes(), &[])
107
+
.map(|id| engine_guard.get_decl(id).signature())
108
+
}
109
+
110
+
/// Creates CommandArgument context(s), and optionally adds a Command context for subcommands
111
+
/// if we're at argument index 0 and the command has subcommands.
112
+
pub fn create_command_argument_contexts(
113
+
command_name: String,
114
+
arg_index: usize,
115
+
prefix: String,
116
+
span: Span,
117
+
working_set: &StateWorkingSet,
118
+
_engine_guard: &EngineState,
119
+
) -> Vec<CompletionContext> {
120
+
let mut contexts = Vec::new();
121
+
122
+
// Always add the CommandArgument context
123
+
contexts.push(CompletionContext {
124
+
kind: CompletionKind::CommandArgument {
125
+
command_name: command_name.clone(),
126
+
arg_index,
127
+
},
128
+
prefix: prefix.clone(),
129
+
span,
130
+
});
131
+
132
+
// If we're at argument index 0, check if the command has subcommands
133
+
if arg_index == 0 {
134
+
// Check if command has subcommands
135
+
// Subcommands are commands that start with "command_name " (with space)
136
+
let parent_prefix = format!("{} ", command_name);
137
+
let subcommands = working_set
138
+
.find_commands_by_predicate(|value| value.starts_with(parent_prefix.as_bytes()), true);
139
+
140
+
if !subcommands.is_empty() {
141
+
// Command has subcommands - add a Command context for subcommands
142
+
console_log!(
143
+
"[completion] Command {command_name:?} has subcommands, adding Command context for subcommands"
144
+
);
145
+
contexts.push(CompletionContext {
146
+
kind: CompletionKind::Command {
147
+
parent_command: Some(command_name),
148
+
},
149
+
prefix,
150
+
span,
151
+
});
152
+
}
153
+
}
154
+
155
+
contexts
156
+
}
157
+
158
+
pub fn determine_flag_or_argument_context(
159
+
input: &str,
160
+
shapes: &[(Span, FlatShape)],
161
+
prefix: &str,
162
+
idx: usize,
163
+
local_span: Span,
164
+
span: Span,
165
+
global_offset: usize,
166
+
working_set: &StateWorkingSet,
167
+
_engine_guard: &EngineState,
168
+
) -> Vec<CompletionContext> {
169
+
let trimmed_prefix = prefix.trim();
170
+
if trimmed_prefix.starts_with('-') {
171
+
// This looks like a flag - find the command
172
+
if let Some((cmd_name, _)) =
173
+
find_command_and_arg_index(input, shapes, idx, local_span, global_offset)
174
+
{
175
+
vec![CompletionContext {
176
+
kind: CompletionKind::Flag {
177
+
command_name: cmd_name,
178
+
},
179
+
prefix: trimmed_prefix.to_string(),
180
+
span,
181
+
}]
182
+
} else {
183
+
vec![CompletionContext {
184
+
kind: CompletionKind::Argument,
185
+
prefix: prefix.to_string(),
186
+
span,
187
+
}]
188
+
}
189
+
} else {
190
+
// This is a positional argument - find the command and argument index
191
+
if let Some((cmd_name, arg_index)) =
192
+
find_command_and_arg_index(input, shapes, idx, local_span, global_offset)
193
+
{
194
+
create_command_argument_contexts(
195
+
cmd_name,
196
+
arg_index,
197
+
trimmed_prefix.to_string(),
198
+
span,
199
+
working_set,
200
+
_engine_guard,
201
+
)
202
+
} else {
203
+
vec![CompletionContext {
204
+
kind: CompletionKind::Argument,
205
+
prefix: prefix.to_string(),
206
+
span,
207
+
}]
208
+
}
209
+
}
210
+
}
211
+
212
+
pub fn handle_block_or_closure(
213
+
input: &str,
214
+
shapes: &[(Span, FlatShape)],
215
+
working_set: &StateWorkingSet,
216
+
engine_guard: &EngineState,
217
+
prefix: &str,
218
+
span: Span,
219
+
shape_name: &str,
220
+
current_idx: usize,
221
+
local_span: Span,
222
+
global_offset: usize,
223
+
) -> Vec<CompletionContext> {
224
+
console_log!("[completion] Processing {shape_name} shape with prefix: {prefix:?}");
225
+
226
+
// Check if the content ends with a pipe or semicolon
227
+
let prefix_ends_with_separator = ends_with_separator(prefix);
228
+
let last_sep_pos_in_prefix = if prefix_ends_with_separator {
229
+
find_last_separator_pos(prefix)
230
+
} else {
231
+
None
232
+
};
233
+
console_log!(
234
+
"[completion] {shape_name}: prefix_ends_with_separator={prefix_ends_with_separator}, last_sep_pos_in_prefix={last_sep_pos_in_prefix:?}"
235
+
);
236
+
237
+
if let Some((trimmed_prefix, adjusted_span, is_empty)) = handle_block_prefix(prefix, span) {
238
+
console_log!(
239
+
"[completion] {shape_name}: trimmed_prefix={trimmed_prefix:?}, is_empty={is_empty}"
240
+
);
241
+
242
+
if is_empty {
243
+
// Empty block/closure or just whitespace
244
+
// Check if there's a command shape before this closure/block shape
245
+
// If so, we might be completing after that command
246
+
let mut found_command: Option<String> = None;
247
+
for i in (0..current_idx).rev() {
248
+
if let Some((prev_span, prev_shape)) = shapes.get(i) {
249
+
let prev_local_span = to_local_span(*prev_span, global_offset);
250
+
// Check if this shape is before the current closure and is a command
251
+
if prev_local_span.end <= local_span.start {
252
+
if is_command_shape(input, prev_shape, prev_local_span) {
253
+
let cmd_text = safe_slice(input, prev_local_span);
254
+
let cmd_full = cmd_text.trim().to_string();
255
+
256
+
// Extract the full command text - if it contains spaces, it might be a subcommand
257
+
// We'll use the first word for parent_command to show subcommands
258
+
// The suggestion generator will filter appropriately
259
+
let cmd_first_word = extract_command_name(cmd_text).to_string();
260
+
261
+
// If the command contains spaces, it's likely a full command (subcommand)
262
+
// In that case, we shouldn't show subcommands
263
+
if cmd_full.contains(' ') && cmd_full != cmd_first_word {
264
+
// It's a full command (subcommand), don't show subcommands
265
+
console_log!(
266
+
"[completion] {shape_name} is empty but found full command {cmd_full:?} before it, not showing completions"
267
+
);
268
+
return Vec::new();
269
+
}
270
+
271
+
// Use the first word to show subcommands
272
+
found_command = Some(cmd_first_word);
273
+
console_log!(
274
+
"[completion] {shape_name} is empty but found command {found_command:?} before it"
275
+
);
276
+
break;
277
+
}
278
+
}
279
+
}
280
+
}
281
+
282
+
if let Some(cmd_name) = found_command {
283
+
// We found a command before the closure, show subcommands of that command
284
+
console_log!(
285
+
"[completion] {shape_name} is empty, showing subcommands of {cmd_name:?}"
286
+
);
287
+
vec![CompletionContext {
288
+
kind: CompletionKind::Command {
289
+
parent_command: Some(cmd_name),
290
+
},
291
+
prefix: String::new(),
292
+
span: adjusted_span,
293
+
}]
294
+
} else {
295
+
// Truly empty - show all commands
296
+
console_log!("[completion] {shape_name} is empty, setting Command context");
297
+
vec![CompletionContext {
298
+
kind: CompletionKind::Command {
299
+
parent_command: None,
300
+
},
301
+
prefix: String::new(),
302
+
span: adjusted_span,
303
+
}]
304
+
}
305
+
} else if let Some(last_sep_pos) = last_sep_pos_in_prefix {
306
+
// After a separator - command context
307
+
let after_sep = prefix[last_sep_pos..].trim_start();
308
+
console_log!(
309
+
"[completion] {shape_name} has separator at {last_sep_pos}, after_sep={after_sep:?}, setting Command context"
310
+
);
311
+
vec![CompletionContext {
312
+
kind: CompletionKind::Command {
313
+
parent_command: None,
314
+
},
315
+
prefix: after_sep.to_string(),
316
+
span: Span::new(span.start + last_sep_pos, span.end),
317
+
}]
318
+
} else {
319
+
console_log!(
320
+
"[completion] {shape_name} has no separator, checking for variable/flag/argument context"
321
+
);
322
+
// Check if this is a variable or cell path first
323
+
let trimmed = trimmed_prefix.trim();
324
+
325
+
if trimmed.starts_with('$') {
326
+
// Variable or cell path completion
327
+
if let Some((var_name, path_so_far, cell_prefix)) = parse_cell_path(trimmed) {
328
+
let var_id = lookup_variable_id(var_name, working_set);
329
+
330
+
if let Some(var_id) = var_id {
331
+
let prefix_byte_len = cell_prefix.len();
332
+
let cell_span_start = adjusted_span.end.saturating_sub(prefix_byte_len);
333
+
console_log!(
334
+
"[completion] {shape_name}: Setting CellPath context with var {var_name:?}, prefix {cell_prefix:?}"
335
+
);
336
+
vec![CompletionContext {
337
+
kind: CompletionKind::CellPath {
338
+
var_id,
339
+
path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
340
+
},
341
+
prefix: cell_prefix.to_string(),
342
+
span: Span::new(cell_span_start, adjusted_span.end),
343
+
}]
344
+
} else {
345
+
// Unknown variable, fall back to variable completion
346
+
let var_prefix = trimmed[1..].to_string();
347
+
console_log!(
348
+
"[completion] {shape_name}: Unknown var, setting Variable context with prefix {var_prefix:?}"
349
+
);
350
+
vec![CompletionContext {
351
+
kind: CompletionKind::Variable,
352
+
prefix: var_prefix,
353
+
span: adjusted_span,
354
+
}]
355
+
}
356
+
} else {
357
+
// Simple variable completion (no dot)
358
+
let var_prefix = if trimmed.len() > 1 {
359
+
trimmed[1..].to_string()
360
+
} else {
361
+
String::new()
362
+
};
363
+
console_log!(
364
+
"[completion] {shape_name}: Setting Variable context with prefix {var_prefix:?}"
365
+
);
366
+
vec![CompletionContext {
367
+
kind: CompletionKind::Variable,
368
+
prefix: var_prefix,
369
+
span: adjusted_span,
370
+
}]
371
+
}
372
+
} else if trimmed.starts_with('-') {
373
+
// Flag completion
374
+
if let Some((cmd_name, _)) = find_command_and_arg_index(
375
+
input,
376
+
shapes,
377
+
current_idx,
378
+
local_span,
379
+
global_offset,
380
+
) {
381
+
console_log!(
382
+
"[completion] {shape_name}: Found command {cmd_name:?} for flag completion"
383
+
);
384
+
vec![CompletionContext {
385
+
kind: CompletionKind::Flag {
386
+
command_name: cmd_name,
387
+
},
388
+
prefix: trimmed.to_string(),
389
+
span: adjusted_span,
390
+
}]
391
+
} else {
392
+
vec![CompletionContext {
393
+
kind: CompletionKind::Argument,
394
+
prefix: trimmed_prefix.to_string(),
395
+
span: adjusted_span,
396
+
}]
397
+
}
398
+
} else {
399
+
// Try to find the command and argument index
400
+
if let Some((cmd_name, arg_index)) = find_command_and_arg_index(
401
+
input,
402
+
shapes,
403
+
current_idx,
404
+
local_span,
405
+
global_offset,
406
+
) {
407
+
console_log!(
408
+
"[completion] {shape_name}: Found command {cmd_name:?} with arg_index {arg_index} for argument completion"
409
+
);
410
+
create_command_argument_contexts(
411
+
cmd_name,
412
+
arg_index,
413
+
trimmed.to_string(),
414
+
adjusted_span,
415
+
working_set,
416
+
engine_guard,
417
+
)
418
+
} else {
419
+
// No command found, treat as regular argument
420
+
console_log!(
421
+
"[completion] {shape_name}: No command found, using Argument context"
422
+
);
423
+
vec![CompletionContext {
424
+
kind: CompletionKind::Argument,
425
+
prefix: trimmed_prefix.to_string(),
426
+
span: adjusted_span,
427
+
}]
428
+
}
429
+
}
430
+
}
431
+
} else {
432
+
Vec::new()
433
+
}
434
+
}
435
+
436
+
pub fn handle_variable_string_shape(
437
+
input: &str,
438
+
shapes: &[(Span, FlatShape)],
439
+
working_set: &StateWorkingSet,
440
+
engine_guard: &EngineState,
441
+
idx: usize,
442
+
prefix: &str,
443
+
span: Span,
444
+
local_span: Span,
445
+
global_offset: usize,
446
+
) -> Vec<CompletionContext> {
447
+
if idx == 0 {
448
+
return Vec::new();
449
+
}
450
+
451
+
let prev_shape = &shapes[idx - 1];
452
+
let prev_local_span = to_local_span(prev_shape.0, global_offset);
453
+
454
+
if let FlatShape::Variable(var_id) = prev_shape.1 {
455
+
// Check if the variable shape ends right where this shape starts (or very close)
456
+
// Allow for a small gap (like a dot) between shapes
457
+
let gap = local_span.start.saturating_sub(prev_local_span.end);
458
+
if gap <= 1 {
459
+
// This is a cell path - the String shape contains the field name(s)
460
+
// The prefix might be like "na" or "field.subfield"
461
+
let trimmed_prefix = prefix.trim();
462
+
let (path_so_far, cell_prefix) = parse_cell_path_from_fields(trimmed_prefix);
463
+
464
+
let prefix_byte_len = cell_prefix.len();
465
+
let cell_span_start = span.end.saturating_sub(prefix_byte_len);
466
+
console_log!(
467
+
"[completion] Detected cell path from Variable+String shapes, var_id={var_id:?}, prefix={cell_prefix:?}, path={path_so_far:?}"
468
+
);
469
+
vec![CompletionContext {
470
+
kind: CompletionKind::CellPath {
471
+
var_id,
472
+
path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
473
+
},
474
+
prefix: cell_prefix.to_string(),
475
+
span: Span::new(cell_span_start, span.end),
476
+
}]
477
+
} else {
478
+
// Gap between shapes, use helper to determine context
479
+
determine_flag_or_argument_context(
480
+
input,
481
+
shapes,
482
+
&prefix.trim(),
483
+
idx,
484
+
local_span,
485
+
span,
486
+
global_offset,
487
+
working_set,
488
+
engine_guard,
489
+
)
490
+
}
491
+
} else {
492
+
// Previous shape is not a Variable, use helper to determine context
493
+
determine_flag_or_argument_context(
494
+
input,
495
+
shapes,
496
+
&prefix.trim(),
497
+
idx,
498
+
local_span,
499
+
span,
500
+
global_offset,
501
+
working_set,
502
+
engine_guard,
503
+
)
504
+
}
505
+
}
506
+
507
+
pub fn handle_dot_shape(
508
+
_input: &str,
509
+
shapes: &[(Span, FlatShape)],
510
+
idx: usize,
511
+
prefix: &str,
512
+
span: Span,
513
+
local_span: Span,
514
+
global_offset: usize,
515
+
) -> Vec<CompletionContext> {
516
+
if idx == 0 {
517
+
return vec![CompletionContext {
518
+
kind: CompletionKind::Argument,
519
+
prefix: prefix.to_string(),
520
+
span,
521
+
}];
522
+
}
523
+
524
+
let prev_shape = &shapes[idx - 1];
525
+
let prev_local_span = to_local_span(prev_shape.0, global_offset);
526
+
527
+
if let FlatShape::Variable(var_id) = prev_shape.1 {
528
+
// Check if the variable shape ends right where this shape starts
529
+
if prev_local_span.end == local_span.start {
530
+
let trimmed_prefix = prefix.trim();
531
+
// Parse path members from the prefix (which is like ".field" or ".field.subfield")
532
+
let after_dot = &trimmed_prefix[1..]; // Remove leading dot
533
+
let (path_so_far, cell_prefix) = if after_dot.is_empty() {
534
+
(vec![], "")
535
+
} else {
536
+
parse_cell_path_from_fields(after_dot)
537
+
};
538
+
539
+
let prefix_byte_len = cell_prefix.len();
540
+
let cell_span_start = span.end.saturating_sub(prefix_byte_len);
541
+
console_log!(
542
+
"[completion] Detected cell path from adjacent Variable shape, var_id={var_id:?}, prefix={cell_prefix:?}"
543
+
);
544
+
vec![CompletionContext {
545
+
kind: CompletionKind::CellPath {
546
+
var_id,
547
+
path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
548
+
},
549
+
prefix: cell_prefix.to_string(),
550
+
span: Span::new(cell_span_start, span.end),
551
+
}]
552
+
} else {
553
+
// Gap between shapes, fall through to default handling
554
+
vec![CompletionContext {
555
+
kind: CompletionKind::Argument,
556
+
prefix: prefix.to_string(),
557
+
span,
558
+
}]
559
+
}
560
+
} else {
561
+
// Previous shape is not a Variable, this is likely a file path starting with .
562
+
vec![CompletionContext {
563
+
kind: CompletionKind::Argument,
564
+
prefix: prefix.to_string(),
565
+
span,
566
+
}]
567
+
}
568
+
}
569
+
570
+
pub fn determine_context_from_shape(
571
+
input: &str,
572
+
shapes: &[(Span, FlatShape)],
573
+
working_set: &StateWorkingSet,
574
+
engine_guard: &EngineState,
575
+
byte_pos: usize,
576
+
global_offset: usize,
577
+
) -> Vec<CompletionContext> {
578
+
// First, check if cursor is within a shape
579
+
for (idx, (span, shape)) in shapes.iter().enumerate() {
580
+
let local_span = to_local_span(*span, global_offset);
581
+
582
+
if local_span.start <= byte_pos && byte_pos <= local_span.end {
583
+
console_log!("[completion] Cursor in shape {idx}: {shape:?} at {local_span:?}");
584
+
585
+
// Check if there's a pipe or semicolon between this shape's end and the cursor
586
+
// If so, we're starting a new command and should ignore this shape
587
+
let has_sep = has_separator_between(input, local_span.end, byte_pos);
588
+
if has_sep {
589
+
console_log!(
590
+
"[completion] Separator found between shape end ({end}) and cursor ({byte_pos}), skipping shape",
591
+
end = local_span.end
592
+
);
593
+
// There's a separator, so we're starting a new command - skip this shape
594
+
continue;
595
+
}
596
+
597
+
let span = Span::new(local_span.start, std::cmp::min(local_span.end, byte_pos));
598
+
let prefix = safe_slice(input, span);
599
+
console_log!("[completion] Processing shape {idx} with prefix: {prefix:?}");
600
+
601
+
// Special case: if prefix is just '{' (possibly with whitespace),
602
+
// we're at the start of a block and should complete commands
603
+
let trimmed_prefix = prefix.trim();
604
+
if trimmed_prefix == "{" {
605
+
// We're right after '{' - command context
606
+
if let Some((_, adjusted_span, _)) = handle_block_prefix(&prefix, span) {
607
+
return vec![CompletionContext {
608
+
kind: CompletionKind::Command {
609
+
parent_command: None,
610
+
},
611
+
prefix: String::new(),
612
+
span: adjusted_span,
613
+
}];
614
+
}
615
+
} else {
616
+
match shape {
617
+
// Special case: Check if we're completing a cell path where the Variable and field are in separate shapes
618
+
_ if { idx > 0 && matches!(shape, FlatShape::String) } => {
619
+
let contexts = handle_variable_string_shape(
620
+
input,
621
+
shapes,
622
+
working_set,
623
+
engine_guard,
624
+
idx,
625
+
&prefix,
626
+
span,
627
+
local_span,
628
+
global_offset,
629
+
);
630
+
if !contexts.is_empty() {
631
+
return contexts;
632
+
}
633
+
}
634
+
// Special case: Check if we're completing a cell path where the Variable and dot are in separate shapes
635
+
_ if {
636
+
let trimmed_prefix = prefix.trim();
637
+
trimmed_prefix.starts_with('.') && idx > 0
638
+
} =>
639
+
{
640
+
let contexts = handle_dot_shape(
641
+
input,
642
+
shapes,
643
+
idx,
644
+
&prefix,
645
+
span,
646
+
local_span,
647
+
global_offset,
648
+
);
649
+
if !contexts.is_empty() {
650
+
return contexts;
651
+
}
652
+
}
653
+
_ if {
654
+
// Check if this is a variable or cell path (starts with $) before treating as command
655
+
let trimmed_prefix = prefix.trim();
656
+
trimmed_prefix.starts_with('$')
657
+
} =>
658
+
{
659
+
let trimmed_prefix = prefix.trim();
660
+
// Check if this is a cell path (contains a dot after $)
661
+
if let Some((var_name, path_so_far, cell_prefix)) =
662
+
parse_cell_path(trimmed_prefix)
663
+
{
664
+
// Find the variable ID
665
+
let var_id = lookup_variable_id(var_name, working_set);
666
+
667
+
if let Some(var_id) = var_id {
668
+
// Calculate span for the cell path member being completed
669
+
let prefix_byte_len = cell_prefix.len();
670
+
let cell_span_start = span.end.saturating_sub(prefix_byte_len);
671
+
return vec![CompletionContext {
672
+
kind: CompletionKind::CellPath {
673
+
var_id,
674
+
path_so_far: path_so_far
675
+
.iter()
676
+
.map(|s| s.to_string())
677
+
.collect(),
678
+
},
679
+
prefix: cell_prefix.to_string(),
680
+
span: Span::new(cell_span_start, span.end),
681
+
}];
682
+
} else {
683
+
// Unknown variable, fall back to variable completion
684
+
let var_prefix = trimmed_prefix[1..].to_string();
685
+
return vec![CompletionContext {
686
+
kind: CompletionKind::Variable,
687
+
prefix: var_prefix,
688
+
span,
689
+
}];
690
+
}
691
+
} else {
692
+
// Variable completion context (no dot)
693
+
let var_prefix = if trimmed_prefix.len() > 1 {
694
+
trimmed_prefix[1..].to_string()
695
+
} else {
696
+
String::new()
697
+
};
698
+
return vec![CompletionContext {
699
+
kind: CompletionKind::Variable,
700
+
prefix: var_prefix,
701
+
span,
702
+
}];
703
+
}
704
+
}
705
+
_ if is_command_shape(input, shape, local_span) => {
706
+
let (full_prefix, full_span) =
707
+
build_command_prefix(input, shapes, idx, span, &prefix, global_offset);
708
+
return vec![CompletionContext {
709
+
kind: CompletionKind::Command {
710
+
parent_command: None,
711
+
},
712
+
prefix: full_prefix,
713
+
span: full_span,
714
+
}];
715
+
}
716
+
FlatShape::Block | FlatShape::Closure => {
717
+
let contexts = handle_block_or_closure(
718
+
input,
719
+
shapes,
720
+
working_set,
721
+
engine_guard,
722
+
&prefix,
723
+
span,
724
+
shape.as_str().trim_start_matches("shape_"),
725
+
idx,
726
+
local_span,
727
+
global_offset,
728
+
);
729
+
if !contexts.is_empty() {
730
+
return contexts;
731
+
}
732
+
}
733
+
FlatShape::Variable(var_id) => {
734
+
// Variable or cell path completion context
735
+
let trimmed_prefix = prefix.trim();
736
+
if trimmed_prefix.starts_with('$') {
737
+
// Check if this is a cell path (contains a dot after $)
738
+
if let Some((_, path_so_far, cell_prefix)) =
739
+
parse_cell_path(trimmed_prefix)
740
+
{
741
+
let prefix_byte_len = cell_prefix.len();
742
+
let cell_span_start = span.end.saturating_sub(prefix_byte_len);
743
+
return vec![CompletionContext {
744
+
kind: CompletionKind::CellPath {
745
+
var_id: *var_id,
746
+
path_so_far: path_so_far
747
+
.iter()
748
+
.map(|s| s.to_string())
749
+
.collect(),
750
+
},
751
+
prefix: cell_prefix.to_string(),
752
+
span: Span::new(cell_span_start, span.end),
753
+
}];
754
+
} else {
755
+
// Simple variable completion
756
+
let var_prefix = trimmed_prefix[1..].to_string();
757
+
return vec![CompletionContext {
758
+
kind: CompletionKind::Variable,
759
+
prefix: var_prefix,
760
+
span,
761
+
}];
762
+
}
763
+
} else {
764
+
// Fallback to argument context if no $ found
765
+
return vec![CompletionContext {
766
+
kind: CompletionKind::Argument,
767
+
prefix: prefix.to_string(),
768
+
span,
769
+
}];
770
+
}
771
+
}
772
+
_ => {
773
+
// Check if this is a variable or cell path (starts with $)
774
+
let trimmed_prefix = prefix.trim();
775
+
if trimmed_prefix.starts_with('$') {
776
+
// Check if this is a cell path (contains a dot after $)
777
+
if let Some((var_name, path_so_far, cell_prefix)) =
778
+
parse_cell_path(trimmed_prefix)
779
+
{
780
+
let var_id = lookup_variable_id(var_name, working_set);
781
+
if let Some(var_id) = var_id {
782
+
let prefix_byte_len = cell_prefix.len();
783
+
let cell_span_start = span.end.saturating_sub(prefix_byte_len);
784
+
return vec![CompletionContext {
785
+
kind: CompletionKind::CellPath {
786
+
var_id,
787
+
path_so_far: path_so_far
788
+
.iter()
789
+
.map(|s| s.to_string())
790
+
.collect(),
791
+
},
792
+
prefix: cell_prefix.to_string(),
793
+
span: Span::new(cell_span_start, span.end),
794
+
}];
795
+
} else {
796
+
let var_prefix = trimmed_prefix[1..].to_string();
797
+
return vec![CompletionContext {
798
+
kind: CompletionKind::Variable,
799
+
prefix: var_prefix,
800
+
span,
801
+
}];
802
+
}
803
+
} else {
804
+
// Simple variable completion
805
+
let var_prefix = if trimmed_prefix.len() > 1 {
806
+
trimmed_prefix[1..].to_string()
807
+
} else {
808
+
String::new()
809
+
};
810
+
return vec![CompletionContext {
811
+
kind: CompletionKind::Variable,
812
+
prefix: var_prefix,
813
+
span,
814
+
}];
815
+
}
816
+
} else {
817
+
// Use helper to determine flag or argument context
818
+
return determine_flag_or_argument_context(
819
+
input,
820
+
shapes,
821
+
&trimmed_prefix,
822
+
idx,
823
+
local_span,
824
+
span,
825
+
global_offset,
826
+
working_set,
827
+
engine_guard,
828
+
);
829
+
}
830
+
}
831
+
}
832
+
}
833
+
break;
834
+
}
835
+
}
836
+
Vec::new()
837
+
}
838
+
839
+
pub fn determine_context_fallback(
840
+
input: &str,
841
+
shapes: &[(Span, FlatShape)],
842
+
working_set: &StateWorkingSet,
843
+
engine_guard: &EngineState,
844
+
byte_pos: usize,
845
+
global_offset: usize,
846
+
) -> Vec<CompletionContext> {
847
+
use nu_parser::{TokenContents, lex};
848
+
849
+
console_log!("[completion] Context is None, entering fallback logic");
850
+
// Check if there's a command-like shape before us
851
+
let mut has_separator_after_command = false;
852
+
for (span, shape) in shapes.iter().rev() {
853
+
let local_span = to_local_span(*span, global_offset);
854
+
if local_span.end <= byte_pos {
855
+
if is_command_shape(input, shape, local_span) {
856
+
// Check if there's a pipe or semicolon between this command and the cursor
857
+
has_separator_after_command =
858
+
has_separator_between(input, local_span.end, byte_pos);
859
+
console_log!(
860
+
"[completion] Found command shape {shape:?} at {local_span:?}, has_separator_after_command={has_separator_after_command}"
861
+
);
862
+
if !has_separator_after_command {
863
+
// Extract the command text (full command including subcommands)
864
+
let cmd = safe_slice(input, local_span);
865
+
let cmd_full = cmd.trim().to_string();
866
+
let cmd_first_word = extract_command_name(cmd).to_string();
867
+
868
+
// Check if we're right after the command (only whitespace between command and cursor)
869
+
let text_after_command = if local_span.end < input.len() {
870
+
&input[local_span.end..byte_pos]
871
+
} else {
872
+
""
873
+
};
874
+
let is_right_after_command = text_after_command.trim().is_empty();
875
+
876
+
// If we're right after a command, check if it has positional arguments
877
+
if is_right_after_command {
878
+
// Check if the command text contains spaces (indicating it's a subcommand like "attr category")
879
+
let is_subcommand = cmd_full.contains(' ') && cmd_full != cmd_first_word;
880
+
881
+
// First, try the full command name (e.g., "attr category")
882
+
// If that doesn't exist, fall back to the first word (e.g., "attr")
883
+
let full_cmd_exists =
884
+
get_command_signature(engine_guard, &cmd_full).is_some();
885
+
let cmd_name = if full_cmd_exists {
886
+
cmd_full.clone()
887
+
} else {
888
+
cmd_first_word.clone()
889
+
};
890
+
891
+
let mut context = Vec::with_capacity(2);
892
+
if let Some(signature) = get_command_signature(engine_guard, &cmd_name) {
893
+
// Check if command has any positional arguments
894
+
let has_positional_args = !signature.required_positional.is_empty()
895
+
|| !signature.optional_positional.is_empty();
896
+
897
+
if has_positional_args {
898
+
// Count existing arguments before cursor
899
+
let mut arg_count = 0;
900
+
for (prev_span, prev_shape) in shapes.iter().rev() {
901
+
let prev_local_span = to_local_span(*prev_span, global_offset);
902
+
if prev_local_span.end <= byte_pos
903
+
&& prev_local_span.end > local_span.end
904
+
{
905
+
if !is_command_shape(input, prev_shape, prev_local_span) {
906
+
let arg_text = safe_slice(input, prev_local_span);
907
+
let trimmed_arg = arg_text.trim();
908
+
// Don't count flags (starting with -) or empty arguments
909
+
if !trimmed_arg.is_empty()
910
+
&& !trimmed_arg.starts_with('-')
911
+
{
912
+
arg_count += 1;
913
+
}
914
+
}
915
+
}
916
+
}
917
+
918
+
console_log!(
919
+
"[completion] Right after command {cmd_name:?}, setting CommandArgument context with arg_index: {arg_count}"
920
+
);
921
+
922
+
// Use helper to create CommandArgument context(s) - may include subcommand context
923
+
let arg_contexts = create_command_argument_contexts(
924
+
cmd_name.clone(),
925
+
arg_count,
926
+
String::new(),
927
+
Span::new(byte_pos, byte_pos),
928
+
working_set,
929
+
engine_guard,
930
+
);
931
+
context.extend(arg_contexts);
932
+
}
933
+
}
934
+
// No positional arguments
935
+
// If this is a subcommand (contains spaces), don't show subcommands
936
+
// Only show subcommands if we're using just the base command (single word)
937
+
if is_subcommand && full_cmd_exists {
938
+
console_log!(
939
+
"[completion] Command {cmd_name:?} is a subcommand with no positional args, not showing completions"
940
+
);
941
+
} else {
942
+
// Show subcommands of the base command
943
+
console_log!(
944
+
"[completion] Command {cmd_name:?} has no positional args, showing subcommands"
945
+
);
946
+
context.push(CompletionContext {
947
+
kind: CompletionKind::Command {
948
+
parent_command: Some(cmd_first_word),
949
+
},
950
+
prefix: String::new(),
951
+
span: Span::new(byte_pos, byte_pos),
952
+
});
953
+
}
954
+
// reverse to put subcommands in the beginning
955
+
context.reverse();
956
+
return context;
957
+
} else {
958
+
// Not right after command, complete the command itself
959
+
console_log!("[completion] Set Command context with prefix: {cmd:?}");
960
+
return vec![CompletionContext {
961
+
kind: CompletionKind::Command {
962
+
parent_command: None,
963
+
},
964
+
prefix: cmd.to_string(),
965
+
span: local_span,
966
+
}];
967
+
}
968
+
}
969
+
}
970
+
break;
971
+
}
972
+
}
973
+
974
+
// No command found before, check context from tokens
975
+
console_log!("[completion] No command found before cursor, checking tokens");
976
+
// No command before, check context from tokens
977
+
let (tokens, _) = lex(input.as_bytes(), 0, &[], &[], true);
978
+
let last_token = tokens.iter().filter(|t| t.span.end <= byte_pos).last();
979
+
980
+
let is_cmd_context = if let Some(token) = last_token {
981
+
let matches = matches!(
982
+
token.contents,
983
+
TokenContents::Pipe
984
+
| TokenContents::PipePipe
985
+
| TokenContents::Semicolon
986
+
| TokenContents::Eol
987
+
);
988
+
console_log!(
989
+
"[completion] Last token: {contents:?}, is_cmd_context from token={matches}",
990
+
contents = token.contents
991
+
);
992
+
matches
993
+
} else {
994
+
console_log!(
995
+
"[completion] No last token found, assuming start of input (is_cmd_context=true)"
996
+
);
997
+
true // Start of input
998
+
};
999
+
1000
+
// Look for the last non-whitespace token before cursor
1001
+
let text_before = &input[..byte_pos];
1002
+
1003
+
// Also check if we're inside a block - if the last non-whitespace char before cursor is '{'
1004
+
let text_before_trimmed = text_before.trim_end();
1005
+
let is_inside_block = text_before_trimmed.ends_with('{');
1006
+
// If we found a separator after a command, we're starting a new command
1007
+
let is_cmd_context = is_cmd_context || is_inside_block || has_separator_after_command;
1008
+
console_log!(
1009
+
"[completion] is_inside_block={is_inside_block}, has_separator_after_command={has_separator_after_command}, final is_cmd_context={is_cmd_context}"
1010
+
);
1011
+
1012
+
// Find the last word before cursor
1013
+
let last_word_start = text_before
1014
+
.rfind(|c: char| c.is_whitespace() || is_separator_char(c))
1015
+
.map(|i| i + 1)
1016
+
.unwrap_or(0);
1017
+
1018
+
let last_word = text_before[last_word_start..].trim_start();
1019
+
console_log!("[completion] last_word_start={last_word_start}, last_word={last_word:?}");
1020
+
1021
+
if is_cmd_context {
1022
+
vec![CompletionContext {
1023
+
kind: CompletionKind::Command {
1024
+
parent_command: None,
1025
+
},
1026
+
prefix: last_word.to_string(),
1027
+
span: Span::new(last_word_start, byte_pos),
1028
+
}]
1029
+
} else {
1030
+
// Check if this is a variable or cell path (starts with $)
1031
+
let trimmed_word = last_word.trim();
1032
+
if trimmed_word.starts_with('$') {
1033
+
// Check if this is a cell path (contains a dot after $)
1034
+
if let Some((var_name, path_so_far, cell_prefix)) = parse_cell_path(trimmed_word) {
1035
+
let var_id = lookup_variable_id(&var_name, working_set);
1036
+
1037
+
if let Some(var_id) = var_id {
1038
+
let prefix_byte_len = cell_prefix.len();
1039
+
let cell_span_start = byte_pos.saturating_sub(prefix_byte_len);
1040
+
vec![CompletionContext {
1041
+
kind: CompletionKind::CellPath {
1042
+
var_id,
1043
+
path_so_far: path_so_far.iter().map(|s| s.to_string()).collect(),
1044
+
},
1045
+
prefix: cell_prefix.to_string(),
1046
+
span: Span::new(cell_span_start, byte_pos),
1047
+
}]
1048
+
} else {
1049
+
let var_prefix = trimmed_word[1..].to_string();
1050
+
vec![CompletionContext {
1051
+
kind: CompletionKind::Variable,
1052
+
prefix: var_prefix,
1053
+
span: Span::new(last_word_start, byte_pos),
1054
+
}]
1055
+
}
1056
+
} else {
1057
+
// Simple variable completion
1058
+
let var_prefix = trimmed_word[1..].to_string();
1059
+
vec![CompletionContext {
1060
+
kind: CompletionKind::Variable,
1061
+
prefix: var_prefix,
1062
+
span: Span::new(last_word_start, byte_pos),
1063
+
}]
1064
+
}
1065
+
} else if trimmed_word.starts_with('-') {
1066
+
// Try to find command by looking backwards through shapes
1067
+
let mut found_cmd = None;
1068
+
for (span, shape) in shapes.iter().rev() {
1069
+
let local_span = to_local_span(*span, global_offset);
1070
+
if local_span.end <= byte_pos && is_command_shape(input, shape, local_span) {
1071
+
let cmd_text = safe_slice(input, local_span);
1072
+
let cmd_name = extract_command_name(cmd_text).to_string();
1073
+
found_cmd = Some(cmd_name);
1074
+
break;
1075
+
}
1076
+
}
1077
+
if let Some(cmd_name) = found_cmd {
1078
+
vec![CompletionContext {
1079
+
kind: CompletionKind::Flag {
1080
+
command_name: cmd_name,
1081
+
},
1082
+
prefix: trimmed_word.to_string(),
1083
+
span: Span::new(last_word_start, byte_pos),
1084
+
}]
1085
+
} else {
1086
+
vec![CompletionContext {
1087
+
kind: CompletionKind::Argument,
1088
+
prefix: last_word.to_string(),
1089
+
span: Span::new(last_word_start, byte_pos),
1090
+
}]
1091
+
}
1092
+
} else {
1093
+
// Try to find command and argument index
1094
+
let mut found_cmd = None;
1095
+
let mut arg_count = 0;
1096
+
for (span, shape) in shapes.iter().rev() {
1097
+
let local_span = to_local_span(*span, global_offset);
1098
+
if local_span.end <= byte_pos {
1099
+
if is_command_shape(input, shape, local_span) {
1100
+
let cmd_text = safe_slice(input, local_span);
1101
+
let cmd_name = extract_command_name(cmd_text).to_string();
1102
+
found_cmd = Some(cmd_name);
1103
+
break;
1104
+
} else {
1105
+
let arg_text = safe_slice(input, local_span);
1106
+
let trimmed_arg = arg_text.trim();
1107
+
if !trimmed_arg.is_empty() && !trimmed_arg.starts_with('-') {
1108
+
arg_count += 1;
1109
+
}
1110
+
}
1111
+
}
1112
+
}
1113
+
if let Some(cmd_name) = found_cmd {
1114
+
create_command_argument_contexts(
1115
+
cmd_name,
1116
+
arg_count,
1117
+
trimmed_word.to_string(),
1118
+
Span::new(last_word_start, byte_pos),
1119
+
working_set,
1120
+
engine_guard,
1121
+
)
1122
+
} else {
1123
+
vec![CompletionContext {
1124
+
kind: CompletionKind::Argument,
1125
+
prefix: last_word.to_string(),
1126
+
span: Span::new(last_word_start, byte_pos),
1127
+
}]
1128
+
}
1129
+
}
1130
+
}
1131
+
}
1132
+
1133
+
pub fn determine_context(
1134
+
input: &str,
1135
+
shapes: &[(Span, FlatShape)],
1136
+
working_set: &StateWorkingSet,
1137
+
engine_guard: &EngineState,
1138
+
byte_pos: usize,
1139
+
global_offset: usize,
1140
+
) -> Vec<CompletionContext> {
1141
+
// First try to determine context from shapes
1142
+
let contexts = determine_context_from_shape(
1143
+
input,
1144
+
shapes,
1145
+
working_set,
1146
+
engine_guard,
1147
+
byte_pos,
1148
+
global_offset,
1149
+
);
1150
+
if !contexts.is_empty() {
1151
+
return contexts;
1152
+
}
1153
+
1154
+
// Fallback to token-based context determination
1155
+
determine_context_fallback(
1156
+
input,
1157
+
shapes,
1158
+
working_set,
1159
+
engine_guard,
1160
+
byte_pos,
1161
+
global_offset,
1162
+
)
1163
+
}
+169
src/completion/helpers.rs
+169
src/completion/helpers.rs
···
1
+
use nu_parser::FlatShape;
2
+
use nu_protocol::engine::StateWorkingSet;
3
+
use nu_protocol::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, Span};
4
+
5
+
/// Macro for console logging that automatically converts formatted strings to JsValue
6
+
#[macro_export]
7
+
macro_rules! console_log {
8
+
($($arg:tt)*) => {
9
+
#[cfg(debug_assertions)]
10
+
web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!($($arg)*)));
11
+
};
12
+
}
13
+
14
+
pub fn is_separator_char(c: char) -> bool {
15
+
['|', ';', '(', '{'].contains(&c)
16
+
}
17
+
18
+
pub fn is_command_separator_char(c: char) -> bool {
19
+
['|', ';'].contains(&c)
20
+
}
21
+
22
+
pub fn has_separator_between(input: &str, start: usize, end: usize) -> bool {
23
+
if start < end && start < input.len() {
24
+
let text_between = &input[start..std::cmp::min(end, input.len())];
25
+
text_between.chars().any(|c| is_separator_char(c))
26
+
} else {
27
+
false
28
+
}
29
+
}
30
+
31
+
pub fn find_last_separator_pos(text: &str) -> Option<usize> {
32
+
text.rfind(|c| is_command_separator_char(c)).map(|i| i + 1)
33
+
}
34
+
35
+
pub fn ends_with_separator(text: &str) -> bool {
36
+
let text = text.trim_end();
37
+
text.ends_with('|') || text.ends_with(';')
38
+
}
39
+
40
+
pub fn to_local_span(span: Span, global_offset: usize) -> Span {
41
+
Span::new(
42
+
span.start.saturating_sub(global_offset),
43
+
span.end.saturating_sub(global_offset),
44
+
)
45
+
}
46
+
47
+
pub fn safe_slice(input: &str, span: Span) -> &str {
48
+
if span.start < input.len() {
49
+
let safe_end = std::cmp::min(span.end, input.len());
50
+
&input[span.start..safe_end]
51
+
} else {
52
+
""
53
+
}
54
+
}
55
+
56
+
pub fn is_command_shape(input: &str, shape: &FlatShape, local_span: Span) -> bool {
57
+
matches!(
58
+
shape,
59
+
FlatShape::External(_) | FlatShape::InternalCall(_) | FlatShape::Keyword
60
+
) || matches!(shape, FlatShape::Garbage) && {
61
+
if local_span.start < input.len() {
62
+
let prev_text = safe_slice(input, local_span);
63
+
!prev_text.trim().starts_with('-')
64
+
} else {
65
+
false
66
+
}
67
+
}
68
+
}
69
+
70
+
pub fn handle_block_prefix(prefix: &str, span: Span) -> Option<(&str, Span, bool)> {
71
+
let mut block_prefix = prefix;
72
+
let mut block_span_start = span.start;
73
+
74
+
// Remove leading '{' and whitespace
75
+
if block_prefix.starts_with('{') {
76
+
block_prefix = &block_prefix[1..];
77
+
block_span_start += 1;
78
+
}
79
+
let trimmed_block_prefix = block_prefix.trim_start();
80
+
if trimmed_block_prefix != block_prefix {
81
+
// Adjust span start to skip whitespace
82
+
block_span_start += block_prefix.len() - trimmed_block_prefix.len();
83
+
}
84
+
85
+
let is_empty = trimmed_block_prefix.is_empty();
86
+
Some((
87
+
trimmed_block_prefix,
88
+
Span::new(block_span_start, span.end),
89
+
is_empty,
90
+
))
91
+
}
92
+
93
+
pub fn extract_command_name(cmd_text: &str) -> &str {
94
+
cmd_text
95
+
.split_whitespace()
96
+
.next()
97
+
.unwrap_or(cmd_text)
98
+
.trim()
99
+
}
100
+
101
+
pub fn lookup_variable_id(
102
+
var_name: &str,
103
+
working_set: &StateWorkingSet,
104
+
) -> Option<nu_protocol::VarId> {
105
+
match var_name {
106
+
"env" => Some(ENV_VARIABLE_ID),
107
+
"nu" => Some(NU_VARIABLE_ID),
108
+
"in" => Some(IN_VARIABLE_ID),
109
+
_ => working_set.find_variable(var_name.as_bytes()),
110
+
}
111
+
}
112
+
113
+
pub fn parse_cell_path(text: &str) -> Option<(&str, Vec<&str>, &str)> {
114
+
let trimmed = text.trim();
115
+
if !trimmed.starts_with('$') {
116
+
return None;
117
+
}
118
+
119
+
// Check if this is a cell path (contains a dot after $)
120
+
if let Some(dot_pos) = trimmed[1..].find('.') {
121
+
let var_name = &trimmed[1..dot_pos + 1];
122
+
let after_var = &trimmed[dot_pos + 2..];
123
+
let parts: Vec<&str> = after_var.split('.').collect();
124
+
let (path_so_far, cell_prefix) = if parts.is_empty() {
125
+
(vec![], "")
126
+
} else if after_var.ends_with('.') {
127
+
(
128
+
parts.iter().filter(|s| !s.is_empty()).copied().collect(),
129
+
"",
130
+
)
131
+
} else {
132
+
let path: Vec<&str> = parts[..parts.len().saturating_sub(1)]
133
+
.iter()
134
+
.copied()
135
+
.collect();
136
+
let prefix = parts.last().copied().unwrap_or("");
137
+
(path, prefix)
138
+
};
139
+
Some((var_name, path_so_far, cell_prefix))
140
+
} else {
141
+
None
142
+
}
143
+
}
144
+
145
+
pub fn parse_cell_path_from_fields(text: &str) -> (Vec<&str>, &str) {
146
+
let trimmed = text.trim();
147
+
let parts: Vec<&str> = trimmed.split('.').collect();
148
+
if parts.is_empty() {
149
+
(vec![], "")
150
+
} else if trimmed.ends_with('.') {
151
+
(
152
+
parts.iter().filter(|s| !s.is_empty()).copied().collect(),
153
+
"",
154
+
)
155
+
} else {
156
+
let path: Vec<&str> = parts[..parts.len().saturating_sub(1)]
157
+
.iter()
158
+
.copied()
159
+
.collect();
160
+
let prefix = parts.last().copied().unwrap_or("");
161
+
(path, prefix)
162
+
}
163
+
}
164
+
165
+
pub fn to_char_span(input: &str, span: Span) -> Span {
166
+
let char_start = input[..span.start].chars().count();
167
+
let char_end = input[..span.end].chars().count();
168
+
Span::new(char_start, char_end)
169
+
}
+97
src/completion/mod.rs
+97
src/completion/mod.rs
···
1
+
use crate::console_log;
2
+
use futures::FutureExt;
3
+
use js_sys::Promise;
4
+
use nu_parser::{flatten_block, parse};
5
+
use nu_protocol::engine::StateWorkingSet;
6
+
use wasm_bindgen::prelude::*;
7
+
use wasm_bindgen_futures::future_to_promise;
8
+
9
+
use super::*;
10
+
11
+
pub mod context;
12
+
pub mod helpers;
13
+
pub mod suggestions;
14
+
pub mod types;
15
+
pub mod variables;
16
+
17
+
pub use context::determine_context;
18
+
pub use suggestions::generate_suggestions;
19
+
pub use types::{CompletionContext, Suggestion};
20
+
21
+
#[wasm_bindgen]
22
+
pub fn completion(input: String, js_cursor_pos: usize) -> Promise {
23
+
future_to_promise(completion_impl(input, js_cursor_pos).map(|s| Ok(JsValue::from_str(&s))))
24
+
}
25
+
26
+
pub async fn completion_impl(input: String, js_cursor_pos: usize) -> String {
27
+
let engine_guard = read_engine_state().await;
28
+
let stack_guard = crate::read_stack().await;
29
+
let root = get_pwd();
30
+
31
+
// Map UTF-16 cursor position (from JS) to Byte index (for Rust)
32
+
let byte_pos = input
33
+
.char_indices()
34
+
.map(|(i, _)| i)
35
+
.nth(js_cursor_pos)
36
+
.unwrap_or(input.len());
37
+
38
+
let (working_set, shapes, global_offset) = {
39
+
let mut working_set = StateWorkingSet::new(&engine_guard);
40
+
let global_offset = working_set.next_span_start();
41
+
let block = parse(&mut working_set, None, input.as_bytes(), false);
42
+
let shapes = flatten_block(&working_set, &block);
43
+
(working_set, shapes, global_offset)
44
+
};
45
+
46
+
// Initial state logging
47
+
console_log!(
48
+
"[completion] Input: {input:?}, JS cursor: {js_cursor_pos}, byte cursor: {byte_pos}"
49
+
);
50
+
console_log!(
51
+
"[completion] Found {count} shapes, global_offset: {global_offset}",
52
+
count = shapes.len()
53
+
);
54
+
for (idx, (span, shape)) in shapes.iter().enumerate() {
55
+
let (local_start, local_end) = (
56
+
span.start.saturating_sub(global_offset),
57
+
span.end.saturating_sub(global_offset),
58
+
);
59
+
console_log!(
60
+
"[completion] Shape {idx}: {shape:?} at [{start}, {end}] (local: [{local_start}, {local_end}])",
61
+
start = span.start,
62
+
end = span.end
63
+
);
64
+
}
65
+
66
+
// Determine completion context
67
+
let context = determine_context(
68
+
&input,
69
+
&shapes,
70
+
&working_set,
71
+
&engine_guard,
72
+
byte_pos,
73
+
global_offset,
74
+
);
75
+
76
+
// Convert Vec to HashSet
77
+
use std::collections::HashSet;
78
+
let context_set: HashSet<CompletionContext> = context.into_iter().collect();
79
+
80
+
// Generate suggestions based on context
81
+
let suggestions = generate_suggestions(
82
+
&input,
83
+
context_set,
84
+
&working_set,
85
+
&engine_guard,
86
+
&stack_guard,
87
+
&root,
88
+
byte_pos,
89
+
);
90
+
91
+
drop(working_set);
92
+
drop(engine_guard);
93
+
94
+
let suggestions = serde_json::to_string(&suggestions).unwrap_or_else(|_| "[]".to_string());
95
+
console_log!("{suggestions}");
96
+
suggestions
97
+
}
+602
src/completion/suggestions.rs
+602
src/completion/suggestions.rs
···
1
+
use crate::completion::context::get_command_signature;
2
+
use crate::completion::helpers::to_char_span;
3
+
use crate::completion::types::{CompletionContext, CompletionKind, Suggestion};
4
+
use crate::completion::variables::*;
5
+
use crate::console_log;
6
+
use nu_protocol::Span;
7
+
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
8
+
use std::collections::HashSet;
9
+
10
+
pub fn generate_command_suggestions(
11
+
input: &str,
12
+
working_set: &StateWorkingSet,
13
+
prefix: String,
14
+
span: Span,
15
+
parent_command: Option<String>,
16
+
) -> Vec<Suggestion> {
17
+
console_log!(
18
+
"[completion] Generating Command suggestions with prefix: {prefix:?}, parent_command: {parent_command:?}"
19
+
);
20
+
21
+
let span = to_char_span(input, span);
22
+
let mut suggestions = Vec::new();
23
+
let mut cmd_count = 0;
24
+
25
+
// Determine search prefix and name extraction logic
26
+
let (search_prefix, parent_prefix_opt) = if let Some(parent) = &parent_command {
27
+
// Show only subcommands of the parent command
28
+
// Subcommands are commands that start with "parent_command " (with space)
29
+
let parent_prefix = format!("{} ", parent);
30
+
let search_prefix = if prefix.is_empty() {
31
+
parent_prefix.clone()
32
+
} else {
33
+
format!("{}{}", parent_prefix, prefix)
34
+
};
35
+
(search_prefix, Some(parent_prefix))
36
+
} else {
37
+
// Regular command completion - show all commands
38
+
(prefix.clone(), None)
39
+
};
40
+
41
+
let cmds = working_set
42
+
.find_commands_by_predicate(|value| value.starts_with(search_prefix.as_bytes()), true);
43
+
44
+
for (_, name, desc, _) in cmds {
45
+
let name_str = String::from_utf8_lossy(&name).to_string();
46
+
47
+
// Extract the command name to display
48
+
// For subcommands, extract just the subcommand name (part after "parent_command ")
49
+
// For regular commands, use the full command name
50
+
let display_name = if let Some(parent_prefix) = &parent_prefix_opt {
51
+
if let Some(subcommand_name) = name_str.strip_prefix(parent_prefix) {
52
+
subcommand_name.to_string()
53
+
} else {
54
+
continue; // Skip if it doesn't match the parent prefix
55
+
}
56
+
} else {
57
+
name_str
58
+
};
59
+
60
+
suggestions.push(Suggestion {
61
+
rendered: {
62
+
let name_colored = ansi_term::Color::Green.bold().paint(&display_name);
63
+
let desc_str = desc.as_deref().unwrap_or("<no description>");
64
+
format!("{name_colored} {desc_str}")
65
+
},
66
+
name: display_name,
67
+
description: desc.map(|d| d.to_string()),
68
+
span_start: span.start,
69
+
span_end: span.end,
70
+
});
71
+
cmd_count += 1;
72
+
}
73
+
console_log!("[completion] Found {cmd_count} command suggestions");
74
+
suggestions.sort();
75
+
suggestions
76
+
}
77
+
78
+
pub fn generate_argument_suggestions(
79
+
input: &str,
80
+
prefix: String,
81
+
span: Span,
82
+
root: &std::sync::Arc<vfs::VfsPath>,
83
+
) -> Vec<Suggestion> {
84
+
console_log!("[completion] Generating Argument suggestions with prefix: {prefix:?}");
85
+
// File completion
86
+
let mut file_suggestions = generate_file_suggestions(&prefix, span, root, None, input);
87
+
console_log!(
88
+
"[completion] Found {file_count} file suggestions",
89
+
file_count = file_suggestions.len()
90
+
);
91
+
file_suggestions.sort();
92
+
file_suggestions
93
+
}
94
+
95
+
pub fn generate_flag_suggestions(
96
+
input: &str,
97
+
engine_guard: &EngineState,
98
+
prefix: String,
99
+
span: Span,
100
+
command_name: String,
101
+
) -> Vec<Suggestion> {
102
+
console_log!(
103
+
"[completion] Generating Flag suggestions for command: {command_name:?}, prefix: {prefix:?}"
104
+
);
105
+
106
+
let mut suggestions = Vec::new();
107
+
if let Some(signature) = get_command_signature(engine_guard, &command_name) {
108
+
let span = to_char_span(input, span);
109
+
let mut flag_count = 0;
110
+
111
+
// Get switches from signature
112
+
// Signature has a named field that contains named arguments (including switches)
113
+
for flag in &signature.named {
114
+
// Check if this is a switch (has no argument)
115
+
// Switches have arg: None, named arguments have arg: Some(SyntaxShape)
116
+
let is_switch = flag.arg.is_none();
117
+
118
+
if is_switch {
119
+
let long_name = format!("--{}", flag.long);
120
+
let short_name = flag.short.map(|c| format!("-{}", c));
121
+
122
+
// Determine which flags to show based on prefix:
123
+
// - If prefix is empty or exactly "-", show all flags (both short and long)
124
+
// - If prefix starts with "--", only show long flags that match the prefix
125
+
// - If prefix starts with "-" (but not "--"), only show short flags that match the prefix
126
+
let show_all = prefix.is_empty() || prefix == "-";
127
+
128
+
// Helper to create a flag suggestion
129
+
let create_flag_suggestion = |flag_name: String| -> Suggestion {
130
+
Suggestion {
131
+
name: flag_name.clone(),
132
+
description: Some(flag.desc.clone()),
133
+
rendered: {
134
+
let flag_colored = ansi_term::Color::Cyan.bold().paint(&flag_name);
135
+
format!("{flag_colored} {}", flag.desc)
136
+
},
137
+
span_start: span.start,
138
+
span_end: span.end,
139
+
}
140
+
};
141
+
142
+
// Add long flag if it matches
143
+
let should_show_long = if show_all {
144
+
true // Show all flags when prefix is "-" or empty
145
+
} else if prefix.starts_with("--") {
146
+
long_name.starts_with(&prefix) // Only show long flags matching prefix
147
+
} else {
148
+
false // Don't show long flags if prefix is short flag format
149
+
};
150
+
151
+
if should_show_long {
152
+
suggestions.push(create_flag_suggestion(long_name));
153
+
flag_count += 1;
154
+
}
155
+
156
+
// Add short flag if it matches
157
+
if let Some(short) = &short_name {
158
+
let flag_char = flag.short.unwrap_or(' ');
159
+
let should_show_short = if show_all {
160
+
true // Show all flags when prefix is "-" or empty
161
+
} else if prefix.starts_with("-") && !prefix.starts_with("--") {
162
+
// For combined short flags like "-a" or "-af", suggest flags that can be appended
163
+
// Extract already used flags from prefix (e.g., "-a" -> ['a'], "-af" -> ['a', 'f'])
164
+
let used_flags: Vec<char> = prefix[1..].chars().collect();
165
+
166
+
// Show if this flag isn't already in the prefix
167
+
!used_flags.contains(&flag_char)
168
+
} else {
169
+
false // Don't show short flags if prefix is long flag format
170
+
};
171
+
172
+
if should_show_short {
173
+
// If prefix already contains flags (like "-a"), create combined suggestion (like "-af")
174
+
let suggestion_name = if prefix.len() > 1 && prefix.starts_with("-") {
175
+
format!("{}{}", prefix, flag_char)
176
+
} else {
177
+
short.clone()
178
+
};
179
+
suggestions.push(create_flag_suggestion(suggestion_name));
180
+
flag_count += 1;
181
+
}
182
+
}
183
+
}
184
+
}
185
+
186
+
console_log!("[completion] Found {flag_count} flag suggestions");
187
+
} else {
188
+
console_log!("[completion] Could not find signature for command: {command_name:?}");
189
+
}
190
+
suggestions.sort();
191
+
suggestions
192
+
}
193
+
194
+
pub fn generate_command_argument_suggestions(
195
+
input: &str,
196
+
engine_guard: &EngineState,
197
+
_working_set: &StateWorkingSet,
198
+
prefix: String,
199
+
span: Span,
200
+
command_name: String,
201
+
arg_index: usize,
202
+
root: &std::sync::Arc<vfs::VfsPath>,
203
+
) -> Vec<Suggestion> {
204
+
console_log!(
205
+
"[completion] Generating CommandArgument suggestions for command: {command_name:?}, arg_index: {arg_index}, prefix: {prefix:?}"
206
+
);
207
+
208
+
let mut suggestions = Vec::new();
209
+
210
+
if let Some(signature) = get_command_signature(engine_guard, &command_name) {
211
+
// First, check if we're completing an argument for a flag
212
+
// Look backwards from the current position to find the previous flag
213
+
let text_before = if span.start < input.len() {
214
+
&input[..span.start]
215
+
} else {
216
+
""
217
+
};
218
+
let text_before_trimmed = text_before.trim_end();
219
+
220
+
// Check if the last word before cursor is a flag
221
+
let last_word_start = text_before_trimmed
222
+
.rfind(|c: char| c.is_whitespace())
223
+
.map(|i| i + 1)
224
+
.unwrap_or(0);
225
+
let last_word = &text_before_trimmed[last_word_start..];
226
+
227
+
if last_word.starts_with('-') {
228
+
// We're after a flag - check if this flag accepts an argument
229
+
let flag_name = last_word.trim();
230
+
let is_long_flag = flag_name.starts_with("--");
231
+
let flag_to_match: Option<(bool, String)> = if is_long_flag {
232
+
// Long flag: --flag-name
233
+
flag_name.strip_prefix("--").map(|s| (true, s.to_string()))
234
+
} else {
235
+
// Short flag: -f (single character)
236
+
flag_name
237
+
.strip_prefix("-")
238
+
.and_then(|s| s.chars().next().map(|c| (false, c.to_string())))
239
+
};
240
+
241
+
if let Some((is_long, flag_name_to_match)) = flag_to_match {
242
+
// Find the flag in the signature
243
+
for flag in &signature.named {
244
+
let matches_flag = if is_long {
245
+
// Long flag
246
+
flag.long == flag_name_to_match
247
+
} else {
248
+
// Short flag - compare character
249
+
flag.short
250
+
.map(|c| c.to_string() == flag_name_to_match)
251
+
.unwrap_or(false)
252
+
};
253
+
254
+
if matches_flag {
255
+
// Found the flag - check if it accepts an argument
256
+
if let Some(flag_arg_shape) = &flag.arg {
257
+
// Flag accepts an argument - use its type
258
+
console_log!(
259
+
"[completion] Flag {flag_name:?} accepts argument of type {:?}",
260
+
flag_arg_shape
261
+
);
262
+
let mut add_file_suggestions = || {
263
+
let file_suggestions = generate_file_suggestions(
264
+
&prefix,
265
+
span,
266
+
root,
267
+
Some(flag.desc.clone()),
268
+
input,
269
+
);
270
+
let file_count = file_suggestions.len();
271
+
suggestions.extend(file_suggestions);
272
+
console_log!(
273
+
"[completion] Found {file_count} file suggestions for flag argument"
274
+
);
275
+
};
276
+
match flag_arg_shape {
277
+
nu_protocol::SyntaxShape::Filepath
278
+
| nu_protocol::SyntaxShape::Any => {
279
+
add_file_suggestions();
280
+
}
281
+
nu_protocol::SyntaxShape::OneOf(l)
282
+
if l.contains(&nu_protocol::SyntaxShape::Filepath) =>
283
+
{
284
+
add_file_suggestions();
285
+
}
286
+
_ => {
287
+
// Flag argument is not a filepath type
288
+
console_log!(
289
+
"[completion] Flag {flag_name:?} argument is type {:?}, not suggesting files",
290
+
flag_arg_shape
291
+
);
292
+
}
293
+
}
294
+
return suggestions;
295
+
} else {
296
+
// Flag doesn't accept an argument - fall through to positional argument check
297
+
console_log!(
298
+
"[completion] Flag {flag_name:?} doesn't accept an argument, checking positional arguments"
299
+
);
300
+
break;
301
+
}
302
+
}
303
+
}
304
+
}
305
+
}
306
+
307
+
// Not after a flag, or flag doesn't accept an argument - check positional arguments
308
+
// Get positional arguments from signature
309
+
// Check if argument is in required or optional positional
310
+
let required_count = signature.required_positional.len();
311
+
312
+
// Find the argument at the given index
313
+
let arg = if arg_index < signature.required_positional.len() {
314
+
signature.required_positional.get(arg_index)
315
+
} else {
316
+
let optional_index = arg_index - required_count;
317
+
signature.optional_positional.get(optional_index)
318
+
};
319
+
320
+
if let Some(arg) = arg {
321
+
let mut add_file_suggestions = || {
322
+
let file_suggestions =
323
+
generate_file_suggestions(&prefix, span, root, Some(arg.desc.clone()), input);
324
+
let file_count = file_suggestions.len();
325
+
suggestions.extend(file_suggestions);
326
+
console_log!(
327
+
"[completion] Found {file_count} file suggestions for argument {arg_index}"
328
+
);
329
+
};
330
+
331
+
match &arg.shape {
332
+
nu_protocol::SyntaxShape::Filepath | nu_protocol::SyntaxShape::Any => {
333
+
add_file_suggestions();
334
+
}
335
+
nu_protocol::SyntaxShape::OneOf(l)
336
+
if l.contains(&nu_protocol::SyntaxShape::Filepath) =>
337
+
{
338
+
add_file_suggestions();
339
+
}
340
+
_ => {
341
+
// For other types, don't suggest files
342
+
console_log!(
343
+
"[completion] Argument {arg_index} is type {:?}, not suggesting files",
344
+
arg.shape
345
+
);
346
+
}
347
+
}
348
+
} else {
349
+
// Argument index out of range - command doesn't accept that many positional arguments
350
+
// Don't suggest files since we know the type (it's not a valid argument)
351
+
console_log!(
352
+
"[completion] Argument index {arg_index} out of range, not suggesting files"
353
+
);
354
+
}
355
+
} else {
356
+
// No signature found, fall back to file completion
357
+
console_log!(
358
+
"[completion] Could not find signature for command: {command_name:?}, using file completion"
359
+
);
360
+
let file_suggestions = generate_file_suggestions(&prefix, span, root, None, input);
361
+
suggestions.extend(file_suggestions);
362
+
}
363
+
suggestions.sort();
364
+
suggestions
365
+
}
366
+
367
+
pub fn generate_variable_suggestions(
368
+
input: &str,
369
+
working_set: &StateWorkingSet,
370
+
prefix: String,
371
+
span: Span,
372
+
byte_pos: usize,
373
+
) -> Vec<Suggestion> {
374
+
console_log!("[completion] Generating Variable suggestions with prefix: {prefix:?}");
375
+
376
+
// Collect all available variables
377
+
let variables = collect_variables(working_set, input, byte_pos);
378
+
let span = to_char_span(input, span);
379
+
let mut suggestions = Vec::new();
380
+
let mut var_count = 0;
381
+
382
+
for (var_name, var_id) in variables {
383
+
// Filter by prefix (variable name includes $, so we need to check after $)
384
+
if var_name.len() > 1 && var_name[1..].starts_with(&prefix) {
385
+
// Get variable type
386
+
let var_type = working_set.get_variable(var_id).ty.to_string();
387
+
388
+
suggestions.push(Suggestion {
389
+
name: var_name.clone(),
390
+
description: Some(var_type.clone()),
391
+
rendered: {
392
+
let var_colored = ansi_term::Color::Blue.bold().paint(&var_name);
393
+
format!("{var_colored} {var_type}")
394
+
},
395
+
span_start: span.start,
396
+
span_end: span.end,
397
+
});
398
+
var_count += 1;
399
+
}
400
+
}
401
+
402
+
console_log!("[completion] Found {var_count} variable suggestions");
403
+
suggestions.sort();
404
+
suggestions
405
+
}
406
+
407
+
pub fn generate_cell_path_suggestions(
408
+
input: &str,
409
+
working_set: &StateWorkingSet,
410
+
engine_guard: &EngineState,
411
+
stack_guard: &Stack,
412
+
prefix: String,
413
+
span: Span,
414
+
var_id: nu_protocol::VarId,
415
+
path_so_far: Vec<String>,
416
+
) -> Vec<Suggestion> {
417
+
console_log!(
418
+
"[completion] Generating CellPath suggestions with prefix: {prefix:?}, path: {path_so_far:?}"
419
+
);
420
+
421
+
let mut suggestions = Vec::new();
422
+
// Evaluate the variable to get its value
423
+
if let Some(var_value) =
424
+
eval_variable_for_completion(var_id, working_set, engine_guard, stack_guard)
425
+
{
426
+
// Follow the path to get the value at the current level
427
+
let current_value = if path_so_far.is_empty() {
428
+
var_value
429
+
} else {
430
+
let path_refs: Vec<&str> = path_so_far.iter().map(|s| s.as_str()).collect();
431
+
follow_cell_path(&var_value, &path_refs).unwrap_or(var_value)
432
+
};
433
+
434
+
// Get columns/fields from the current value
435
+
let columns = get_columns_from_value(¤t_value);
436
+
let span = to_char_span(input, span);
437
+
let mut field_count = 0;
438
+
439
+
for (col_name, col_type) in columns {
440
+
// Filter by prefix
441
+
if col_name.starts_with(&prefix) {
442
+
let type_str = col_type.as_deref().unwrap_or("any");
443
+
suggestions.push(Suggestion {
444
+
name: col_name.clone(),
445
+
description: Some(type_str.to_string()),
446
+
rendered: {
447
+
let col_colored = ansi_term::Color::Yellow.paint(&col_name);
448
+
format!("{col_colored} {type_str}")
449
+
},
450
+
span_start: span.start,
451
+
span_end: span.end,
452
+
});
453
+
field_count += 1;
454
+
}
455
+
}
456
+
457
+
console_log!("[completion] Found {field_count} cell path suggestions");
458
+
} else {
459
+
// Variable couldn't be evaluated - this is expected for runtime variables
460
+
// We can't provide cell path completions without knowing the structure
461
+
console_log!(
462
+
"[completion] Could not evaluate variable {var_id:?} for cell path completion (runtime variable)"
463
+
);
464
+
465
+
// Try to get type information to provide better feedback
466
+
if let Ok(var_info) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
467
+
working_set.get_variable(var_id)
468
+
})) {
469
+
console_log!("[completion] Variable type: {ty:?}", ty = var_info.ty);
470
+
}
471
+
}
472
+
suggestions.sort();
473
+
suggestions
474
+
}
475
+
476
+
pub fn generate_file_suggestions(
477
+
prefix: &str,
478
+
span: Span,
479
+
root: &std::sync::Arc<vfs::VfsPath>,
480
+
description: Option<String>,
481
+
input: &str,
482
+
) -> Vec<Suggestion> {
483
+
let (dir, file_prefix) = prefix
484
+
.rfind('/')
485
+
.map(|idx| (&prefix[..idx + 1], &prefix[idx + 1..]))
486
+
.unwrap_or(("", prefix));
487
+
488
+
let dir_to_join = (dir.len() > 1 && dir.ends_with('/'))
489
+
.then(|| &dir[..dir.len() - 1])
490
+
.unwrap_or(dir);
491
+
492
+
let target_dir = if !dir.is_empty() {
493
+
match root.join(dir_to_join) {
494
+
Ok(d) if d.is_dir().unwrap_or(false) => Some(d),
495
+
_ => None,
496
+
}
497
+
} else {
498
+
Some(root.join("").unwrap())
499
+
};
500
+
501
+
let mut file_suggestions = Vec::new();
502
+
if let Some(d) = target_dir {
503
+
if let Ok(iterator) = d.read_dir() {
504
+
let char_span = to_char_span(input, span);
505
+
for entry in iterator {
506
+
let name = entry.filename();
507
+
if name.starts_with(file_prefix) {
508
+
let full_completion = format!("{}{}", dir, name);
509
+
file_suggestions.push(Suggestion {
510
+
name: full_completion.clone(),
511
+
description: description.clone(),
512
+
rendered: full_completion,
513
+
span_start: char_span.start,
514
+
span_end: char_span.end,
515
+
});
516
+
}
517
+
}
518
+
}
519
+
}
520
+
file_suggestions
521
+
}
522
+
523
+
pub fn generate_suggestions(
524
+
input: &str,
525
+
contexts: HashSet<CompletionContext>,
526
+
working_set: &StateWorkingSet,
527
+
engine_guard: &EngineState,
528
+
stack_guard: &Stack,
529
+
root: &std::sync::Arc<vfs::VfsPath>,
530
+
byte_pos: usize,
531
+
) -> Vec<Suggestion> {
532
+
console_log!("contexts: {contexts:?}");
533
+
534
+
let mut context_vec: Vec<_> = contexts.into_iter().collect();
535
+
context_vec.sort_by_key(|ctx| match &ctx.kind {
536
+
CompletionKind::Command { .. } => 0,
537
+
CompletionKind::Flag { .. } => 1,
538
+
CompletionKind::Variable => 2,
539
+
CompletionKind::CellPath { .. } => 3,
540
+
CompletionKind::CommandArgument { .. } => 4,
541
+
CompletionKind::Argument => 5,
542
+
});
543
+
544
+
let mut suggestions = Vec::new();
545
+
for context in context_vec.iter() {
546
+
let mut sug = match &context.kind {
547
+
CompletionKind::Command { parent_command } => generate_command_suggestions(
548
+
input,
549
+
working_set,
550
+
context.prefix.clone(),
551
+
context.span,
552
+
parent_command.clone(),
553
+
),
554
+
CompletionKind::Argument => {
555
+
generate_argument_suggestions(input, context.prefix.clone(), context.span, root)
556
+
}
557
+
CompletionKind::Flag { command_name } => generate_flag_suggestions(
558
+
input,
559
+
engine_guard,
560
+
context.prefix.clone(),
561
+
context.span,
562
+
command_name.clone(),
563
+
),
564
+
CompletionKind::CommandArgument {
565
+
command_name,
566
+
arg_index,
567
+
} => generate_command_argument_suggestions(
568
+
input,
569
+
engine_guard,
570
+
working_set,
571
+
context.prefix.clone(),
572
+
context.span,
573
+
command_name.clone(),
574
+
*arg_index,
575
+
root,
576
+
),
577
+
CompletionKind::Variable => generate_variable_suggestions(
578
+
input,
579
+
working_set,
580
+
context.prefix.clone(),
581
+
context.span,
582
+
byte_pos,
583
+
),
584
+
CompletionKind::CellPath {
585
+
var_id,
586
+
path_so_far,
587
+
} => generate_cell_path_suggestions(
588
+
input,
589
+
working_set,
590
+
engine_guard,
591
+
stack_guard,
592
+
context.prefix.clone(),
593
+
context.span,
594
+
*var_id,
595
+
path_so_far.clone(),
596
+
),
597
+
};
598
+
suggestions.append(&mut sug);
599
+
}
600
+
601
+
suggestions
602
+
}
+88
src/completion/types.rs
+88
src/completion/types.rs
···
1
+
use nu_protocol::Span;
2
+
use serde::Serialize;
3
+
4
+
#[derive(Debug, Serialize)]
5
+
pub struct Suggestion {
6
+
pub name: String,
7
+
pub description: Option<String>,
8
+
pub rendered: String,
9
+
pub span_start: usize, // char index (not byte)
10
+
pub span_end: usize, // char index (not byte)
11
+
}
12
+
13
+
impl PartialEq for Suggestion {
14
+
fn eq(&self, other: &Self) -> bool {
15
+
self.name == other.name
16
+
}
17
+
}
18
+
impl Eq for Suggestion {}
19
+
impl PartialOrd for Suggestion {
20
+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
21
+
self.name.partial_cmp(&other.name)
22
+
}
23
+
}
24
+
impl Ord for Suggestion {
25
+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
26
+
self.name.cmp(&other.name)
27
+
}
28
+
}
29
+
30
+
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
31
+
pub enum CompletionKind {
32
+
Command {
33
+
parent_command: Option<String>, // If Some, only show subcommands of this command
34
+
},
35
+
Argument,
36
+
Flag {
37
+
command_name: String,
38
+
},
39
+
CommandArgument {
40
+
command_name: String,
41
+
arg_index: usize,
42
+
},
43
+
Variable, // prefix is without the $ prefix
44
+
CellPath {
45
+
var_id: nu_protocol::VarId, // variable ID for evaluation
46
+
path_so_far: Vec<String>, // path members accessed before current one
47
+
},
48
+
}
49
+
50
+
#[derive(Debug)]
51
+
pub struct CompletionContext {
52
+
pub kind: CompletionKind,
53
+
pub prefix: String, // the partial text being completed
54
+
pub span: Span,
55
+
}
56
+
57
+
impl PartialEq for CompletionContext {
58
+
fn eq(&self, other: &Self) -> bool {
59
+
self.kind == other.kind && self.prefix == other.prefix
60
+
}
61
+
}
62
+
63
+
impl Eq for CompletionContext {}
64
+
65
+
impl PartialOrd for CompletionContext {
66
+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
67
+
match self.kind.partial_cmp(&other.kind) {
68
+
Some(std::cmp::Ordering::Equal) => self.prefix.partial_cmp(&other.prefix),
69
+
other => other,
70
+
}
71
+
}
72
+
}
73
+
74
+
impl Ord for CompletionContext {
75
+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
76
+
match self.kind.cmp(&other.kind) {
77
+
std::cmp::Ordering::Equal => self.prefix.cmp(&other.prefix),
78
+
other => other,
79
+
}
80
+
}
81
+
}
82
+
83
+
impl std::hash::Hash for CompletionContext {
84
+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
85
+
self.kind.hash(state);
86
+
self.prefix.hash(state);
87
+
}
88
+
}
+218
src/completion/variables.rs
+218
src/completion/variables.rs
···
1
+
use crate::console_log;
2
+
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};
3
+
use nu_protocol::{ENV_VARIABLE_ID, IN_VARIABLE_ID, NU_VARIABLE_ID, Span, Value};
4
+
use std::collections::HashMap;
5
+
6
+
pub fn eval_variable_for_completion(
7
+
var_id: nu_protocol::VarId,
8
+
working_set: &StateWorkingSet,
9
+
engine_guard: &EngineState,
10
+
stack_guard: &Stack,
11
+
) -> Option<Value> {
12
+
match var_id {
13
+
id if id == NU_VARIABLE_ID => {
14
+
// $nu - get from engine state constant
15
+
engine_guard.get_constant(id).cloned()
16
+
}
17
+
id if id == ENV_VARIABLE_ID => {
18
+
// $env - build from environment variables in engine state
19
+
// EnvVars is HashMap<String, HashMap<String, Value>> (overlay -> vars)
20
+
let mut pairs: Vec<(String, Value)> = Vec::new();
21
+
for overlay_env in engine_guard.env_vars.values() {
22
+
for (name, value) in overlay_env.iter() {
23
+
pairs.push((name.clone(), value.clone()));
24
+
}
25
+
}
26
+
pairs.sort_by(|a, b| a.0.cmp(&b.0));
27
+
// Deduplicate by name (later overlays override earlier ones)
28
+
pairs.dedup_by(|a, b| a.0 == b.0);
29
+
Some(Value::record(pairs.into_iter().collect(), Span::unknown()))
30
+
}
31
+
id if id == IN_VARIABLE_ID => {
32
+
// $in - typically not available at completion time
33
+
None
34
+
}
35
+
_ => {
36
+
// User-defined variable - try to get const value first
37
+
let var_info = working_set.get_variable(var_id);
38
+
if let Some(const_val) = &var_info.const_val {
39
+
Some(const_val.clone())
40
+
} else {
41
+
// Variable doesn't have a const value (runtime value)
42
+
// Try to get the value from the stack (runtime storage)
43
+
match stack_guard.get_var(var_id, Span::unknown()) {
44
+
Ok(value) => {
45
+
console_log!("[completion] Found variable {var_id:?} value in stack");
46
+
Some(value)
47
+
}
48
+
Err(_) => {
49
+
// Variable not in stack either
50
+
console_log!(
51
+
"[completion] Variable {var_id:?} has no const value and not in stack, type: {ty:?}",
52
+
ty = var_info.ty
53
+
);
54
+
None
55
+
}
56
+
}
57
+
}
58
+
}
59
+
}
60
+
}
61
+
62
+
pub fn get_columns_from_value(value: &Value) -> Vec<(String, Option<String>)> {
63
+
match value {
64
+
Value::Record { val, .. } => val
65
+
.iter()
66
+
.map(|(name, v)| (name.to_string(), Some(v.get_type().to_string())))
67
+
.collect(),
68
+
Value::List { vals, .. } => {
69
+
// Get common columns from list of records
70
+
if let Some(first) = vals.first() {
71
+
if let Value::Record { val, .. } = first {
72
+
return val
73
+
.iter()
74
+
.map(|(name, v)| (name.to_string(), Some(v.get_type().to_string())))
75
+
.collect();
76
+
}
77
+
}
78
+
vec![]
79
+
}
80
+
_ => vec![],
81
+
}
82
+
}
83
+
84
+
pub fn follow_cell_path(value: &Value, path: &[&str]) -> Option<Value> {
85
+
let mut current = value.clone();
86
+
for member in path {
87
+
match ¤t {
88
+
Value::Record { val, .. } => {
89
+
current = val.get(member)?.clone();
90
+
}
91
+
Value::List { vals, .. } => {
92
+
// Try to parse as index or get from first record
93
+
if let Ok(idx) = member.parse::<usize>() {
94
+
current = vals.get(idx)?.clone();
95
+
} else if let Some(first) = vals.first() {
96
+
if let Value::Record { val, .. } = first {
97
+
current = val.get(member)?.clone();
98
+
} else {
99
+
return None;
100
+
}
101
+
} else {
102
+
return None;
103
+
}
104
+
}
105
+
_ => return None,
106
+
}
107
+
}
108
+
Some(current)
109
+
}
110
+
111
+
pub fn extract_closure_params(input: &str, cursor_pos: usize) -> Vec<String> {
112
+
let mut params = Vec::new();
113
+
114
+
// Find all closures in the input by looking for {|...| patterns
115
+
// We need to find closures that contain the cursor position
116
+
let mut brace_stack: Vec<usize> = Vec::new(); // Stack of opening brace positions
117
+
let mut closures: Vec<(usize, usize, Vec<String>)> = Vec::new(); // (start, end, params)
118
+
119
+
let mut i = 0;
120
+
let chars: Vec<char> = input.chars().collect();
121
+
122
+
while i < chars.len() {
123
+
if chars[i] == '{' {
124
+
brace_stack.push(i);
125
+
} else if chars[i] == '}' {
126
+
if let Some(start) = brace_stack.pop() {
127
+
// Check if this is a closure with parameters: {|param| ...}
128
+
if start + 1 < chars.len() && chars[start + 1] == '|' {
129
+
// Find the parameter list
130
+
let param_start = start + 2;
131
+
let mut param_end = param_start;
132
+
133
+
// Find the closing | of the parameter list
134
+
while param_end < chars.len() && chars[param_end] != '|' {
135
+
param_end += 1;
136
+
}
137
+
138
+
if param_end < chars.len() {
139
+
// Extract parameter names
140
+
let params_text: String = chars[param_start..param_end].iter().collect();
141
+
let param_names: Vec<String> = params_text
142
+
.split(',')
143
+
.map(|s| s.trim().to_string())
144
+
.filter(|s| !s.is_empty())
145
+
.collect();
146
+
147
+
closures.push((start, i + 1, param_names));
148
+
}
149
+
}
150
+
}
151
+
}
152
+
i += 1;
153
+
}
154
+
155
+
// Find closures that contain the cursor position
156
+
// A closure contains the cursor if: start <= cursor_pos < end
157
+
for (start, end, param_names) in closures {
158
+
if start <= cursor_pos && cursor_pos < end {
159
+
console_log!(
160
+
"[completion] Found closure at [{start}, {end}) containing cursor {cursor_pos}, params: {param_names:?}"
161
+
);
162
+
params.extend(param_names);
163
+
}
164
+
}
165
+
166
+
params
167
+
}
168
+
169
+
pub fn collect_variables(
170
+
working_set: &StateWorkingSet,
171
+
input: &str,
172
+
cursor_pos: usize,
173
+
) -> HashMap<String, nu_protocol::VarId> {
174
+
let mut variables = HashMap::new();
175
+
176
+
// Add built-in variables
177
+
variables.insert("$nu".to_string(), NU_VARIABLE_ID);
178
+
variables.insert("$in".to_string(), IN_VARIABLE_ID);
179
+
variables.insert("$env".to_string(), ENV_VARIABLE_ID);
180
+
181
+
// Collect closure parameters at cursor position
182
+
// We don't need real var_ids for closure parameters since they're not evaluated yet
183
+
// We'll use a placeholder var_id (using IN_VARIABLE_ID as a safe placeholder)
184
+
// The actual var_id lookup will happen when the variable is used
185
+
let closure_params = extract_closure_params(input, cursor_pos);
186
+
for param_name in closure_params {
187
+
let var_name = format!("${}", param_name);
188
+
// Use IN_VARIABLE_ID as placeholder - it's safe since we're just using it for the name
189
+
// The completion logic only needs the name, not the actual var_id
190
+
variables.insert(var_name.clone(), IN_VARIABLE_ID);
191
+
console_log!("[completion] Added closure parameter: {var_name:?}");
192
+
}
193
+
194
+
// Collect from working set delta scope
195
+
let mut removed_overlays = vec![];
196
+
for scope_frame in working_set.delta.scope.iter().rev() {
197
+
for overlay_frame in scope_frame.active_overlays(&mut removed_overlays).rev() {
198
+
for (name, var_id) in &overlay_frame.vars {
199
+
let name = String::from_utf8_lossy(name).to_string();
200
+
variables.insert(name, *var_id);
201
+
}
202
+
}
203
+
}
204
+
205
+
// Collect from permanent state scope
206
+
for overlay_frame in working_set
207
+
.permanent_state
208
+
.active_overlays(&removed_overlays)
209
+
.rev()
210
+
{
211
+
for (name, var_id) in &overlay_frame.vars {
212
+
let name = String::from_utf8_lossy(name).to_string();
213
+
variables.insert(name, *var_id);
214
+
}
215
+
}
216
+
217
+
variables
218
+
}
-213
src/completion.rs
-213
src/completion.rs
···
1
-
use super::*;
2
-
3
-
#[derive(Debug, Serialize)]
4
-
struct Suggestion {
5
-
name: String,
6
-
description: Option<String>,
7
-
is_command: bool,
8
-
rendered: String,
9
-
span_start: usize, // char index (not byte)
10
-
span_end: usize, // char index (not byte)
11
-
}
12
-
13
-
impl PartialEq for Suggestion {
14
-
fn eq(&self, other: &Self) -> bool {
15
-
self.name == other.name
16
-
}
17
-
}
18
-
impl Eq for Suggestion {}
19
-
impl PartialOrd for Suggestion {
20
-
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
21
-
self.name.partial_cmp(&other.name)
22
-
}
23
-
}
24
-
impl Ord for Suggestion {
25
-
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
26
-
self.name.cmp(&other.name)
27
-
}
28
-
}
29
-
30
-
#[wasm_bindgen]
31
-
pub fn completion(input: &str, js_cursor_pos: usize) -> String {
32
-
let engine_guard = ENGINE_STATE
33
-
.get()
34
-
.unwrap()
35
-
.lock()
36
-
.expect("engine state initialized");
37
-
let root = get_pwd();
38
-
39
-
// Map UTF-16 cursor position (from JS) to Byte index (for Rust)
40
-
let byte_pos = input
41
-
.char_indices()
42
-
.map(|(i, _)| i)
43
-
.nth(js_cursor_pos)
44
-
.unwrap_or(input.len());
45
-
46
-
// 1. Parse & Flatten
47
-
let mut working_set = StateWorkingSet::new(&engine_guard);
48
-
// CRITICAL: Capture the start offset so we can normalize spans later
49
-
let global_offset = working_set.next_span_start();
50
-
let block = parse(&mut working_set, None, input.as_bytes(), false);
51
-
let shapes = flatten_block(&working_set, &block);
52
-
53
-
// 2. Identify context
54
-
let mut is_command_pos = false;
55
-
let mut current_span = Span::new(byte_pos, byte_pos);
56
-
let mut prefix = "".to_string();
57
-
let mut found_shape = false;
58
-
59
-
// Check if cursor is inside or touching a shape
60
-
for (span, shape) in &shapes {
61
-
// Convert global span to local indices
62
-
let local_start = span.start.saturating_sub(global_offset);
63
-
let local_end = span.end.saturating_sub(global_offset);
64
-
let local_span = Span::new(local_start, local_end);
65
-
66
-
if local_span.contains(byte_pos) || local_span.end == byte_pos {
67
-
current_span = local_span;
68
-
found_shape = true;
69
-
let safe_end = std::cmp::min(local_span.end, byte_pos);
70
-
if local_span.start < input.len() {
71
-
prefix = input[local_span.start..safe_end].to_string();
72
-
}
73
-
74
-
match shape {
75
-
FlatShape::External(_) | FlatShape::InternalCall(_) | FlatShape::Keyword => {
76
-
is_command_pos = true;
77
-
}
78
-
FlatShape::Garbage => {
79
-
// Check if it looks like a flag
80
-
if prefix.starts_with('-') {
81
-
is_command_pos = false;
82
-
} else {
83
-
// Assume command if it's garbage but not a flag (e.g. typing a new command)
84
-
is_command_pos = true;
85
-
}
86
-
}
87
-
_ => {
88
-
is_command_pos = false;
89
-
}
90
-
}
91
-
break;
92
-
}
93
-
}
94
-
95
-
// Fallback to Lexer if in whitespace
96
-
if !found_shape {
97
-
let (tokens, _err) = lex(input.as_bytes(), 0, &[], &[], true);
98
-
let last_token = tokens.iter().filter(|t| t.span.end <= byte_pos).last();
99
-
100
-
if let Some(token) = last_token {
101
-
match token.contents {
102
-
TokenContents::Pipe
103
-
| TokenContents::PipePipe
104
-
| TokenContents::Semicolon
105
-
| TokenContents::Eol => {
106
-
is_command_pos = true;
107
-
}
108
-
_ => {
109
-
let text = &input[token.span.start..token.span.end];
110
-
if text == "{" || text == "(" || text == ";" || text == "&&" || text == "||" {
111
-
is_command_pos = true;
112
-
} else {
113
-
is_command_pos = false;
114
-
}
115
-
}
116
-
}
117
-
} else {
118
-
is_command_pos = true; // Start of input
119
-
}
120
-
}
121
-
122
-
let mut suggestions: Vec<Suggestion> = Vec::new();
123
-
124
-
// Convert byte-spans back to char-spans for JS
125
-
let to_char_span = |start: usize, end: usize| -> (usize, usize) {
126
-
let char_start = input[..start].chars().count();
127
-
let char_end = input[..end].chars().count();
128
-
(char_start, char_end)
129
-
};
130
-
131
-
let (span_start, span_end) = to_char_span(current_span.start, current_span.end);
132
-
133
-
let mut add_cmd_suggestion = |name: Vec<u8>, desc: Option<String>| {
134
-
let name = String::from_utf8_lossy(&name).to_string();
135
-
suggestions.push(Suggestion {
136
-
rendered: {
137
-
let name = ansi_term::Color::Green.bold().paint(&name);
138
-
let desc = desc.as_deref().unwrap_or("<no description>");
139
-
format!("{name} {desc}")
140
-
},
141
-
name: name.clone(), // Replacement text is just the name
142
-
description: desc,
143
-
is_command: true,
144
-
span_start,
145
-
span_end,
146
-
});
147
-
};
148
-
149
-
let working_set = StateWorkingSet::new(&engine_guard);
150
-
151
-
if is_command_pos {
152
-
for (_, name, desc, _) in working_set
153
-
.find_commands_by_predicate(|value| value.starts_with(prefix.as_bytes()), true)
154
-
{
155
-
add_cmd_suggestion(name, desc);
156
-
}
157
-
} else {
158
-
// File completion
159
-
// Split prefix into directory and file part
160
-
let (dir, file_prefix) = if let Some(idx) = prefix.rfind('/') {
161
-
(&prefix[..idx + 1], &prefix[idx + 1..])
162
-
} else {
163
-
("", prefix.as_str())
164
-
};
165
-
166
-
// Fix: Clean up the directory path before joining.
167
-
// VFS often fails if 'join' is called with a trailing slash (e.g. "a/") because it interprets it as "a" + ""
168
-
let dir_to_join = if dir.len() > 1 && dir.ends_with('/') {
169
-
&dir[..dir.len() - 1]
170
-
} else {
171
-
dir
172
-
};
173
-
174
-
let target_dir = if !dir.is_empty() {
175
-
match root.join(dir_to_join) {
176
-
Ok(d) => {
177
-
if let Ok(true) = d.is_dir() {
178
-
Some(d)
179
-
} else {
180
-
None
181
-
}
182
-
}
183
-
Err(_) => None,
184
-
}
185
-
} else {
186
-
// If prefix is empty, list current directory
187
-
Some(root.join("").unwrap())
188
-
};
189
-
190
-
if let Some(d) = target_dir {
191
-
if let Ok(iterator) = d.read_dir() {
192
-
for entry in iterator {
193
-
let name = entry.filename();
194
-
if name.starts_with(file_prefix) {
195
-
let full_completion = format!("{}{}", dir, name);
196
-
suggestions.push(Suggestion {
197
-
name: full_completion.clone(),
198
-
description: None,
199
-
is_command: false,
200
-
rendered: full_completion,
201
-
span_start,
202
-
span_end,
203
-
})
204
-
}
205
-
}
206
-
}
207
-
}
208
-
}
209
-
210
-
suggestions.sort();
211
-
212
-
serde_json::to_string(&suggestions).unwrap_or_else(|_| "[]".to_string())
213
-
}
+1
-1
src/default_context.rs
+1
-1
src/default_context.rs
+71
-17
src/error.rs
+71
-17
src/error.rs
···
1
-
use miette::{GraphicalReportHandler, Report, SourceCode, SourceSpan, SpanContents};
1
+
use miette::{Diagnostic, GraphicalReportHandler, Report, SourceCode, SourceSpan, SpanContents};
2
+
use nu_protocol::{ShellError, Span};
3
+
use vfs::{VfsError, error::VfsErrorKind};
2
4
3
5
pub struct CommandError {
4
6
pub error: Report,
5
7
pub start_offset: usize,
8
+
pub input: String,
9
+
}
10
+
11
+
impl CommandError {
12
+
pub fn new<E>(error: E, input: impl Into<String>) -> Self
13
+
where
14
+
E: Diagnostic + Clone + Send + Sync + 'static,
15
+
{
16
+
Self {
17
+
error: Report::new(error),
18
+
start_offset: 0,
19
+
input: input.into(),
20
+
}
21
+
}
22
+
23
+
pub fn with_start_offset(mut self, start_offset: usize) -> Self {
24
+
self.start_offset = start_offset;
25
+
self
26
+
}
27
+
}
28
+
29
+
impl From<ShellError> for CommandError {
30
+
fn from(value: ShellError) -> Self {
31
+
CommandError::new(value, String::new())
32
+
}
33
+
}
34
+
35
+
impl From<CommandError> for String {
36
+
fn from(value: CommandError) -> Self {
37
+
let handler = GraphicalReportHandler::new()
38
+
.with_theme(miette::GraphicalTheme::unicode())
39
+
.with_cause_chain();
40
+
41
+
if value.input.is_empty() {
42
+
let mut msg = String::new();
43
+
handler
44
+
.render_report(&mut msg, value.error.as_ref())
45
+
.unwrap();
46
+
return msg;
47
+
}
48
+
49
+
let source = OffsetSource {
50
+
source: value.input,
51
+
start_offset: value.start_offset,
52
+
};
53
+
54
+
let report_with_source = value.error.with_source_code(source);
55
+
let mut msg = String::new();
56
+
handler
57
+
.render_report(&mut msg, report_with_source.as_ref())
58
+
.unwrap();
59
+
msg
60
+
}
6
61
}
7
62
8
63
pub struct OffsetSource {
···
59
114
}
60
115
}
61
116
62
-
pub fn format_error(error: Report, input: String, start_offset: usize) -> String {
63
-
let handler = GraphicalReportHandler::new()
64
-
.with_theme(miette::GraphicalTheme::unicode())
65
-
.with_cause_chain();
66
-
67
-
let source = OffsetSource {
68
-
source: input,
69
-
start_offset,
70
-
};
71
-
72
-
let report_with_source = error.with_source_code(source);
73
-
let mut output = String::new();
74
-
handler
75
-
.render_report(&mut output, report_with_source.as_ref())
76
-
.unwrap();
77
-
output
117
+
pub fn to_shell_err(span: Span) -> impl Fn(VfsError) -> ShellError {
118
+
move |vfs_error: VfsError| ShellError::GenericError {
119
+
error: (match vfs_error.kind() {
120
+
VfsErrorKind::DirectoryExists
121
+
| VfsErrorKind::FileExists
122
+
| VfsErrorKind::FileNotFound
123
+
| VfsErrorKind::InvalidPath => "path error",
124
+
_ => "io error",
125
+
})
126
+
.to_string(),
127
+
msg: vfs_error.to_string(),
128
+
span: Some(span),
129
+
help: None,
130
+
inner: vec![],
131
+
}
78
132
}
+83
-41
src/globals.rs
+83
-41
src/globals.rs
···
1
1
use futures::stream::AbortHandle;
2
-
use nu_protocol::{
3
-
ShellError, Span,
4
-
engine::{EngineState, StateDelta},
5
-
};
2
+
use nu_protocol::Signal;
3
+
use rust_embed::RustEmbed;
6
4
use std::{
7
5
collections::HashMap,
8
6
sync::{
···
11
9
},
12
10
time::{Duration, SystemTime, UNIX_EPOCH},
13
11
};
14
-
use vfs::{VfsError, VfsPath, error::VfsErrorKind};
12
+
use vfs::{EmbeddedFS, OverlayFS, VfsPath};
15
13
use wasm_bindgen::prelude::*;
16
14
17
15
use crate::memory_fs::MemoryFS;
18
16
19
17
static ROOT: OnceLock<Arc<VfsPath>> = OnceLock::new();
20
18
19
+
fn init_vfs() -> Arc<VfsPath> {
20
+
let memory_fs = VfsPath::new(MemoryFS::new());
21
+
let embedded_fs = VfsPath::new(EmbeddedFS::<EmbeddedFiles>::new());
22
+
let overlaid_fs = VfsPath::new(OverlayFS::new(&[memory_fs, embedded_fs]));
23
+
Arc::new(overlaid_fs)
24
+
}
25
+
21
26
pub fn get_vfs() -> Arc<VfsPath> {
22
-
ROOT.get_or_init(|| Arc::new(VfsPath::new(MemoryFS::new())))
23
-
.clone()
27
+
ROOT.get_or_init(init_vfs).clone()
24
28
}
25
29
30
+
#[derive(RustEmbed, Debug)]
31
+
#[folder = "embedded/"]
32
+
#[exclude = ".gitkeep"]
33
+
pub struct EmbeddedFiles;
34
+
26
35
static PWD: OnceLock<RwLock<Arc<VfsPath>>> = OnceLock::new();
27
36
28
37
pub fn get_pwd() -> Arc<VfsPath> {
···
34
43
35
44
pub fn set_pwd(path: Arc<VfsPath>) {
36
45
*PWD.get_or_init(|| RwLock::new(get_vfs())).write().unwrap() = path;
37
-
}
38
-
39
-
pub fn to_shell_err(span: Span) -> impl Fn(VfsError) -> ShellError {
40
-
move |vfs_error: VfsError| ShellError::GenericError {
41
-
error: (match vfs_error.kind() {
42
-
VfsErrorKind::DirectoryExists
43
-
| VfsErrorKind::FileExists
44
-
| VfsErrorKind::FileNotFound
45
-
| VfsErrorKind::InvalidPath => "path error",
46
-
_ => "io error",
47
-
})
48
-
.to_string(),
49
-
msg: vfs_error.to_string(),
50
-
span: Some(span),
51
-
help: None,
52
-
inner: vec![],
53
-
}
54
46
}
55
47
56
48
pub struct TaskInfo {
···
141
133
false
142
134
}
143
135
144
-
static PENDING_DELTAS: OnceLock<Mutex<Vec<StateDelta>>> = OnceLock::new();
136
+
// static PENDING_DELTAS: OnceLock<Mutex<Vec<StateDelta>>> = OnceLock::new();
145
137
146
-
pub fn queue_delta(delta: StateDelta) {
147
-
let _ = PENDING_DELTAS.get_or_init(|| Mutex::new(Vec::new()));
148
-
if let Ok(mut guard) = PENDING_DELTAS.get().unwrap().lock() {
149
-
guard.push(delta);
150
-
}
151
-
}
138
+
// pub fn queue_delta(delta: StateDelta) {
139
+
// let _ = PENDING_DELTAS.get_or_init(|| Mutex::new(Vec::new()));
140
+
// if let Ok(mut guard) = PENDING_DELTAS.get().unwrap().lock() {
141
+
// guard.push(delta);
142
+
// }
143
+
// }
152
144
153
-
pub fn apply_pending_deltas(engine_state: &mut EngineState) -> Result<(), ShellError> {
154
-
if let Some(mutex) = PENDING_DELTAS.get() {
155
-
if let Ok(mut guard) = mutex.lock() {
156
-
for delta in guard.drain(..) {
157
-
engine_state.merge_delta(delta)?;
158
-
}
159
-
}
160
-
}
161
-
Ok(())
162
-
}
145
+
// pub fn apply_pending_deltas(engine_state: &mut EngineState) -> Result<(), ShellError> {
146
+
// if let Some(mutex) = PENDING_DELTAS.get() {
147
+
// if let Ok(mut guard) = mutex.lock() {
148
+
// for delta in guard.drain(..) {
149
+
// engine_state.merge_delta(delta)?;
150
+
// }
151
+
// }
152
+
// }
153
+
// Ok(())
154
+
// }
163
155
164
156
pub static CONSOLE_CALLBACK: OnceLock<Mutex<Option<CallbackWrapper>>> = OnceLock::new();
165
157
···
187
179
pub fn current_time() -> Option<SystemTime> {
188
180
UNIX_EPOCH.checked_add(Duration::from_millis(js_sys::Date::now() as u64))
189
181
}
182
+
183
+
use js_sys::Int32Array;
184
+
use std::cell::RefCell;
185
+
186
+
// We use thread_local storage because the Wasm worker is single-threaded.
187
+
// This holds the reference to the SharedArrayBuffer view passed from JS.
188
+
thread_local! {
189
+
pub static INTERRUPT_BUFFER: RefCell<Option<Int32Array>> = RefCell::new(None);
190
+
}
191
+
192
+
#[wasm_bindgen]
193
+
pub fn set_interrupt_buffer(buffer: Int32Array) {
194
+
INTERRUPT_BUFFER.with(|b| {
195
+
*b.borrow_mut() = Some(buffer);
196
+
});
197
+
}
198
+
199
+
pub fn check_interrupt() -> bool {
200
+
INTERRUPT_BUFFER.with(|b| {
201
+
if let Some(buffer) = b.borrow().as_ref() {
202
+
match js_sys::Atomics::load(buffer, 0) {
203
+
Ok(1) => true,
204
+
_ => false,
205
+
}
206
+
} else {
207
+
false
208
+
}
209
+
})
210
+
}
211
+
212
+
pub fn set_interrupt(value: bool) {
213
+
INTERRUPT_BUFFER.with(|b| {
214
+
if let Some(buffer) = b.borrow().as_ref() {
215
+
let _ = js_sys::Atomics::store(buffer, 0, value as i32);
216
+
}
217
+
});
218
+
}
219
+
220
+
pub struct InterruptBool;
221
+
222
+
impl Signal for InterruptBool {
223
+
#[inline]
224
+
fn get(&self) -> bool {
225
+
check_interrupt()
226
+
}
227
+
#[inline]
228
+
fn set(&self, value: bool) {
229
+
set_interrupt(value);
230
+
}
231
+
}
+15
-15
src/highlight.rs
+15
-15
src/highlight.rs
···
1
+
use futures::FutureExt;
2
+
use js_sys::Promise;
3
+
use wasm_bindgen_futures::future_to_promise;
4
+
1
5
use super::*;
2
6
3
7
#[wasm_bindgen]
4
-
pub fn highlight(input: &str) -> String {
5
-
let engine_guard = ENGINE_STATE
6
-
.get()
7
-
.unwrap()
8
-
.lock()
9
-
.expect("engine state initialized");
10
-
let mut working_set = StateWorkingSet::new(&engine_guard);
11
-
12
-
// Capture the global offset before parsing, as parse will generate spans relative to this
13
-
let global_offset = working_set.next_span_start();
14
-
15
-
// Parse the input block using Nushell's full parser
16
-
let block = parse(&mut working_set, None, input.as_bytes(), false);
8
+
pub fn highlight(input: String) -> Promise {
9
+
future_to_promise(highlight_impl(input).map(|s| Ok(JsValue::from_str(&s))))
10
+
}
17
11
18
-
// Flatten the block to get shapes (semantic tokens)
19
-
let shapes = flatten_block(&working_set, &block);
12
+
pub async fn highlight_impl(input: String) -> String {
13
+
let (global_offset, shapes) = {
14
+
let engine_guard = read_engine_state().await;
15
+
let mut working_set = StateWorkingSet::new(&engine_guard);
16
+
let offset = working_set.next_span_start();
17
+
let block = parse(&mut working_set, None, input.as_bytes(), false);
18
+
(offset, flatten_block(&working_set, &block))
19
+
};
20
20
21
21
let mut output = String::new();
22
22
let mut last_end = 0;
+171
-136
src/lib.rs
+171
-136
src/lib.rs
···
1
-
use jacquard::chrono;
2
-
use miette::Report;
1
+
use async_lock::{RwLock, RwLockReadGuard, RwLockUpgradableReadGuard, RwLockWriteGuard};
2
+
use futures::TryFutureExt;
3
+
use js_sys::Promise;
4
+
use nu_cmd_base::hook::eval_hook;
3
5
use nu_cmd_extra::add_extra_command_context;
4
6
use nu_cmd_lang::create_default_context;
5
7
use nu_engine::{command_prelude::*, eval_block};
6
-
use nu_parser::{FlatShape, TokenContents, flatten_block, lex, parse};
8
+
use nu_parser::{FlatShape, flatten_block, parse};
7
9
use nu_protocol::{
8
-
Config, ListStream, PipelineData, Span,
9
-
engine::{Call, EngineState, Stack, StateWorkingSet},
10
+
Config, ListStream, PipelineData, Signals, Span,
11
+
engine::{EngineState, Stack, StateWorkingSet},
10
12
};
11
-
use serde::Serialize;
12
13
use std::{
14
+
fmt::Write,
13
15
io::Cursor,
14
-
sync::{Arc, Mutex, OnceLock},
15
-
time::UNIX_EPOCH,
16
+
sync::{Arc, OnceLock},
16
17
};
17
-
use vfs::VfsError;
18
18
use wasm_bindgen::prelude::*;
19
+
use wasm_bindgen_futures::future_to_promise;
19
20
20
21
pub mod cmd;
21
22
pub mod completion;
···
27
28
28
29
use crate::{
29
30
cmd::{
30
-
Cd, Fetch, Job, JobKill, JobList, Ls, Mkdir, Open, Pwd, Random, Rm, Save, Source, Sys,
31
-
Version,
31
+
Cd, Eval, Fetch, Glob, Job, JobKill, JobList, Ls, Mkdir, Mv, Open, Print, Pwd, Random, Rm,
32
+
Save, SourceFile, Sys,
32
33
},
33
34
default_context::add_shell_command_context,
34
-
error::format_error,
35
-
globals::{apply_pending_deltas, current_time, get_pwd, print_to_console},
35
+
globals::{InterruptBool, get_pwd, get_vfs, print_to_console, set_interrupt},
36
36
};
37
37
use error::CommandError;
38
-
use globals::get_vfs;
39
38
40
-
static ENGINE_STATE: OnceLock<Mutex<EngineState>> = OnceLock::new();
41
-
static STACK: OnceLock<Mutex<Stack>> = OnceLock::new();
39
+
#[wasm_bindgen]
40
+
extern "C" {
41
+
#[wasm_bindgen(js_namespace = console)]
42
+
fn error(msg: String);
42
43
43
-
fn init_engine_internal() -> Result<(), Report> {
44
-
let mut engine_state = create_default_context();
45
-
engine_state = add_shell_command_context(engine_state);
46
-
engine_state = add_extra_command_context(engine_state);
44
+
type Error;
47
45
48
-
let write_file = |name: &str, contents: &str| {
49
-
get_vfs()
50
-
.join(name)
51
-
.and_then(|p| p.create_file())
52
-
.and_then(|mut f| f.write_all(contents.as_bytes()).map_err(VfsError::from))
53
-
.map_err(|e| miette::miette!(e.to_string()))
54
-
};
46
+
#[wasm_bindgen(constructor)]
47
+
fn new() -> Error;
55
48
56
-
let access_log = format!(
57
-
r#"/dysnomia.v000 /user: 90008/ /ip: [REDACTED]/ /time: [REDACTED]//
58
-
/dysnomia.v002 /user: 90008/ /ip: [REDACTED]/ /time: [REDACTED]//
59
-
/dysnomia.v011 /user: 90008/ /ip: [REDACTED]/ /time: [REDACTED]//
60
-
[...ENTRIES TRUNCATED...]
61
-
/dysnomia.v099 /user: anonymous/ /ip: [REDACTED]/ /time: {time}//"#,
62
-
time = current_time()
63
-
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
64
-
.map_or_else(
65
-
|| "unknown".to_string(),
66
-
|time| chrono::DateTime::from_timestamp_nanos(time.as_nanos() as i64)
67
-
.format("%Y-%m-%dT%H:%M:%SZ")
68
-
.to_string()
69
-
)
70
-
);
71
-
write_file(".access.log", &access_log)?;
49
+
#[wasm_bindgen(structural, method, getter)]
50
+
fn stack(error: &Error) -> String;
51
+
}
72
52
73
-
let welcome_txt = r#"welcome anonymous !
53
+
fn panic_hook(info: &std::panic::PanicHookInfo) {
54
+
let mut msg = info.to_string();
74
55
56
+
msg.push_str("\n\nStack:\n\n");
57
+
let e = Error::new();
58
+
let stack = e.stack();
59
+
msg.push_str(&stack);
60
+
msg.push_str("\n\n");
75
61
76
-
you are interfacing with dysnomia.v099
77
-
using the nu shell.
62
+
let _ = print_to_console(&msg, false);
63
+
}
78
64
65
+
static ENGINE_STATE: OnceLock<RwLock<EngineState>> = OnceLock::new();
66
+
#[inline]
67
+
async fn read_engine_state() -> RwLockReadGuard<'static, EngineState> {
68
+
unsafe { ENGINE_STATE.get().unwrap_unchecked() }
69
+
.read()
70
+
.await
71
+
}
72
+
#[inline]
73
+
async fn write_engine_state() -> RwLockWriteGuard<'static, EngineState> {
74
+
unsafe { ENGINE_STATE.get().unwrap_unchecked() }
75
+
.write()
76
+
.await
77
+
}
79
78
80
-
a few commands you can try:
79
+
static STACK: OnceLock<RwLock<Stack>> = OnceLock::new();
80
+
#[inline]
81
+
async fn read_stack() -> RwLockReadGuard<'static, Stack> {
82
+
unsafe { STACK.get().unwrap_unchecked() }.read().await
83
+
}
84
+
#[inline]
85
+
async fn write_stack() -> RwLockWriteGuard<'static, Stack> {
86
+
unsafe { STACK.get().unwrap_unchecked() }.write().await
87
+
}
88
+
89
+
#[wasm_bindgen]
90
+
pub fn init_engine() -> Promise {
91
+
std::panic::set_hook(Box::new(panic_hook));
92
+
future_to_promise(
93
+
init_engine_internal()
94
+
.map_ok(|_| JsValue::null())
95
+
.map_err(|s| JsValue::from_str(&s)),
96
+
)
97
+
}
81
98
82
-
"hello, user!" | save message.txt
83
-
fetch at://ptr.pet
84
-
ls --help"#;
85
-
write_file("welcome.txt", &welcome_txt)?;
99
+
async fn init_engine_internal() -> Result<(), String> {
100
+
let mut engine_state = create_default_context();
101
+
engine_state = add_shell_command_context(engine_state);
102
+
engine_state = add_extra_command_context(engine_state);
86
103
87
104
let mut working_set = StateWorkingSet::new(&engine_state);
88
-
let decls: [Box<dyn Command>; 15] = [
105
+
let decls: [Box<dyn Command>; 18] = [
89
106
Box::new(Ls),
90
107
Box::new(Open),
91
108
Box::new(Save),
92
109
Box::new(Mkdir),
110
+
Box::new(Mv),
93
111
Box::new(Pwd),
94
112
Box::new(Cd),
95
113
Box::new(Rm),
96
114
Box::new(Fetch),
97
-
Box::new(Source),
115
+
Box::new(SourceFile),
116
+
Box::new(Eval),
98
117
Box::new(Job),
99
118
Box::new(JobList),
100
119
Box::new(JobKill),
101
120
Box::new(Sys),
102
121
Box::new(Random),
103
-
Box::new(Version),
122
+
Box::new(Print),
123
+
Box::new(Glob),
104
124
];
105
125
for decl in decls {
106
126
working_set.add_decl(decl);
107
127
}
108
-
engine_state.merge_delta(working_set.delta)?;
128
+
engine_state
129
+
.merge_delta(working_set.delta)
130
+
.map_err(CommandError::from)?;
109
131
110
132
let mut config = Config::default();
111
133
config.use_ansi_coloring = true.into();
112
134
config.show_banner = nu_protocol::BannerKind::Full;
135
+
config.hooks.display_output = Some("table".into_value(Span::unknown()));
136
+
config.table.show_empty = false;
113
137
engine_state.config = Arc::new(config);
114
138
139
+
engine_state.set_signals(Signals::new(Arc::new(InterruptBool)));
140
+
115
141
ENGINE_STATE
116
-
.set(Mutex::new(engine_state))
117
-
.map_err(|_| miette::miette!("ENGINE_STATE was already set!?"))?;
142
+
.set(RwLock::new(engine_state))
143
+
.map_err(|_| "ENGINE_STATE was already set!?".to_string())?;
118
144
STACK
119
-
.set(Mutex::new(Stack::new()))
120
-
.map_err(|_| miette::miette!("STACK was already set!?"))?;
145
+
.set(RwLock::new(Stack::new()))
146
+
.map_err(|_| "STACK was already set!?".to_string())?;
147
+
148
+
let mut startup_script = String::new();
149
+
150
+
// source our "nu rc"
151
+
let rc_path = get_vfs().join("/.env.nu").ok();
152
+
let rc = rc_path.and_then(|env| env.exists().ok().and_then(|ok| ok.then_some(env)));
153
+
if let Some(env) = rc {
154
+
writeln!(&mut startup_script, "eval file {path}", path = env.as_str()).unwrap();
155
+
}
156
+
157
+
// add some aliases for some commands
158
+
let aliases = ["alias l = ls", "alias la = ls -a", "alias . = eval file"];
159
+
for alias in aliases {
160
+
writeln!(&mut startup_script, "{alias}").unwrap();
161
+
}
121
162
122
-
// web_sys::console::log_1(&"Hello, World!".into());
163
+
run_command_internal(&startup_script).await?;
123
164
124
165
Ok(())
125
166
}
126
167
127
-
#[wasm_bindgen]
128
-
pub fn init_engine() -> String {
129
-
console_error_panic_hook::set_once();
130
-
init_engine_internal().map_or_else(|err| format!("error: {err}"), |_| String::new())
131
-
}
168
+
async fn run_command_internal(input: &str) -> Result<(), String> {
169
+
let mut engine_state = unsafe { ENGINE_STATE.get().unwrap_unchecked() }
170
+
.upgradable_read()
171
+
.await;
172
+
let (mut working_set, signals, config) = {
173
+
let mut write_engine_state = RwLockUpgradableReadGuard::upgrade(engine_state).await;
174
+
// apply_pending_deltas(&mut write_engine_state).map_err(|e| CommandError {
175
+
// error: Report::new(e),
176
+
// start_offset: 0,
177
+
// })?;
178
+
write_engine_state.add_env_var(
179
+
"PWD".to_string(),
180
+
get_pwd_string().into_value(Span::unknown()),
181
+
);
182
+
engine_state = RwLockWriteGuard::downgrade_to_upgradable(write_engine_state);
132
183
133
-
fn run_command_internal(
134
-
engine_state: &mut EngineState,
135
-
stack: &mut Stack,
136
-
input: &str,
137
-
) -> Result<(), CommandError> {
138
-
// apply any pending deltas from previous commands (like `source`)
139
-
apply_pending_deltas(engine_state).map_err(|e| CommandError {
140
-
error: Report::new(e),
141
-
start_offset: 0,
142
-
})?;
143
-
// set PWD
144
-
engine_state.add_env_var(
145
-
"PWD".to_string(),
146
-
get_pwd_string().into_value(Span::unknown()),
147
-
);
148
-
149
-
let mut working_set = StateWorkingSet::new(engine_state);
184
+
(
185
+
StateWorkingSet::new(&engine_state),
186
+
engine_state.signals().clone(),
187
+
engine_state.config.clone(),
188
+
)
189
+
};
150
190
let start_offset = working_set.next_span_start();
151
191
let block = parse(&mut working_set, Some("entry"), input.as_bytes(), false);
152
192
153
-
let cmd_err = |err: ShellError| CommandError {
154
-
error: Report::new(err),
155
-
start_offset,
156
-
};
193
+
let cmd_err = |err: ShellError| CommandError::new(err, input).with_start_offset(start_offset);
157
194
158
195
if let Some(err) = working_set.parse_errors.into_iter().next() {
159
-
return Err(CommandError {
160
-
error: Report::new(err),
161
-
start_offset,
162
-
});
196
+
return Err(CommandError::new(err, input)
197
+
.with_start_offset(start_offset)
198
+
.into());
163
199
}
164
200
if let Some(err) = working_set.compile_errors.into_iter().next() {
165
-
return Err(CommandError {
166
-
error: Report::new(err),
167
-
start_offset,
168
-
});
201
+
return Err(CommandError::new(err, input)
202
+
.with_start_offset(start_offset)
203
+
.into());
169
204
}
205
+
let delta = working_set.delta;
170
206
171
-
engine_state
172
-
.merge_delta(working_set.delta)
173
-
.map_err(cmd_err)?;
174
-
let result = eval_block::<nu_protocol::debugger::WithoutDebug>(
175
-
engine_state,
176
-
stack,
177
-
&block,
178
-
PipelineData::Empty,
179
-
);
207
+
let result = {
208
+
let mut write_engine_state = RwLockUpgradableReadGuard::upgrade(engine_state).await;
209
+
let mut stack = write_stack().await;
210
+
write_engine_state.merge_delta(delta).map_err(cmd_err)?;
211
+
engine_state = RwLockWriteGuard::downgrade_to_upgradable(write_engine_state);
212
+
let res = eval_block::<nu_protocol::debugger::WithoutDebug>(
213
+
&engine_state,
214
+
&mut stack,
215
+
&block,
216
+
PipelineData::Empty,
217
+
);
218
+
// apply_pending_deltas(&mut write_engine_state).map_err(cmd_err)?;
219
+
res
220
+
};
180
221
181
222
let pipeline_data = result.map_err(cmd_err)?.body;
182
-
let signals = engine_state.signals().clone();
183
223
184
224
// this is annoying but we have to collect here so we can uncover errors
185
225
// before passing the data off to Table, because otherwise Table
···
189
229
let pipeline_data = match pipeline_data {
190
230
PipelineData::Empty => return Ok(()),
191
231
PipelineData::Value(Value::Error { error, .. }, _) => {
192
-
return Err(cmd_err(*error));
232
+
return Err(cmd_err(*error).into());
193
233
}
194
234
PipelineData::ByteStream(s, m) => match (s.span(), s.type_(), s.reader()) {
195
235
(span, ty, Some(r)) => {
···
220
260
x => x,
221
261
};
222
262
223
-
// TODO: idk what this does i copied it from PipelineData::print_table
224
-
// dunno if it matters, we can just use nu_command::Table and it works fine i think
225
-
let table_command = engine_state
226
-
.table_decl_id
227
-
.map(|decl_id| engine_state.get_decl(decl_id))
228
-
.filter(|command| command.block_id().is_some())
229
-
.unwrap_or(&nu_command::Table);
230
-
let call = Call::new(pipeline_data.span().unwrap_or_else(Span::unknown));
231
-
232
-
let res = table_command
233
-
.run(engine_state, stack, &call, pipeline_data)
234
-
.map_err(cmd_err)?;
263
+
let res = {
264
+
let mut write_engine_state = RwLockUpgradableReadGuard::upgrade(engine_state).await;
265
+
let mut stack = write_stack().await;
266
+
eval_hook(
267
+
&mut write_engine_state,
268
+
&mut stack,
269
+
Some(pipeline_data),
270
+
vec![],
271
+
config.hooks.display_output.as_ref().unwrap(),
272
+
"display_output",
273
+
)
274
+
.map_err(cmd_err)?
275
+
};
235
276
236
277
match res {
237
278
PipelineData::Empty => {}
238
279
PipelineData::Value(v, _) => {
239
-
print_to_console(&v.to_expanded_string("\n", &engine_state.config), true)
280
+
print_to_console(&v.to_expanded_string("\n", &config), true);
240
281
}
241
282
PipelineData::ByteStream(s, _) => {
242
283
for line in s.lines().into_iter().flatten() {
···
246
287
}
247
288
PipelineData::ListStream(s, _) => {
248
289
for item in s.into_iter() {
249
-
let out = item.to_expanded_string("\n", &engine_state.config);
290
+
let out = item
291
+
.unwrap_error()
292
+
.map_err(cmd_err)?
293
+
.to_expanded_string("\n", &config);
250
294
print_to_console(&out, true);
251
295
}
252
296
}
···
256
300
}
257
301
258
302
#[wasm_bindgen]
259
-
pub fn run_command(input: &str) -> Option<String> {
260
-
let (mut engine_guard, mut stack_guard) = (
261
-
ENGINE_STATE
262
-
.get()
263
-
.unwrap()
264
-
.lock()
265
-
.expect("engine state initialized"),
266
-
STACK.get().unwrap().lock().expect("stack initialized"),
267
-
);
303
+
pub fn run_command(input: String) -> Promise {
304
+
set_interrupt(false);
268
305
269
-
match run_command_internal(&mut engine_guard, &mut stack_guard, input) {
270
-
Ok(_) => None,
271
-
Err(cmd_err) => Some(format_error(
272
-
cmd_err.error,
273
-
input.to_owned(),
274
-
cmd_err.start_offset,
275
-
)),
276
-
}
306
+
future_to_promise(async move {
307
+
run_command_internal(&input)
308
+
.map_ok(|_| JsValue::null())
309
+
.map_err(|s| JsValue::from_str(&s))
310
+
.await
311
+
})
277
312
}
278
313
279
314
#[wasm_bindgen]
+10
-10
www/index.html
+10
-10
www/index.html
···
1
1
<!doctype html>
2
2
<html lang="en">
3
-
<head>
4
-
<meta charset="UTF-8" />
5
-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
-
<title>dysnomia</title>
8
-
</head>
9
-
<body>
10
-
<div id="app"></div>
11
-
<script type="module" src="/src/main.ts"></script>
12
-
</body>
3
+
<head>
4
+
<meta charset="UTF-8" />
5
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+
<title>faunu</title>
8
+
</head>
9
+
<body>
10
+
<div id="app"></div>
11
+
<script type="module" src="/src/main.ts"></script>
12
+
</body>
13
13
</html>
+1
-1
www/package.json
+1
-1
www/package.json
+39
-29
www/src/main.ts
+39
-29
www/src/main.ts
···
14
14
type Candidate = {
15
15
name: string;
16
16
description: string;
17
-
is_command: boolean;
18
17
rendered: string;
19
18
span_start: number;
20
19
span_end: number;
···
23
22
type SearchState = "history" | "completion" | "none";
24
23
25
24
async function bootstrap() {
25
+
const sab = new SharedArrayBuffer(4);
26
+
const interruptBuffer = new Int32Array(sab);
27
+
26
28
// Instantiate the worker using the imported class
27
29
const worker = new DysnomiaWorker();
28
30
···
171
173
output += currentLine;
172
174
}
173
175
} catch (e) {
176
+
console.error(`cant highlight: ${e}`);
174
177
output += currentLine;
175
178
}
176
179
···
243
246
cursorPos = (before + candidate.name).length;
244
247
};
245
248
249
+
const applyCompletionFromBase = (baseLine: string, candidate: Candidate) => {
250
+
currentLine = getLineWithCompletion(baseLine, candidate);
251
+
const before = baseLine.slice(0, candidate.span_start);
252
+
cursorPos = (before + candidate.name).length;
253
+
};
254
+
246
255
const updateCompletionCandidates = async () => {
247
256
// Don't try to get completion if worker isn't loaded
248
257
if (!isWorkerReady) return;
···
290
299
completionCandidates = matches.map((cmd) => ({
291
300
name: cmd,
292
301
description: "",
293
-
is_command: true,
294
302
rendered: cmd,
295
303
span_start: 0,
296
304
span_end: currentLine.length,
···
366
374
if (completionCandidates.length > 0) {
367
375
const cand = completionCandidates[completionIndex];
368
376
if (cand) {
369
-
currentLine = getLineWithCompletion(completionBaseLine, cand);
370
-
cursorPos = currentLine.length;
377
+
applyCompletionFromBase(completionBaseLine, cand);
371
378
}
372
379
}
373
380
···
397
404
"\x1b[33mengine is still loading, please wait (_ _ )zZ...\x1b[0m\r\n",
398
405
);
399
406
} else {
400
-
// ASYNC execution
401
-
const output: string | undefined = await callWorker(
402
-
"run",
403
-
trimmed,
404
-
);
405
-
if (output) {
406
-
term.write(output.replace(/\n/g, "\r\n"));
407
-
if (output && !output.endsWith("\n")) {
408
-
term.write("\r\n");
407
+
try {
408
+
const output: string | undefined = await callWorker(
409
+
"run",
410
+
trimmed,
411
+
);
412
+
if (output) {
413
+
term.write(output.replace(/\n/g, "\r\n"));
414
+
if (output && !output.endsWith("\n")) {
415
+
term.write("\r\n");
416
+
}
409
417
}
418
+
419
+
// update history
420
+
const idx = history.indexOf(trimmed);
421
+
if (idx >= 0) history.splice(idx, 1);
422
+
history.push(trimmed);
423
+
historyIndex = history.length;
424
+
} catch (error) {
425
+
term.write(`${error}`.replace(/\n/g, "\r\n"));
410
426
}
411
427
412
-
// Update cached PWD after command execution (cd, etc)
428
+
// update pwd
413
429
cachedPwd = await callWorker("get_pwd");
414
-
415
-
// Update History
416
-
const idx = history.indexOf(trimmed);
417
-
if (idx >= 0) history.splice(idx, 1);
418
-
history.push(trimmed);
419
-
historyIndex = history.length;
420
430
}
421
431
} catch (err) {
422
-
term.write(`\x1b[31mfatal: ${err}\x1b[0m\r\n`);
432
+
term.write(
433
+
`\x1b[31mfatal: ${err}\x1b[0m\r\n`.replace(/\n/g, "\r\n"),
434
+
);
423
435
} finally {
424
436
isRunningCommand = false;
425
437
}
···
448
460
break;
449
461
450
462
case "\u0003": // Ctrl+C
463
+
Atomics.store(interruptBuffer, 0, 1);
451
464
currentLine = "";
452
465
cursorPos = 0;
453
466
completionCandidates = [];
···
511
524
(completionIndex - 1 + completionCandidates.length) %
512
525
completionCandidates.length;
513
526
const candidate = completionCandidates[completionIndex];
514
-
currentLine = getLineWithCompletion(completionBaseLine, candidate);
515
-
cursorPos = currentLine.length;
527
+
applyCompletionFromBase(completionBaseLine, candidate);
516
528
await refreshPrompt();
517
529
break;
518
530
}
···
542
554
if (completionCandidates.length > 0 && e === "\x1b[B") {
543
555
completionIndex = (completionIndex + 1) % completionCandidates.length;
544
556
const candidate = completionCandidates[completionIndex];
545
-
currentLine = getLineWithCompletion(completionBaseLine, candidate);
546
-
cursorPos = currentLine.length;
557
+
applyCompletionFromBase(completionBaseLine, candidate);
547
558
await refreshPrompt();
548
559
break;
549
560
}
···
578
589
(completionIndex + 1) % completionCandidates.length;
579
590
580
591
const candidate = completionCandidates[completionIndex];
581
-
currentLine = getLineWithCompletion(completionBaseLine, candidate);
592
+
applyCompletionFromBase(completionBaseLine, candidate);
582
593
await refreshPrompt();
583
594
} else {
584
595
// Guard: ensure worker is ready
···
613
624
(completionIndex - 1 + completionCandidates.length) %
614
625
completionCandidates.length;
615
626
const candidate = completionCandidates[completionIndex];
616
-
currentLine = getLineWithCompletion(completionBaseLine, candidate);
617
-
cursorPos = currentLine.length;
627
+
applyCompletionFromBase(completionBaseLine, candidate);
618
628
await refreshPrompt();
619
629
}
620
630
break;
···
641
651
642
652
await readyPromise;
643
653
644
-
await callWorker("run", "open welcome.txt");
654
+
await callWorker("set-interrupt-buffer", interruptBuffer);
645
655
646
656
term.write(getPrompt());
647
657
+15
-8
www/src/worker.ts
+15
-8
www/src/worker.ts
···
6
6
get_pwd_string,
7
7
register_console_callback,
8
8
register_task_count_callback,
9
+
set_interrupt_buffer,
9
10
} from "../../pkg";
10
11
11
12
// Initialize WASM
12
13
await init();
13
-
init_engine();
14
14
15
-
// Setup Callbacks to proxy messages back to Main Thread
16
15
register_console_callback((msg: string, isCmd: boolean) => {
17
16
self.postMessage({ type: "console", payload: { msg, isCmd } });
18
17
});
···
21
20
self.postMessage({ type: "task_count", payload: count });
22
21
});
23
22
24
-
// Handle messages from Main Thread
25
-
self.onmessage = (e) => {
23
+
try {
24
+
await init_engine();
25
+
} catch (error) {
26
+
console.error(error);
27
+
}
28
+
29
+
self.onmessage = async (e) => {
26
30
const { id, type, payload } = e.data;
27
31
28
32
// console.log("Received message:", id, type, payload);
···
30
34
try {
31
35
let result;
32
36
switch (type) {
37
+
case "set-interrupt-buffer":
38
+
set_interrupt_buffer(payload);
39
+
break;
33
40
case "run":
34
-
result = run_command(payload);
41
+
result = await run_command(payload);
35
42
break;
36
43
case "completion":
37
-
result = completion(payload.line, payload.cursor);
44
+
result = await completion(payload.line, payload.cursor);
38
45
break;
39
46
case "highlight":
40
-
result = highlight(payload);
47
+
result = await highlight(payload);
41
48
break;
42
49
case "get_pwd":
43
50
result = get_pwd_string();
44
51
break;
45
52
default:
46
-
throw new Error(`Unknown message type: ${type}`);
53
+
throw new Error(`unknown message type: ${type}`);
47
54
}
48
55
self.postMessage({ id, type: `${type}_result`, payload: result });
49
56
} catch (err) {