mirror of
https://github.com/zyedidia/micro.git
synced 2026-03-29 22:27:13 +09:00
Compare commits
1724 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f59468642d | ||
|
|
85e85b7ccc | ||
|
|
8f5888e7bf | ||
|
|
f0da73bae2 | ||
|
|
d92deacf99 | ||
|
|
ffd7b5c770 | ||
|
|
bd8c0d25c8 | ||
|
|
052a36b896 | ||
|
|
a76bf02f5f | ||
|
|
92f4cb7ef7 | ||
|
|
1cf9537340 | ||
|
|
60c8c81da3 | ||
|
|
c76a973877 | ||
|
|
6def99ce24 | ||
|
|
26930ca81f | ||
|
|
cd379cd838 | ||
|
|
ee157f6503 | ||
|
|
48ca19873f | ||
|
|
fee5528309 | ||
|
|
671a188802 | ||
|
|
18d540583b | ||
|
|
943ea15fa3 | ||
|
|
2ef57977d7 | ||
|
|
527750b68d | ||
|
|
629efe5eb7 | ||
|
|
a19cd2e6d0 | ||
|
|
9e8d76f2fa | ||
|
|
8a9a14562f | ||
|
|
a6f5dee45c | ||
|
|
b12886b066 | ||
|
|
56f5b475eb | ||
|
|
c51f84955e | ||
|
|
e4bf1e9984 | ||
|
|
917e2922a1 | ||
|
|
59e8f3aa3e | ||
|
|
53bda0cfa7 | ||
|
|
f059541e0d | ||
|
|
d78fe81e21 | ||
|
|
25b9342fbe | ||
|
|
70bcf9f618 | ||
|
|
8848388411 | ||
|
|
dff8b33e9c | ||
|
|
8a2048e7f6 | ||
|
|
0174d7dba4 | ||
|
|
e1827480c9 | ||
|
|
d8584d1ddb | ||
|
|
f0cdc3cabb | ||
|
|
2ef4f83358 | ||
|
|
a9120ce270 | ||
|
|
190d9d0609 | ||
|
|
cf3fdb344a | ||
|
|
b91242124c | ||
|
|
5ffc19f159 | ||
|
|
cc994b6241 | ||
|
|
087e7207f7 | ||
|
|
db32b84cd1 | ||
|
|
00006aa2b4 | ||
|
|
600d8558b2 | ||
|
|
4874823240 | ||
|
|
6a0e4b5564 | ||
|
|
b2d7c8c5d4 | ||
|
|
38f88ade60 | ||
|
|
8348cc8ec2 | ||
|
|
743d42e417 | ||
|
|
faa207907c | ||
|
|
9d1c489574 | ||
|
|
30ed25859a | ||
|
|
d2288c5f66 | ||
|
|
a07ee26b05 | ||
|
|
a7ce85d6f6 | ||
|
|
7c71995aaf | ||
|
|
357c4b0fcd | ||
|
|
0d4f85304b | ||
|
|
e18e41eb45 | ||
|
|
5519f053ac | ||
|
|
2f4b3b2a8c | ||
|
|
ea290e4fb5 | ||
|
|
8b414e6187 | ||
|
|
e7ef81ed97 | ||
|
|
12c286f9b1 | ||
|
|
7b5bc8fe37 | ||
|
|
bad78797bb | ||
|
|
bf1258578c | ||
|
|
6588f02f7b | ||
|
|
5e9c6375d0 | ||
|
|
7d47659481 | ||
|
|
dcd4bae96f | ||
|
|
4f628bf30b | ||
|
|
36e83a46e4 | ||
|
|
1a64ffb88b | ||
|
|
7c77927913 | ||
|
|
8224037080 | ||
|
|
d7e3fc99f1 | ||
|
|
feaf3951d2 | ||
|
|
399c629076 | ||
|
|
47ed7447f1 | ||
|
|
62a98ea3c5 | ||
|
|
0a9194b883 | ||
|
|
a938917b9b | ||
|
|
695d4c2b1b | ||
|
|
34724b941a | ||
|
|
8176e8c6f8 | ||
|
|
0d5b1cd64d | ||
|
|
9fd5b133ea | ||
|
|
432b57a070 | ||
|
|
ee55732be3 | ||
|
|
90304fb472 | ||
|
|
f6aec1af5f | ||
|
|
b77b61d677 | ||
|
|
05a0598f16 | ||
|
|
daa6f122a7 | ||
|
|
71f5f043fb | ||
|
|
f3eaf99665 | ||
|
|
c88c1b84da | ||
|
|
e1e310a96e | ||
|
|
d29b941be0 | ||
|
|
cde696915d | ||
|
|
185b8de17b | ||
|
|
d65285ee54 | ||
|
|
848bd1ba8c | ||
|
|
00205aa6a7 | ||
|
|
ecb9fd5a8a | ||
|
|
bdf9e6d3a4 | ||
|
|
3ed77dbb2e | ||
|
|
57a992c4a3 | ||
|
|
c63614213d | ||
|
|
b7a54fa74a | ||
|
|
63046ae909 | ||
|
|
af48e4b79b | ||
|
|
4e73d0779b | ||
|
|
6f424f3213 | ||
|
|
e110e93e0f | ||
|
|
8ddf335e68 | ||
|
|
ca9d102267 | ||
|
|
c3d120ccdf | ||
|
|
a4a6445e1d | ||
|
|
13e30a63eb | ||
|
|
e561952781 | ||
|
|
9fe58bf84f | ||
|
|
e84f85340b | ||
|
|
aaf90c6f52 | ||
|
|
0962e1bfba | ||
|
|
2f45644d14 | ||
|
|
c2a2316c28 | ||
|
|
6957e83cdb | ||
|
|
ce91e41e5a | ||
|
|
6d99d34eb0 | ||
|
|
b77980082c | ||
|
|
2fd59adffa | ||
|
|
57c34e2248 | ||
|
|
6514b77e0d | ||
|
|
8a907956d1 | ||
|
|
de33eac058 | ||
|
|
c4bfa825a1 | ||
|
|
b0c50d371f | ||
|
|
b4d4119572 | ||
|
|
674258787e | ||
|
|
2354412922 | ||
|
|
3ac30343b8 | ||
|
|
37343350ca | ||
|
|
d9e9d4403f | ||
|
|
741f494841 | ||
|
|
69b6c724fc | ||
|
|
31936358c1 | ||
|
|
fe58ff5753 | ||
|
|
9aaafe5dcf | ||
|
|
c2bd5e4eec | ||
|
|
98ddb62af4 | ||
|
|
24a684cff2 | ||
|
|
b4e7e981f3 | ||
|
|
e73549c61a | ||
|
|
e759d4087b | ||
|
|
106ba48079 | ||
|
|
ef768e36f3 | ||
|
|
f5e1f93ee5 | ||
|
|
a52c0c2907 | ||
|
|
be7d27bc49 | ||
|
|
f6a9c482a6 | ||
|
|
6e3f38b271 | ||
|
|
8483f1da1e | ||
|
|
28ed47e358 | ||
|
|
6a1b8f4a4f | ||
|
|
dba8ef2fdd | ||
|
|
b0624cb66e | ||
|
|
09ea82c97e | ||
|
|
d94b81b8e6 | ||
|
|
bcb1947a0a | ||
|
|
b0b5d7b392 | ||
|
|
2598d8ad70 | ||
|
|
f731e422ea | ||
|
|
abcdeb01e9 | ||
|
|
d326a9cddd | ||
|
|
e3131a0779 | ||
|
|
46c5a81b0d | ||
|
|
59146cabb1 | ||
|
|
35e3bddea0 | ||
|
|
016b8dcc4c | ||
|
|
03228762d4 | ||
|
|
953f5a0eff | ||
|
|
477bdb3dc8 | ||
|
|
d74f40882d | ||
|
|
d965e8de4f | ||
|
|
866b3c9238 | ||
|
|
3252324d24 | ||
|
|
8e7a016917 | ||
|
|
cf41a587a3 | ||
|
|
1dc1c65565 | ||
|
|
97ee344268 | ||
|
|
b658f94e5a | ||
|
|
0abe427026 | ||
|
|
063389afdf | ||
|
|
1e998ab0e4 | ||
|
|
b3e40a2644 | ||
|
|
fa4103f7aa | ||
|
|
17f0eb80cd | ||
|
|
35dfb1830b | ||
|
|
76f09adb48 | ||
|
|
289c7147e4 | ||
|
|
be34e1241c | ||
|
|
83ea8d8be9 | ||
|
|
7c90f4d6f1 | ||
|
|
61a90f7666 | ||
|
|
8d373cde6e | ||
|
|
6a465500bc | ||
|
|
f3e8413e77 | ||
|
|
f2a1e2337f | ||
|
|
c7f36f9480 | ||
|
|
955bde4abc | ||
|
|
afb03aa37f | ||
|
|
6c3814dfac | ||
|
|
d234e9ec41 | ||
|
|
c2c0325384 | ||
|
|
dfb6bc0312 | ||
|
|
0c6a7e2837 | ||
|
|
ddc8bf455e | ||
|
|
2855ae204c | ||
|
|
0bf54ff0e7 | ||
|
|
50ff45c213 | ||
|
|
eb2b546600 | ||
|
|
dc4da37908 | ||
|
|
9333354fc8 | ||
|
|
b557ed2221 | ||
|
|
021f8da6f1 | ||
|
|
6d0128059b | ||
|
|
d6dd838abd | ||
|
|
938fb7983a | ||
|
|
d9e262c394 | ||
|
|
e98be1a1e5 | ||
|
|
41fe7d090e | ||
|
|
aadf5b40ec | ||
|
|
ebf6d69f26 | ||
|
|
ebf616399e | ||
|
|
08708f79bf | ||
|
|
d7b39fe7a5 | ||
|
|
48ace1c530 | ||
|
|
fde1cc563f | ||
|
|
abf07a8357 | ||
|
|
a2f7080602 | ||
|
|
a2916c0e32 | ||
|
|
0301e3539e | ||
|
|
6632ab0a77 | ||
|
|
466c48da31 | ||
|
|
2c72a3755c | ||
|
|
92054aa649 | ||
|
|
4b5be43e60 | ||
|
|
d18864e607 | ||
|
|
4010a16784 | ||
|
|
01c6ea26b8 | ||
|
|
d83b912b3b | ||
|
|
604d78de0f | ||
|
|
9e8420aab5 | ||
|
|
dc3d6f5bc3 | ||
|
|
93431a9ddf | ||
|
|
ec6be38391 | ||
|
|
3777bcf295 | ||
|
|
3d09dfe574 | ||
|
|
532f932712 | ||
|
|
eeeb927a1d | ||
|
|
f9ce549663 | ||
|
|
5a715e7a0b | ||
|
|
e420872a27 | ||
|
|
cef32d4ac7 | ||
|
|
35375a6ea2 | ||
|
|
60eec0eccd | ||
|
|
97665573c7 | ||
|
|
f874377573 | ||
|
|
ce868faece | ||
|
|
e1c1e402a9 | ||
|
|
3b66a3364c | ||
|
|
9b03a3dc6d | ||
|
|
ff24ad5fa8 | ||
|
|
5180634947 | ||
|
|
da643a0c1f | ||
|
|
cd6765673f | ||
|
|
1b73abcfd0 | ||
|
|
f3778baaf4 | ||
|
|
cf1f9fa007 | ||
|
|
34619e111f | ||
|
|
29a5cef559 | ||
|
|
bd83c6a8a9 | ||
|
|
4b0348f64a | ||
|
|
5b52b8a60f | ||
|
|
c0e6dad88b | ||
|
|
a61616d79e | ||
|
|
8663014bbe | ||
|
|
351c7b099a | ||
|
|
c2e7fd34a7 | ||
|
|
a3e61a6e71 | ||
|
|
bd0c172667 | ||
|
|
7746039724 | ||
|
|
629f20720a | ||
|
|
2a3d7720f3 | ||
|
|
b47cf33c3b | ||
|
|
5fba432d78 | ||
|
|
b1efabaaed | ||
|
|
185d54d664 | ||
|
|
96322a6df9 | ||
|
|
42af0816d5 | ||
|
|
8368989341 | ||
|
|
9e60d468ca | ||
|
|
da32457037 | ||
|
|
2d2dbfebff | ||
|
|
6681387b47 | ||
|
|
6a4a915188 | ||
|
|
6ba66320f0 | ||
|
|
1367084a18 | ||
|
|
ec2976b069 | ||
|
|
bad8ed7473 | ||
|
|
e912b7de12 | ||
|
|
8570ff9a8c | ||
|
|
7f7ad29671 | ||
|
|
a95dab078e | ||
|
|
f8218e0648 | ||
|
|
e66d01e989 | ||
|
|
05331e1fa2 | ||
|
|
c5d3008d85 | ||
|
|
922a57b3e7 | ||
|
|
ff6f28e366 | ||
|
|
4951f155ea | ||
|
|
94ff79e7b2 | ||
|
|
3b306c1d3b | ||
|
|
432f1f3363 | ||
|
|
93734f5668 | ||
|
|
b527e4fe42 | ||
|
|
3f22501b1a | ||
|
|
fc706bc404 | ||
|
|
c4d5d7c195 | ||
|
|
a9bb1f35da | ||
|
|
04e5acb1f8 | ||
|
|
e42cf3663b | ||
|
|
a86a6c464e | ||
|
|
88b8fc713d | ||
|
|
9127152d93 | ||
|
|
dde52132cf | ||
|
|
ba594abfad | ||
|
|
5075c91fd4 | ||
|
|
5e28ed4271 | ||
|
|
d29994ada9 | ||
|
|
7f32d31108 | ||
|
|
aa66435353 | ||
|
|
e79869978b | ||
|
|
b41fc10b8f | ||
|
|
5dfaaf8856 | ||
|
|
4dccfc095d | ||
|
|
bc3f845c0d | ||
|
|
6f6b263d10 | ||
|
|
b68461cf72 | ||
|
|
199d65017f | ||
|
|
d2f8adb8ff | ||
|
|
5b18edf865 | ||
|
|
ac3a5154c0 | ||
|
|
adaddba696 | ||
|
|
26c545267d | ||
|
|
3d40e91690 | ||
|
|
7217911c3a | ||
|
|
24eb6fee25 | ||
|
|
65cd6c4605 | ||
|
|
d1e713ce08 | ||
|
|
f39a916e5f | ||
|
|
c0293b5d0e | ||
|
|
bc6dd990e5 | ||
|
|
ccb5904591 | ||
|
|
9eed8bc247 | ||
|
|
763e635fea | ||
|
|
e18f6f832f | ||
|
|
fc4811c1ab | ||
|
|
be136a4648 | ||
|
|
4027081e0e | ||
|
|
e7e0272968 | ||
|
|
e3ae38e54a | ||
|
|
a47e1f0ca5 | ||
|
|
576036f251 | ||
|
|
23a76e1381 | ||
|
|
55e33badd0 | ||
|
|
5bd54747b3 | ||
|
|
bf15f5c585 | ||
|
|
809b95d290 | ||
|
|
8d85cae4c0 | ||
|
|
a5cf06026a | ||
|
|
7cd5024e34 | ||
|
|
0f4f60c018 | ||
|
|
aa305c2676 | ||
|
|
aa774164a7 | ||
|
|
47a129b70f | ||
|
|
c93d7a1b35 | ||
|
|
995e1dc704 | ||
|
|
adfeaf52ba | ||
|
|
f5f4154d4c | ||
|
|
74ee256260 | ||
|
|
d45f8b4d23 | ||
|
|
3335f377a9 | ||
|
|
5ab6c9795f | ||
|
|
15dff722b0 | ||
|
|
a2b9acd153 | ||
|
|
4497daaef1 | ||
|
|
cf2d5dbfe2 | ||
|
|
739dd28652 | ||
|
|
39446df749 | ||
|
|
7cd83b4361 | ||
|
|
0612af1590 | ||
|
|
c7f2c9c704 | ||
|
|
f4a3465a08 | ||
|
|
453e96358a | ||
|
|
b97ded9058 | ||
|
|
253790de99 | ||
|
|
ef18fc572c | ||
|
|
0e4faf108d | ||
|
|
ad487807a5 | ||
|
|
ad50d7aa56 | ||
|
|
ef3f081347 | ||
|
|
bc1d6b6f94 | ||
|
|
fc7058d47c | ||
|
|
ab37e6ad6c | ||
|
|
4bdf788091 | ||
|
|
8c687e8279 | ||
|
|
9336e09532 | ||
|
|
069f7d20bc | ||
|
|
212b0f8c71 | ||
|
|
254b892a3b | ||
|
|
1a710272f8 | ||
|
|
a3885bfb12 | ||
|
|
df968db5a3 | ||
|
|
538f0117bc | ||
|
|
4a5b759f16 | ||
|
|
3380170af8 | ||
|
|
467d384789 | ||
|
|
1563ab93dd | ||
|
|
812c7761dc | ||
|
|
055fff2b08 | ||
|
|
5671e039b9 | ||
|
|
224cbe5093 | ||
|
|
eb49052a48 | ||
|
|
5825353f64 | ||
|
|
8fa34f23d8 | ||
|
|
a5e7122b30 | ||
|
|
6c1db53b65 | ||
|
|
b9f7939018 | ||
|
|
5701ed211a | ||
|
|
8858c03b3b | ||
|
|
2f7858ce25 | ||
|
|
94ab77e2e0 | ||
|
|
fb3923f344 | ||
|
|
354c9efc8f | ||
|
|
149b3ae89f | ||
|
|
0f1483dc8c | ||
|
|
4146730aaf | ||
|
|
c479c9d91a | ||
|
|
0febfd2c80 | ||
|
|
eec4e535b4 | ||
|
|
8aa05cf409 | ||
|
|
fe773c00d2 | ||
|
|
f2cb7d2fc1 | ||
|
|
4412b44b47 | ||
|
|
9cf283e312 | ||
|
|
305f4debff | ||
|
|
93aed1ab9f | ||
|
|
778bfd5cd3 | ||
|
|
16e5f55323 | ||
|
|
1ac4a8e7d3 | ||
|
|
541daf212e | ||
|
|
d4c410f3dc | ||
|
|
4b50599411 | ||
|
|
6cf09f9843 | ||
|
|
37a4cbfd98 | ||
|
|
0f37c0b0bf | ||
|
|
80fe992957 | ||
|
|
e97005f05d | ||
|
|
5335c60d6c | ||
|
|
b8b245f305 | ||
|
|
3d2cc3298e | ||
|
|
a89ddea619 | ||
|
|
6562e3b48d | ||
|
|
c01995c1b6 | ||
|
|
78ce7a5f0f | ||
|
|
afe24698ea | ||
|
|
c50e0cb932 | ||
|
|
e9a4238a3f | ||
|
|
02b71a514a | ||
|
|
9f066f2fbf | ||
|
|
12d727fb93 | ||
|
|
31cf5a15ce | ||
|
|
31fb3f2df2 | ||
|
|
7d87e6db99 | ||
|
|
06d596e780 | ||
|
|
d7b3f961b4 | ||
|
|
c3e2085e3c | ||
|
|
dd619b3ff5 | ||
|
|
dc68183fc1 | ||
|
|
d9735e5c3b | ||
|
|
bd9307483d | ||
|
|
fd8fc3acfa | ||
|
|
f932cfb7f1 | ||
|
|
8817d711b9 | ||
|
|
1956a49f9b | ||
|
|
5d81fc7815 | ||
|
|
0827968f6b | ||
|
|
5b869cb836 | ||
|
|
2d475fbca8 | ||
|
|
36862137db | ||
|
|
34c12e1282 | ||
|
|
8b0c858f28 | ||
|
|
daa0d67c6f | ||
|
|
aaac5466d1 | ||
|
|
89ac5d7de2 | ||
|
|
166e227c9f | ||
|
|
7d1dc1183c | ||
|
|
4662f0c500 | ||
|
|
78fd9fb225 | ||
|
|
be0dcd5d10 | ||
|
|
1857aa4067 | ||
|
|
7a51490591 | ||
|
|
1fc5b316ab | ||
|
|
9e0d3c7cbe | ||
|
|
f12061ea88 | ||
|
|
e87917f1e1 | ||
|
|
aae0f4630e | ||
|
|
523592be26 | ||
|
|
e9337da43f | ||
|
|
3a8898dadd | ||
|
|
1c4e2eb09f | ||
|
|
c11af1c19c | ||
|
|
4f35eed615 | ||
|
|
3a4bdb0db6 | ||
|
|
73093f9497 | ||
|
|
3644ef4a5a | ||
|
|
8b4943fc26 | ||
|
|
a90b17c855 | ||
|
|
9a8b7ab757 | ||
|
|
9ec07b595b | ||
|
|
9bfc35656c | ||
|
|
b2b933c6c1 | ||
|
|
beea8d42d5 | ||
|
|
612ebb2e17 | ||
|
|
957b3aaea7 | ||
|
|
a189d08c30 | ||
|
|
26172b5101 | ||
|
|
51691ed7bf | ||
|
|
5acbccf0b2 | ||
|
|
e33489c04f | ||
|
|
bcb8765049 | ||
|
|
7e34eabb0e | ||
|
|
87661ef308 | ||
|
|
58e11c0b2d | ||
|
|
1739e0c09c | ||
|
|
c46695bb57 | ||
|
|
457a4f8f98 | ||
|
|
e3fd914e0b | ||
|
|
2c219ba647 | ||
|
|
ca9b1d7b14 | ||
|
|
6aa5aa540b | ||
|
|
001498eee4 | ||
|
|
3c87d1cfb4 | ||
|
|
54183ec4d2 | ||
|
|
6f3548e7ce | ||
|
|
2a0d78b86d | ||
|
|
ba98e973c4 | ||
|
|
3515f254c4 | ||
|
|
2823058806 | ||
|
|
5c2fc92332 | ||
|
|
64a6779482 | ||
|
|
49b6cf3673 | ||
|
|
37bd454679 | ||
|
|
f9e8d8b9a0 | ||
|
|
e289d44034 | ||
|
|
81bad4d089 | ||
|
|
9f4da789db | ||
|
|
2fd85cb033 | ||
|
|
8bda0a6b45 | ||
|
|
fd48a3841e | ||
|
|
a69cc72c9d | ||
|
|
57c681eddf | ||
|
|
ec6943b1c9 | ||
|
|
e071a4f8e2 | ||
|
|
04420de96a | ||
|
|
bfc9d4a195 | ||
|
|
e3955882e4 | ||
|
|
e0ce419357 | ||
|
|
2d0ec82baa | ||
|
|
a0a154d957 | ||
|
|
fa05d63d11 | ||
|
|
249405355a | ||
|
|
dab18e2fee | ||
|
|
de35d00ba7 | ||
|
|
f68149489e | ||
|
|
1013b03314 | ||
|
|
96284a1feb | ||
|
|
d2b51a59d6 | ||
|
|
0e56c0c816 | ||
|
|
f40abc1a59 | ||
|
|
0a6948c8ac | ||
|
|
9db7991a1d | ||
|
|
7339afcf73 | ||
|
|
9cbe2c62de | ||
|
|
6e9b8c1bd5 | ||
|
|
e11d9deb6e | ||
|
|
1d93433bfb | ||
|
|
45643f397b | ||
|
|
33d9b8f60b | ||
|
|
6140dabca8 | ||
|
|
27db63433f | ||
|
|
bcdab882bc | ||
|
|
32b8c51992 | ||
|
|
4be3e9122c | ||
|
|
d0f8bede41 | ||
|
|
905e984f29 | ||
|
|
44e417c2f4 | ||
|
|
e03fab8daa | ||
|
|
1ab493de59 | ||
|
|
f56621a4bd | ||
|
|
497ca2c66b | ||
|
|
18ca06d9be | ||
|
|
a732d03b4d | ||
|
|
1856891622 | ||
|
|
8a250f7d95 | ||
|
|
7a013f666e | ||
|
|
41a24e61d6 | ||
|
|
d953339a56 | ||
|
|
76e1d7a3a7 | ||
|
|
91b65001c9 | ||
|
|
aa74b1233c | ||
|
|
61baa73d70 | ||
|
|
efe343b37c | ||
|
|
cc8e9a7e06 | ||
|
|
d7f7d845b9 | ||
|
|
8f0418c9a8 | ||
|
|
71af765b4e | ||
|
|
c0f279ffe8 | ||
|
|
ae9bb763fb | ||
|
|
3c01947cb3 | ||
|
|
53e142fb88 | ||
|
|
2e64499f96 | ||
|
|
11cb702d7f | ||
|
|
7a2820cbc0 | ||
|
|
b181342ff1 | ||
|
|
f0e2f3cc96 | ||
|
|
6ef273accd | ||
|
|
0eadf283a5 | ||
|
|
f8a171379a | ||
|
|
1a62ede320 | ||
|
|
1bb1da4765 | ||
|
|
987d48038a | ||
|
|
abc04ec521 | ||
|
|
b7706d775c | ||
|
|
ac0b89366b | ||
|
|
3293160dcb | ||
|
|
804943a1e8 | ||
|
|
89f50638d7 | ||
|
|
c606c51c8b | ||
|
|
4bde88d126 | ||
|
|
41bae11c1e | ||
|
|
f43a1b5ced | ||
|
|
219f934656 | ||
|
|
26da85dcb1 | ||
|
|
2885b42c62 | ||
|
|
b12eca0a98 | ||
|
|
3e612d2597 | ||
|
|
ade5efef5d | ||
|
|
cb45481526 | ||
|
|
88d8b0b181 | ||
|
|
ea6a87d41a | ||
|
|
1c2fd30cab | ||
|
|
69ed07cc62 | ||
|
|
6d2cbb6cce | ||
|
|
397c29443a | ||
|
|
5b26702d5e | ||
|
|
b9e77eee6a | ||
|
|
8e5fd674cc | ||
|
|
5038167650 | ||
|
|
6787db9eb3 | ||
|
|
75b9c8c1ec | ||
|
|
28267b9eb2 | ||
|
|
cad43914b0 | ||
|
|
a37c30b889 | ||
|
|
f17b42bcd2 | ||
|
|
7bfc90d080 | ||
|
|
1d24609ed1 | ||
|
|
aa81cf5cf6 | ||
|
|
4790c39dfc | ||
|
|
35a9245c5d | ||
|
|
3e3cdfc5b5 | ||
|
|
f0e453b4f9 | ||
|
|
3325b98063 | ||
|
|
4632c3594f | ||
|
|
96c7b1d07b | ||
|
|
f48116801b | ||
|
|
aaf098bb47 | ||
|
|
6d4134a178 | ||
|
|
015fcf5fec | ||
|
|
fddf1690e3 | ||
|
|
0913a1aeb3 | ||
|
|
a19a6d28a7 | ||
|
|
af520cf047 | ||
|
|
db75e11e32 | ||
|
|
797e5cc27f | ||
|
|
36dc6647dd | ||
|
|
44b64f7129 | ||
|
|
0a49ea0a0d | ||
|
|
4f41881c10 | ||
|
|
63299df4b9 | ||
|
|
10b8fb7b26 | ||
|
|
0a7e4c8f06 | ||
|
|
83190a578e | ||
|
|
79349562b2 | ||
|
|
0cb1ad09cd | ||
|
|
6ef00c4c3b | ||
|
|
bb598ae566 | ||
|
|
13c63a9951 | ||
|
|
cf06d06fb3 | ||
|
|
808e3a7c9f | ||
|
|
16e9068cb9 | ||
|
|
3924e363d1 | ||
|
|
a274daeaaf | ||
|
|
e26417fd14 | ||
|
|
d7ba2f600e | ||
|
|
1cf4baa743 | ||
|
|
7e3aa337f6 | ||
|
|
3f01101da4 | ||
|
|
9a6054fc43 | ||
|
|
b2a0745747 | ||
|
|
7911ce1f16 | ||
|
|
8bff7f00d0 | ||
|
|
957273fc92 | ||
|
|
805d6ccaf7 | ||
|
|
fc2566a0de | ||
|
|
86c08bd747 | ||
|
|
0b47502e62 | ||
|
|
2afbcef825 | ||
|
|
0a500be3ba | ||
|
|
3b36316b00 | ||
|
|
d668050ebe | ||
|
|
dd47f167f1 | ||
|
|
2ebeb9d5a5 | ||
|
|
8629357c70 | ||
|
|
c8ff764467 | ||
|
|
8e741599dc | ||
|
|
770cb87f7a | ||
|
|
d82867ee53 | ||
|
|
275bce7d69 | ||
|
|
9094c174cc | ||
|
|
a814677b51 | ||
|
|
8b60e4f3b1 | ||
|
|
c32f5a4859 | ||
|
|
df44f538fd | ||
|
|
a4ae7a1e11 | ||
|
|
70616b335e | ||
|
|
f6e9a16724 | ||
|
|
ac41e186a0 | ||
|
|
a90cb64265 | ||
|
|
5124dd04b3 | ||
|
|
7867d50d67 | ||
|
|
0ba60728e8 | ||
|
|
981263eb81 | ||
|
|
79deabbbd6 | ||
|
|
ba4b028076 | ||
|
|
649e5799c2 | ||
|
|
7339a88d68 | ||
|
|
b0cfb2e691 | ||
|
|
4e0d402cea | ||
|
|
f882248f41 | ||
|
|
f58c5412a8 | ||
|
|
b0e4043513 | ||
|
|
47dd65d4e5 | ||
|
|
fa84f6ddc3 | ||
|
|
2bf40f096e | ||
|
|
4802403308 | ||
|
|
e443adef31 | ||
|
|
cdb057dfc3 | ||
|
|
9da1ef178e | ||
|
|
bf33ab532c | ||
|
|
46c7437270 | ||
|
|
09cab07352 | ||
|
|
b7214da4ea | ||
|
|
5138ae2436 | ||
|
|
98778a80c2 | ||
|
|
e0a8e90ad9 | ||
|
|
2ae9f88eaa | ||
|
|
ee8e022ccf | ||
|
|
3ca55f77a6 | ||
|
|
5f304db4a1 | ||
|
|
93b8f10b02 | ||
|
|
bdb699211a | ||
|
|
acd42df13c | ||
|
|
5fc8f847a6 | ||
|
|
af6ef4f87f | ||
|
|
7f287b62fb | ||
|
|
36d72c4cab | ||
|
|
71ee185b80 | ||
|
|
0360a2fcb5 | ||
|
|
2ee7adb196 | ||
|
|
d247db3e9d | ||
|
|
e4c2f5d259 | ||
|
|
cc15df9307 | ||
|
|
812b547679 | ||
|
|
1c43bb572a | ||
|
|
f96e9e9c1d | ||
|
|
7dfeda1ae5 | ||
|
|
d6ccaf0e41 | ||
|
|
6b6fcc8ba0 | ||
|
|
07bfcc9747 | ||
|
|
423f4675d2 | ||
|
|
c01ba97215 | ||
|
|
288717451f | ||
|
|
a1f3499825 | ||
|
|
63fa8fec41 | ||
|
|
b9e916999f | ||
|
|
afedad9977 | ||
|
|
d82ea2279d | ||
|
|
5b5998cf14 | ||
|
|
7b6430af1c | ||
|
|
a0d475bebf | ||
|
|
31cd4b5795 | ||
|
|
19ee4b281e | ||
|
|
a171795654 | ||
|
|
98d8bfa879 | ||
|
|
7bc2d870cd | ||
|
|
678819683a | ||
|
|
08e46f9112 | ||
|
|
e071209add | ||
|
|
74e79dc8f2 | ||
|
|
955e8ffb08 | ||
|
|
b87a74711e | ||
|
|
ade0e9dd39 | ||
|
|
f05f0b06ac | ||
|
|
f2006f592a | ||
|
|
5e66489836 | ||
|
|
9daa05d696 | ||
|
|
d76704839a | ||
|
|
329669ce79 | ||
|
|
4365b66398 | ||
|
|
5af5140362 | ||
|
|
bf6ce3a17e | ||
|
|
e99fd1337e | ||
|
|
17dac164ea | ||
|
|
b7c99c52d2 | ||
|
|
278aa6b050 | ||
|
|
773c54a40d | ||
|
|
74589af1fc | ||
|
|
f01ad3f726 | ||
|
|
a0f3ec805d | ||
|
|
ea6012922f | ||
|
|
da33b59858 | ||
|
|
9703d4f52f | ||
|
|
f3a30412f4 | ||
|
|
3116b082d8 | ||
|
|
3e0a1b4517 | ||
|
|
ac3de065d9 | ||
|
|
3e63ec74b9 | ||
|
|
c7334eb3b7 | ||
|
|
dfbddd4b86 | ||
|
|
299416062f | ||
|
|
8b8fffb98d | ||
|
|
ec221c0bc4 | ||
|
|
d27f8f9802 | ||
|
|
c40c79427a | ||
|
|
8a4f2193d8 | ||
|
|
aa667f6ba9 | ||
|
|
d067de8150 | ||
|
|
b3559df543 | ||
|
|
f4e94d6d34 | ||
|
|
13daa4e715 | ||
|
|
75be4f5f61 | ||
|
|
46ced988eb | ||
|
|
28acfc6d3f | ||
|
|
660c7d3be5 | ||
|
|
52617bd5a8 | ||
|
|
9db181037f | ||
|
|
861ea5aabc | ||
|
|
e4125c0c6a | ||
|
|
ff9a8a1247 | ||
|
|
ac29e30f54 | ||
|
|
a02ae3ceed | ||
|
|
54c02f4781 | ||
|
|
a5e721b107 | ||
|
|
12a4dd58f3 | ||
|
|
5a7ddb8330 | ||
|
|
cb75531818 | ||
|
|
6229a0579f | ||
|
|
fb980bb695 | ||
|
|
19dc9d7bbc | ||
|
|
1e55b6f6b3 | ||
|
|
0a35bfe2f5 | ||
|
|
2f587c6d48 | ||
|
|
5b426aee86 | ||
|
|
f700769b27 | ||
|
|
04b672eebe | ||
|
|
f7238e8e53 | ||
|
|
33cb39d318 | ||
|
|
612658d9c4 | ||
|
|
c31613b2c7 | ||
|
|
d7419d213a | ||
|
|
67a3f86cc9 | ||
|
|
b0c0747a09 | ||
|
|
64d574c35c | ||
|
|
4e531c2d1e | ||
|
|
ee8fa60bc5 | ||
|
|
e40ff56e07 | ||
|
|
0c1db1e813 | ||
|
|
a9b14d4c1b | ||
|
|
69f77ee2f1 | ||
|
|
3b5b9bbb21 | ||
|
|
65b5d6c5a9 | ||
|
|
1a575bc9ae | ||
|
|
ab242c5b17 | ||
|
|
90977fb4e1 | ||
|
|
404e5d206d | ||
|
|
d41a255361 | ||
|
|
efff850e54 | ||
|
|
4fcdde4149 | ||
|
|
c4d8b9e7fb | ||
|
|
68526dc119 | ||
|
|
4f2fc096e5 | ||
|
|
0f62ef687c | ||
|
|
32eb1135ed | ||
|
|
f88b4a6d57 | ||
|
|
9628b73525 | ||
|
|
d70a48bd13 | ||
|
|
660f1e181a | ||
|
|
773284369b | ||
|
|
f199c15269 | ||
|
|
e7facd74ba | ||
|
|
47e612afef | ||
|
|
921f88b95d | ||
|
|
7fe8d73473 | ||
|
|
fcb09556b1 | ||
|
|
69c6d8a099 | ||
|
|
dd5afc0560 | ||
|
|
471486b531 | ||
|
|
202cfb574c | ||
|
|
ebb0976866 | ||
|
|
5c785ab1ac | ||
|
|
2024b8b2c2 | ||
|
|
86c695ca52 | ||
|
|
9dd1df36d5 | ||
|
|
27f99b6309 | ||
|
|
ed9bc66060 | ||
|
|
d1598bb754 | ||
|
|
cc5855d07b | ||
|
|
487b36f48f | ||
|
|
c0b00c9a4c | ||
|
|
86c2ac95bb | ||
|
|
edb79f2972 | ||
|
|
305cefe461 | ||
|
|
315391b0aa | ||
|
|
fd45acc910 | ||
|
|
a574ae6b6a | ||
|
|
fa56a477c2 | ||
|
|
0db4556efb | ||
|
|
fad95c028a | ||
|
|
5c462f5600 | ||
|
|
5dcc486214 | ||
|
|
9d2915c328 | ||
|
|
4f8f6f1ca3 | ||
|
|
fbf58486fb | ||
|
|
97aae225da | ||
|
|
1ca2debd7c | ||
|
|
c1584dd72f | ||
|
|
2bbd29998e | ||
|
|
3b2d7abe3d | ||
|
|
994d1acbfc | ||
|
|
1f4ae1e2d5 | ||
|
|
18ad74a982 | ||
|
|
4cad06c7b3 | ||
|
|
ec77dccb1d | ||
|
|
63b4848bb0 | ||
|
|
e27802c41e | ||
|
|
75329830f9 | ||
|
|
030a05c103 | ||
|
|
e4751fd84c | ||
|
|
252def5b95 | ||
|
|
42f2af7956 | ||
|
|
91fb8225d1 | ||
|
|
bee60023ae | ||
|
|
0ffae1896b | ||
|
|
8f4820ba28 | ||
|
|
3a02ad8664 | ||
|
|
244e0ded60 | ||
|
|
19926d95fe | ||
|
|
f27ee60149 | ||
|
|
3908813afe | ||
|
|
e6f24b0924 | ||
|
|
b2c1c8f8db | ||
|
|
5a2f9a374b | ||
|
|
59ab5107bf | ||
|
|
06c65d8404 | ||
|
|
d38055f825 | ||
|
|
4aeb4c78ac | ||
|
|
3741a71cc5 | ||
|
|
fc9ddaf941 | ||
|
|
1f6a9cfa46 | ||
|
|
1a18fad1a4 | ||
|
|
39b5c4746e | ||
|
|
5ec08d0a29 | ||
|
|
7ec222895c | ||
|
|
da1ec3132f | ||
|
|
167f1e5770 | ||
|
|
8719b9d75c | ||
|
|
af1f161b06 | ||
|
|
7e4ff05c57 | ||
|
|
f1ecd37578 | ||
|
|
7ccec0e3f7 | ||
|
|
397361f23d | ||
|
|
681da2e90c | ||
|
|
118e6b1804 | ||
|
|
f933b90c66 | ||
|
|
21840d3ffe | ||
|
|
5e80ab9362 | ||
|
|
43eb238b08 | ||
|
|
00718f99cf | ||
|
|
c3a73d63b8 | ||
|
|
bc3c8eaf74 | ||
|
|
8d268ef021 | ||
|
|
c45ff4dd4f | ||
|
|
37ad137012 | ||
|
|
0165c4a40a | ||
|
|
3270acdd00 | ||
|
|
ee84296dfe | ||
|
|
42849e7104 | ||
|
|
a1f6dd6f4f | ||
|
|
47cdfb3de0 | ||
|
|
ac362bf1db | ||
|
|
462f73f695 | ||
|
|
cf92f91e1e | ||
|
|
52d6ac6cda | ||
|
|
eeb2aaf9ae | ||
|
|
f84c9f3b5d | ||
|
|
be56918174 | ||
|
|
08daaf95e4 | ||
|
|
51d73c6618 | ||
|
|
4644a2b5cc | ||
|
|
89863660ba | ||
|
|
641d188997 | ||
|
|
226932e631 | ||
|
|
be8124154b | ||
|
|
f086cc8713 | ||
|
|
624daabc02 | ||
|
|
05a187e470 | ||
|
|
53da1ff1fe | ||
|
|
112c731c7a | ||
|
|
480a220fda | ||
|
|
6cf6857602 | ||
|
|
97e2fb1288 | ||
|
|
d1e70b5abf | ||
|
|
a70fb9db7d | ||
|
|
285503d009 | ||
|
|
f364965ac0 | ||
|
|
61cea4624e | ||
|
|
2899e47591 | ||
|
|
e7ee194acf | ||
|
|
6e5536eae9 | ||
|
|
5514e53a0b | ||
|
|
d8dee90c10 | ||
|
|
dcee63771a | ||
|
|
b7133b302b | ||
|
|
061040f5d9 | ||
|
|
f6ccaadc0c | ||
|
|
7c80de7ee1 | ||
|
|
ef0f506b6f | ||
|
|
3d63f0771a | ||
|
|
20ad87611f | ||
|
|
fa7839e287 | ||
|
|
2aec2c13b5 | ||
|
|
3eb0d71bd3 | ||
|
|
18f9b6f34e | ||
|
|
57110c98e4 | ||
|
|
a6ee75a9cf | ||
|
|
8d1618692e | ||
|
|
960c6cae62 | ||
|
|
67ec0d3c80 | ||
|
|
d3f32b5bc3 | ||
|
|
84e350aa6f | ||
|
|
80242f0e08 | ||
|
|
2a3ce12bd4 | ||
|
|
aed8ba105a | ||
|
|
0e9bc0ed87 | ||
|
|
9a798fe220 | ||
|
|
5c3d9db5c9 | ||
|
|
5ee774892a | ||
|
|
b4dda8bad8 | ||
|
|
47324aea97 | ||
|
|
def2b28d4e | ||
|
|
b7bc34906d | ||
|
|
bb08d5241e | ||
|
|
ab24523bff | ||
|
|
b8debb5404 | ||
|
|
d0e39853c6 | ||
|
|
a0bfd99a5d | ||
|
|
471a8b7c2b | ||
|
|
591e5e3145 | ||
|
|
282e7b1828 | ||
|
|
007b060cbd | ||
|
|
8168a75bde | ||
|
|
5afda4e76c | ||
|
|
88c712b848 | ||
|
|
fca63d02f9 | ||
|
|
330888cb3b | ||
|
|
23c24c776e | ||
|
|
233fa9b25c | ||
|
|
9530d6ad20 | ||
|
|
6458d3cac4 | ||
|
|
6945aa34eb | ||
|
|
47c9cc2fea | ||
|
|
1e90cec6f3 | ||
|
|
843867717c | ||
|
|
dd87769090 | ||
|
|
398370424b | ||
|
|
e6797e0303 | ||
|
|
6041e063e2 | ||
|
|
be2d3c9c1e | ||
|
|
c3861955e0 | ||
|
|
05aa30d1be | ||
|
|
1c2b57dfe8 | ||
|
|
47ef864295 | ||
|
|
a517ea45bd | ||
|
|
342f3c223d | ||
|
|
4a45e69eb1 | ||
|
|
e52d05113e | ||
|
|
45992a0e0a | ||
|
|
1fb405afd3 | ||
|
|
e23d4d8fa1 | ||
|
|
079cbe11f4 | ||
|
|
3e61bd4d49 | ||
|
|
b517ed28c0 | ||
|
|
d8aab386f1 | ||
|
|
7d422bfae2 | ||
|
|
7bc870e72f | ||
|
|
edee53f6f2 | ||
|
|
299712ead3 | ||
|
|
c24f75999a | ||
|
|
bde48c051a | ||
|
|
d087a890ba | ||
|
|
75d4e70560 | ||
|
|
73ab25d008 | ||
|
|
790ccd429c | ||
|
|
47fd1475b5 | ||
|
|
251a2b7455 | ||
|
|
3c85d31c15 | ||
|
|
2e6cbcb362 | ||
|
|
12d74b99e8 | ||
|
|
4cda7e2d92 | ||
|
|
1350deae56 | ||
|
|
df564e1b8b | ||
|
|
bb7ce4cbb3 | ||
|
|
1655fde09b | ||
|
|
89d1f1c202 | ||
|
|
b23c507af5 | ||
|
|
15055440da | ||
|
|
e2b7c85955 | ||
|
|
9c5ab2afbd | ||
|
|
d413562145 | ||
|
|
87f54be13a | ||
|
|
bea1c5dc28 | ||
|
|
04b4dbbfee | ||
|
|
d55e7319da | ||
|
|
54bb99d758 | ||
|
|
b977bf5cca | ||
|
|
fa7f89a400 | ||
|
|
523f75654d | ||
|
|
e85ae907a0 | ||
|
|
b0e287498e | ||
|
|
8a33c98bc6 | ||
|
|
59bf1a2260 | ||
|
|
214adcf611 | ||
|
|
23152f0c50 | ||
|
|
2a4abbee24 | ||
|
|
f637268fa7 | ||
|
|
ea7f90713c | ||
|
|
53a19afe52 | ||
|
|
ed6951a653 | ||
|
|
2e99f52133 | ||
|
|
da5542a557 | ||
|
|
1cd4b2c4dc | ||
|
|
253e86230c | ||
|
|
9f9b5def41 | ||
|
|
d949b58fc0 | ||
|
|
ab74e56a40 | ||
|
|
a537c584d0 | ||
|
|
98365b6bfb | ||
|
|
57c030d3b9 | ||
|
|
89acc703f5 | ||
|
|
6df2d7d822 | ||
|
|
3c192c2fb5 | ||
|
|
995a910f6a | ||
|
|
c29e58e3d4 | ||
|
|
924809b19b | ||
|
|
85e7055505 | ||
|
|
fb6d554df6 | ||
|
|
bd0c5c655e | ||
|
|
e6e190942c | ||
|
|
25ad139675 | ||
|
|
a095644731 | ||
|
|
f197eca320 | ||
|
|
d602cb68ca | ||
|
|
56e98ea5f4 | ||
|
|
16d8a560bf | ||
|
|
32325f99ad | ||
|
|
9b33a1058a | ||
|
|
ff5c8d7451 | ||
|
|
1ba51e4f59 | ||
|
|
7fe2b8ef2f | ||
|
|
7bb61307e0 | ||
|
|
d0057121ef | ||
|
|
18c4196354 | ||
|
|
2fcb40d5a9 | ||
|
|
0adb601f3c | ||
|
|
b669437296 | ||
|
|
9ef27203f0 | ||
|
|
d2a1d849c9 | ||
|
|
712b383e2c | ||
|
|
94175d1aa6 | ||
|
|
9b51069041 | ||
|
|
80ab81fefc | ||
|
|
d00562d37a | ||
|
|
75a344ef56 | ||
|
|
ffebb58d92 | ||
|
|
c9199ba1bd | ||
|
|
5024ecd640 | ||
|
|
a185d6f9a0 | ||
|
|
690610d4b1 | ||
|
|
043f7cdc47 | ||
|
|
4d1ad52405 | ||
|
|
7294424c3e | ||
|
|
bb55fc4150 | ||
|
|
263eec7368 | ||
|
|
7b03f5bab2 | ||
|
|
0fd042dce6 | ||
|
|
e379239140 | ||
|
|
c1db99a5a5 | ||
|
|
fb2bf7a377 | ||
|
|
9404b731ec | ||
|
|
e682c0355b | ||
|
|
d8e7291cb2 | ||
|
|
556a3eb18f | ||
|
|
f9fcdb2e8b | ||
|
|
d695d12872 | ||
|
|
ced7164912 | ||
|
|
ce3bdf63c0 | ||
|
|
4c678c4936 | ||
|
|
18d128eb3d | ||
|
|
97632e5573 | ||
|
|
5dc8fe40ca | ||
|
|
28af256be0 | ||
|
|
c3a165e61d | ||
|
|
33e5dd4aed | ||
|
|
9122f152d1 | ||
|
|
2202b32f31 | ||
|
|
15ab0b2fed | ||
|
|
a8b0f6d679 | ||
|
|
40411ea627 | ||
|
|
8a6a3127c6 | ||
|
|
f951c6f489 | ||
|
|
82a3b8bb39 | ||
|
|
c29ccfe011 | ||
|
|
d48efbf442 | ||
|
|
69ef6def38 | ||
|
|
f7560c3311 | ||
|
|
ea57d8b883 | ||
|
|
a7e5a5b26c | ||
|
|
ea0dda98ce | ||
|
|
41fb57e449 | ||
|
|
3783f0a9f0 | ||
|
|
ae566920b6 | ||
|
|
036ed7b9ed | ||
|
|
5775b4c05d | ||
|
|
29502e7f41 | ||
|
|
362d8eabae | ||
|
|
a3c58e52fc | ||
|
|
1edd161684 | ||
|
|
7f95891a9a | ||
|
|
80c6c8ef9f | ||
|
|
143339dd67 | ||
|
|
7611c13d12 | ||
|
|
d49e366413 | ||
|
|
ac5fbd9515 | ||
|
|
aef75f9b83 | ||
|
|
faec861081 | ||
|
|
5a9a7a3835 | ||
|
|
2649b673f7 | ||
|
|
7958dc0592 | ||
|
|
379a49f944 | ||
|
|
a311e07106 | ||
|
|
496fab031c | ||
|
|
1a95f34b0e | ||
|
|
d560de4b40 | ||
|
|
0d9fc601ac | ||
|
|
325c9111eb | ||
|
|
968d5be74e | ||
|
|
71ee042218 | ||
|
|
d826db89d6 | ||
|
|
7db856d39d | ||
|
|
f90054cf25 | ||
|
|
37ae99ccd9 | ||
|
|
e71b49481b | ||
|
|
701d0dfe3d | ||
|
|
3f02e12539 | ||
|
|
5b689a5592 | ||
|
|
2bc70890f0 | ||
|
|
4e5aa4ecc8 | ||
|
|
5f50d79efa | ||
|
|
000197fd28 | ||
|
|
4cb26d2e8e | ||
|
|
1d41634272 | ||
|
|
32e8284505 | ||
|
|
651cb89948 | ||
|
|
63f18f033c | ||
|
|
0558de12c6 | ||
|
|
95293457fb | ||
|
|
d71ad04d98 | ||
|
|
7134cc8e1c | ||
|
|
3de440338d | ||
|
|
73d14f5d37 | ||
|
|
291b1d1efc | ||
|
|
57960bdc81 | ||
|
|
e1d231baa3 | ||
|
|
8436e2866f | ||
|
|
3ee87e8767 | ||
|
|
11e9419258 | ||
|
|
cb7fe94b04 | ||
|
|
3f01f73ea9 | ||
|
|
c35650e51a | ||
|
|
b0813f12e6 | ||
|
|
67ac3f1a24 | ||
|
|
44fa0d77ff | ||
|
|
d00b9f3b7a | ||
|
|
128dc9fea1 | ||
|
|
ccff712d83 | ||
|
|
069df5ef0b | ||
|
|
0357ec88d5 | ||
|
|
ccc68bcf03 | ||
|
|
92362093ab | ||
|
|
6fbff048f0 | ||
|
|
370e667e91 | ||
|
|
eeaac76f5f | ||
|
|
d13f9602ff | ||
|
|
400ac56651 | ||
|
|
5481a834bf | ||
|
|
e53229ec00 | ||
|
|
cee5a88341 | ||
|
|
1b92700990 | ||
|
|
78b2a99f2e | ||
|
|
4e4b4bfe68 | ||
|
|
a60d348274 | ||
|
|
d1402b6502 | ||
|
|
92e44aa6af | ||
|
|
5311a35f5a | ||
|
|
d2e59b525d | ||
|
|
543f840912 | ||
|
|
ea31c662c5 | ||
|
|
c9b9b3d27f | ||
|
|
59251ee5d0 | ||
|
|
6fd117c5f8 | ||
|
|
0fbae7610c | ||
|
|
c692570212 | ||
|
|
3ecdd96931 | ||
|
|
7bc8d77387 | ||
|
|
4ce02e4c85 | ||
|
|
856acf4a51 | ||
|
|
d70a2fe63d | ||
|
|
855c5283e4 | ||
|
|
60f2c1e4cf | ||
|
|
935d390911 | ||
|
|
0d09aabad6 | ||
|
|
89c468924e | ||
|
|
d0d167b663 | ||
|
|
e721ef8d46 | ||
|
|
7c2baa6086 | ||
|
|
36ecf226a9 | ||
|
|
87b5903f6a | ||
|
|
4c0b00bf2b | ||
|
|
b4b0eda7d9 | ||
|
|
a83ecd477e | ||
|
|
55add69fa0 | ||
|
|
199c295f1f | ||
|
|
ad0e098a25 | ||
|
|
eee9c54a27 | ||
|
|
9719e6caa7 | ||
|
|
418720f6df | ||
|
|
80bd2694d6 | ||
|
|
f6b7aaebbd | ||
|
|
74610b8cd7 | ||
|
|
7492ab4de2 | ||
|
|
c04a4ba604 | ||
|
|
63ccbc1ebd | ||
|
|
ee553b7830 | ||
|
|
97fc52093f | ||
|
|
49397039e0 | ||
|
|
efe1ab5db6 | ||
|
|
daeffdc81b | ||
|
|
30083c4d0f | ||
|
|
56e616d5bf | ||
|
|
112da0b8c6 | ||
|
|
163a3993bd | ||
|
|
1b9bb31dd6 | ||
|
|
8db3b22411 | ||
|
|
4aae5ca451 | ||
|
|
d3a3b7a8cd | ||
|
|
cc9342df9d | ||
|
|
fe0dce0960 | ||
|
|
766f836952 | ||
|
|
78b0aac5ec | ||
|
|
690627a338 | ||
|
|
25ced4c075 | ||
|
|
771b5333aa | ||
|
|
ae72608c5d | ||
|
|
2e778a2a8e | ||
|
|
bc9e811797 | ||
|
|
4db7f33eaf | ||
|
|
d3c5e3ab47 | ||
|
|
b13c6c4892 | ||
|
|
c50dda244b | ||
|
|
3fdc2ca0da | ||
|
|
6b7ca3c559 | ||
|
|
5c2a2b1b7e | ||
|
|
69e45f9a4f | ||
|
|
127ebc15b9 | ||
|
|
ea1de18326 | ||
|
|
edd25c68ee | ||
|
|
e30a4139e6 | ||
|
|
546acfd21d | ||
|
|
d27690b8c6 | ||
|
|
adc56e60fc | ||
|
|
266ce5c43b | ||
|
|
0bf07eadcc | ||
|
|
e4386d9398 | ||
|
|
c1dd403ab9 | ||
|
|
0e4f700527 | ||
|
|
a48c991958 | ||
|
|
cbc250b7d0 | ||
|
|
b27ef219a0 | ||
|
|
d163637fa8 | ||
|
|
905d4d7020 | ||
|
|
f85dd77036 | ||
|
|
8f5f8ffdd6 | ||
|
|
b09093f78c | ||
|
|
104699e500 | ||
|
|
38bf8c0225 | ||
|
|
6acda994e4 | ||
|
|
e563211790 | ||
|
|
6a5879cc15 | ||
|
|
aa624d86e6 | ||
|
|
c410b7b2ce | ||
|
|
79f1539486 | ||
|
|
d7b7cc954a | ||
|
|
1914a5b5ff | ||
|
|
921b828afb | ||
|
|
d3d35bd9ff | ||
|
|
76a328a062 | ||
|
|
fb90e169cb | ||
|
|
a1a307d858 | ||
|
|
3733e7e223 | ||
|
|
3e8a587aa3 | ||
|
|
8f2f1f8c1d | ||
|
|
a940ce3036 | ||
|
|
d7da72a720 | ||
|
|
b54853140a | ||
|
|
8ad2179423 | ||
|
|
3037d72bcb | ||
|
|
7d16e97b95 | ||
|
|
0293b774f3 | ||
|
|
32cd94b88f | ||
|
|
5e5dd78b7c | ||
|
|
1c5c741e87 | ||
|
|
095e6993a8 | ||
|
|
7c3425a012 | ||
|
|
bc724bf781 | ||
|
|
13144d4b57 | ||
|
|
97bdb15bd6 | ||
|
|
fb69ecdc9b | ||
|
|
1fe1c3eabb | ||
|
|
191fd5e495 | ||
|
|
8aa017bfda | ||
|
|
9ea947c808 | ||
|
|
2a7a55eca4 | ||
|
|
759c00098b | ||
|
|
cce36624dc | ||
|
|
4664850186 | ||
|
|
d9c666f6df | ||
|
|
d7e38a52ea | ||
|
|
f3f4790103 | ||
|
|
83c1136ac5 | ||
|
|
0ae5ae5d9a | ||
|
|
c070e3e8f7 | ||
|
|
0de167b07b | ||
|
|
f904e2fe99 | ||
|
|
b195ebad46 | ||
|
|
23ef69b935 | ||
|
|
55c790f069 | ||
|
|
4bcb13efc0 | ||
|
|
50c7441533 | ||
|
|
c1a3ee1706 | ||
|
|
357fc09e69 | ||
|
|
c1d08a6dc0 | ||
|
|
f689143670 | ||
|
|
56b3b79c50 | ||
|
|
f351c251e4 | ||
|
|
5cc66cef42 | ||
|
|
6791759440 | ||
|
|
ac98f21199 | ||
|
|
22ebbcfd89 | ||
|
|
292df7a9f7 | ||
|
|
64fd96611c | ||
|
|
de4a007bdf | ||
|
|
567faeb07e | ||
|
|
3afb3d0b22 | ||
|
|
8172ebf62b | ||
|
|
1720d4023f | ||
|
|
da6ab78384 | ||
|
|
6fe20fb305 | ||
|
|
d41f0bb324 | ||
|
|
8e555e60f7 | ||
|
|
243f99aeb1 | ||
|
|
ab36db7646 | ||
|
|
2e3c87b67d | ||
|
|
a549d12808 | ||
|
|
149fea8b76 | ||
|
|
f7295a25d8 | ||
|
|
9eeb14956c | ||
|
|
796638d095 | ||
|
|
79621505f1 | ||
|
|
e484445b1e | ||
|
|
5eddba5516 | ||
|
|
bdc857952a | ||
|
|
2bcc59faea | ||
|
|
6cc12b871c | ||
|
|
d201e7c503 | ||
|
|
c695df0adf | ||
|
|
a04e3080fb | ||
|
|
7d395a29a7 | ||
|
|
4046bb977e | ||
|
|
d250b9d7b0 | ||
|
|
a7f159bddc | ||
|
|
d0fa467a3c | ||
|
|
f4e0a3c0f8 | ||
|
|
0bc80adc28 | ||
|
|
cfdaf0e3f6 | ||
|
|
b5160c5d2c | ||
|
|
210a538cdd | ||
|
|
fd786b3020 | ||
|
|
ba9560079c | ||
|
|
d5694c0f35 | ||
|
|
1522b24803 | ||
|
|
922baa930d | ||
|
|
faafda6b21 | ||
|
|
9efc4fb5e9 | ||
|
|
37d83a280f | ||
|
|
8c0544c264 | ||
|
|
5e6a26a6ea | ||
|
|
9a09647330 | ||
|
|
0c00e8da0e | ||
|
|
af47cce86b | ||
|
|
301e86a46e | ||
|
|
0b1afe7f6c | ||
|
|
1739f0631a | ||
|
|
4f4e87a82c | ||
|
|
49ec611c8f | ||
|
|
8f06e51170 | ||
|
|
a853164302 | ||
|
|
dc8207149b | ||
|
|
cc73efc0bd | ||
|
|
4992efd172 | ||
|
|
956b1bdb3a | ||
|
|
cd52aaba13 | ||
|
|
cc6189b7ce | ||
|
|
5e82fc4673 | ||
|
|
107a6f877b | ||
|
|
e643860e3d | ||
|
|
0373589ab8 | ||
|
|
dce56a2b85 | ||
|
|
27d2ebfb45 | ||
|
|
1f457f9d9e | ||
|
|
c0282c4a3c | ||
|
|
0a6e1de404 | ||
|
|
131524e670 | ||
|
|
539495d2f7 | ||
|
|
966dac97f8 | ||
|
|
bf6e596808 | ||
|
|
efa24f5a8d | ||
|
|
432146b068 | ||
|
|
02b6eaaff0 | ||
|
|
e7ee18e421 | ||
|
|
dc532e337b | ||
|
|
a952e249b4 | ||
|
|
94465ef1ae | ||
|
|
0a534767f0 | ||
|
|
7937f7038b | ||
|
|
d6a01ad29f | ||
|
|
b1cb583e8c | ||
|
|
ea4d822923 | ||
|
|
583177feff | ||
|
|
3bec1b8c1b | ||
|
|
b992669f5b | ||
|
|
086aa61e5a | ||
|
|
725533d991 | ||
|
|
c310053777 | ||
|
|
2041e12eba | ||
|
|
5d00522d4e | ||
|
|
c71e816e37 | ||
|
|
6721ec8e7d | ||
|
|
93eadfb9dc | ||
|
|
cf3ce29a08 | ||
|
|
cd1117c08c | ||
|
|
f247823936 | ||
|
|
a8feef3c12 | ||
|
|
de9707f088 | ||
|
|
9ff396c69f | ||
|
|
4b350d02e0 | ||
|
|
ae3696e82d | ||
|
|
b3f6731db5 | ||
|
|
2b9ef4d406 | ||
|
|
54a34001e3 | ||
|
|
aad7cc7572 | ||
|
|
def5e29b91 | ||
|
|
187ea0da1c | ||
|
|
5b7fa01825 | ||
|
|
c38044106c | ||
|
|
399e3a5ee1 | ||
|
|
fce5b81c22 | ||
|
|
a4ac9f2b7b | ||
|
|
abedeebc0a | ||
|
|
b905400892 | ||
|
|
c4d6f5e584 | ||
|
|
b5232dd24d | ||
|
|
89886f10c7 | ||
|
|
c679500d06 | ||
|
|
76f80bf694 | ||
|
|
a7b72f0e0e | ||
|
|
00eb6725e6 | ||
|
|
af22e0a567 | ||
|
|
98b6f63b70 | ||
|
|
403a99d2ea | ||
|
|
b2735d7b5b | ||
|
|
699ad316e5 | ||
|
|
8617ae5c1f | ||
|
|
c5ac5be764 | ||
|
|
881f57b047 | ||
|
|
1c2b815d95 | ||
|
|
b45fcf5bd7 | ||
|
|
45ec01d197 | ||
|
|
0df7e59ca4 | ||
|
|
89c34ed8b3 | ||
|
|
d9b8a04841 | ||
|
|
ccfe08bc60 | ||
|
|
2e3ee22aca | ||
|
|
226cf399ba | ||
|
|
0f6260601f | ||
|
|
d2254df062 | ||
|
|
3e83d29fb4 | ||
|
|
a432bc7e7b | ||
|
|
486279e1d1 | ||
|
|
3967981b38 | ||
|
|
597c549b5b | ||
|
|
fc2d9bb461 | ||
|
|
832cc366af | ||
|
|
5458605618 | ||
|
|
104af2ef7a | ||
|
|
64ff933451 | ||
|
|
031ed64305 | ||
|
|
1be332a0f9 | ||
|
|
b9d4dbd5e0 | ||
|
|
2e264b342a | ||
|
|
4972db4bf6 | ||
|
|
b70db77c29 | ||
|
|
f8612d1572 | ||
|
|
f9d0c563e4 | ||
|
|
3105205ab8 | ||
|
|
ec29d592da | ||
|
|
62ac9f79f2 | ||
|
|
a6d695f471 | ||
|
|
59d2fa81dd | ||
|
|
4003586456 | ||
|
|
be44dc3ff5 | ||
|
|
c59b1ea387 | ||
|
|
00089a7c01 | ||
|
|
c661c65c8c | ||
|
|
13da5ced15 | ||
|
|
b970b393f2 | ||
|
|
30323116a5 | ||
|
|
2fc274da7d | ||
|
|
b19b36d113 | ||
|
|
b6f5db3692 | ||
|
|
8bdaacaa5e | ||
|
|
c9ead25b1e | ||
|
|
ddff950fcd | ||
|
|
ed6be89d5c | ||
|
|
6e63472930 | ||
|
|
94397a90bd | ||
|
|
216bfaca7e | ||
|
|
9854fc712f | ||
|
|
99635d9491 | ||
|
|
96209cbeb0 | ||
|
|
db520fe9e7 | ||
|
|
93361fe72d | ||
|
|
4499228cdb | ||
|
|
d79e68bbf9 | ||
|
|
ec9eab8a3e | ||
|
|
628cdd34aa | ||
|
|
20d6dffce9 | ||
|
|
afa58a984a | ||
|
|
209cdccdc5 | ||
|
|
2761f57565 | ||
|
|
791a397d55 | ||
|
|
3e428ff505 | ||
|
|
ddc237872c | ||
|
|
ee5ac6a582 | ||
|
|
5df28c76e8 |
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@@ -0,0 +1,7 @@
|
||||
# See http://editorconfig.org
|
||||
|
||||
# In Go files we indent with tabs but still
|
||||
# set indent_size to control the GitHub web viewer.
|
||||
[*.go]
|
||||
indent_size=4
|
||||
|
||||
9
.github/ISSUE_TEMPLATE
vendored
Normal file
9
.github/ISSUE_TEMPLATE
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
## Description of the problem or steps to reproduce
|
||||
|
||||
## Specifications
|
||||
|
||||
<!-- You can use `micro -version` to get the commit hash. -->
|
||||
|
||||
Commit hash:
|
||||
OS:
|
||||
Terminal:
|
||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,3 +1,19 @@
|
||||
./micro
|
||||
.DS_Store
|
||||
|
||||
micro
|
||||
!cmd/micro
|
||||
binaries/
|
||||
tmp.sh
|
||||
test/
|
||||
.idea/
|
||||
packages/
|
||||
todo.txt
|
||||
test.txt
|
||||
log.txt
|
||||
*.old
|
||||
tools/build-version
|
||||
tools/build-date
|
||||
tools/info-plist
|
||||
tools/bindata
|
||||
tools/vscode-tests/
|
||||
*.hdr
|
||||
|
||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "tools/go-bindata"]
|
||||
path = tools/go-bindata
|
||||
url = https://github.com/zyedidia/go-bindata
|
||||
@@ -1,2 +1,9 @@
|
||||
language: go
|
||||
script: make test
|
||||
go:
|
||||
- "1.11.x"
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
script:
|
||||
- env GO111MODULE=on make build
|
||||
- env GO111MODULE=on make test
|
||||
|
||||
4
LICENSE
4
LICENSE
@@ -1,6 +1,6 @@
|
||||
Micro is licensed under the MIT "Expat" License:
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016: Zachary Yedidia.
|
||||
Copyright (c) 2016-2020: Zachary Yedidia, et al.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
||||
1306
LICENSE-THIRD-PARTY
Normal file
1306
LICENSE-THIRD-PARTY
Normal file
File diff suppressed because it is too large
Load Diff
72
Makefile
72
Makefile
@@ -1,47 +1,71 @@
|
||||
.PHONY: runtime
|
||||
|
||||
VERSION = $(shell git describe --tags --abbrev=0)
|
||||
VERSION = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
|
||||
go run tools/build-version.go)
|
||||
HASH = $(shell git rev-parse --short HEAD)
|
||||
DATE = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
|
||||
go run tools/build-date.go)
|
||||
ADDITIONAL_GO_LINKER_FLAGS = $(shell GOOS=$(shell go env GOHOSTOS) \
|
||||
GOARCH=$(shell go env GOHOSTARCH))
|
||||
GOBIN ?= $(shell go env GOPATH)/bin
|
||||
GOVARS = -X github.com/zyedidia/micro/internal/util.Version=$(VERSION) -X github.com/zyedidia/micro/internal/util.CommitHash=$(HASH) -X 'github.com/zyedidia/micro/internal/util.CompileDate=$(DATE)'
|
||||
DEBUGVAR = -X github.com/zyedidia/micro/internal/util.Debug=ON
|
||||
VSCODE_TESTS_BASE_URL = 'https://raw.githubusercontent.com/microsoft/vscode/e6a45f4242ebddb7aa9a229f85555e8a3bd987e2/src/vs/editor/test/common/model/'
|
||||
|
||||
# Builds micro after checking dependencies but without updating the runtime
|
||||
build: deps tcell
|
||||
go build -ldflags "-X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(shell date -u '+%B %d, %Y')'" -o micro ./cmd/micro
|
||||
build:
|
||||
go build -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
|
||||
|
||||
# Builds micro after building the runtiem and checking dependencies
|
||||
build-dbg:
|
||||
go build -ldflags "-s -w $(ADDITIONAL_GO_LINKER_FLAGS) $(DEBUGVAR)" ./cmd/micro
|
||||
|
||||
build-tags: fetch-tags
|
||||
go build -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
|
||||
|
||||
# Builds micro after building the runtime and checking dependencies
|
||||
build-all: runtime build
|
||||
|
||||
# Builds micro without checking for dependencies
|
||||
build-quick:
|
||||
go build -ldflags "-X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(shell date -u '+%B %d, %Y')'" -o micro ./cmd/micro
|
||||
go build -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
|
||||
|
||||
# Same as 'build' but installs to $GOPATH/bin afterward
|
||||
install: build
|
||||
mv micro $(GOPATH)/bin
|
||||
# Same as 'build' but installs to $GOBIN afterward
|
||||
install:
|
||||
go install -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
|
||||
|
||||
# Same as 'build-all' but installs to $GOPATH/bin afterward
|
||||
# Same as 'build-all' but installs to $GOBIN afterward
|
||||
install-all: runtime install
|
||||
|
||||
# Same as 'build-quick' but installs to $GOPATH/bin afterward
|
||||
install-quick: build-quick
|
||||
mv micro $(GOPATH)/bin
|
||||
# Same as 'build-quick' but installs to $GOBIN afterward
|
||||
install-quick:
|
||||
go install -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
|
||||
|
||||
# Updates tcell
|
||||
tcell:
|
||||
git -C $(GOPATH)/src/github.com/zyedidia/tcell pull
|
||||
|
||||
# Checks for dependencies
|
||||
deps:
|
||||
go get -d ./cmd/micro
|
||||
fetch-tags:
|
||||
git fetch --tags
|
||||
|
||||
# Builds the runtime
|
||||
runtime:
|
||||
go get -u github.com/jteeuwen/go-bindata/...
|
||||
$(GOPATH)/bin/go-bindata -nometadata -o runtime.go runtime/...
|
||||
mv runtime.go cmd/micro
|
||||
git submodule update --init
|
||||
go run runtime/syntax/make_headers.go runtime/syntax
|
||||
go build -o tools/bindata ./tools/go-bindata
|
||||
tools/bindata -pkg config -nomemcopy -nometadata -o runtime.go runtime/...
|
||||
mv runtime.go internal/config
|
||||
gofmt -w internal/config/runtime.go
|
||||
|
||||
testgen:
|
||||
mkdir -p tools/vscode-tests
|
||||
cd tools/vscode-tests && \
|
||||
curl --remote-name-all $(VSCODE_TESTS_BASE_URL){editableTextModelAuto,editableTextModel,model.line}.test.ts
|
||||
tsc tools/vscode-tests/*.ts > /dev/null; true
|
||||
go run tools/testgen.go tools/vscode-tests/*.js > buffer_generated_test.go
|
||||
mv buffer_generated_test.go internal/buffer
|
||||
gofmt -w internal/buffer/buffer_generated_test.go
|
||||
|
||||
test:
|
||||
go get -d ./cmd/micro
|
||||
go test ./cmd/micro
|
||||
go test ./internal/...
|
||||
|
||||
bench:
|
||||
go test -bench=. ./internal/...
|
||||
|
||||
clean:
|
||||
rm -f micro
|
||||
|
||||
238
README.md
238
README.md
@@ -1,54 +1,80 @@
|
||||
# 
|
||||
<img alt="micro logo" src="./assets/micro-logo.svg" width="500px"/>
|
||||
|
||||
[](https://travis-ci.org/zyedidia/micro)
|
||||
[](https://goreportcard.com/report/github.com/zyedidia/micro)
|
||||
[](https://gitter.im/zyedidia/micro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://github.com/zyedidia/micro/releases)
|
||||
[](https://github.com/zyedidia/micro/blob/master/LICENSE)
|
||||
[](https://gitter.im/zyedidia/micro?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://build.snapcraft.io/user/zyedidia/micro)
|
||||
|
||||
Micro is a terminal-based text editor that aims to be easy to use and intuitive, while also taking advantage of the full capabilities
|
||||
of modern terminals. It comes as one single, batteries-included, static binary with no dependencies, and you can download and use it right now.
|
||||
**micro** is a terminal-based text editor that aims to be easy to use and intuitive, while also taking advantage of the capabilities
|
||||
of modern terminals. It comes as a single, batteries-included, static binary with no dependencies; you can download and use it right now!
|
||||
|
||||
As the name indicates, micro aims to be somewhat of a successor to the nano editor by being easy to install and use in a pinch, but micro also aims to be
|
||||
enjoyable to use full time, whether you work in the terminal because you prefer it (like me), or because you need to (over ssh).
|
||||
As its name indicates, micro aims to be somewhat of a successor to the nano editor by being easy to install and use.
|
||||
It strives to be enjoyable as a full-time editor for people who prefer to work in a terminal, or those who regularly edit files over SSH.
|
||||
|
||||
Here is a picture of micro editing its source code.
|
||||
|
||||

|
||||
|
||||
To see more screenshots of micro, showcasing all of the default colorschemes, see [here](http://zbyedidia.webfactional.com/micro/screenshots.html).
|
||||
To see more screenshots of micro, showcasing some of the default color schemes, see [here](http://zbyedidia.webfactional.com/micro/screenshots.html).
|
||||
|
||||
You can also check out the website for Micro at https://micro-editor.github.io.
|
||||
|
||||
# Features
|
||||
## Table of Contents
|
||||
|
||||
* Easy to use and to install
|
||||
* No dependencies or external files are needed -- just the binary you can download further down the page
|
||||
* Common keybindings (ctrl-s, ctrl-c, ctrl-v, ctrl-z...)
|
||||
* Keybindings can be rebound to your liking
|
||||
* Sane defaults
|
||||
* You shouldn't have to configure much out of the box (and it is extremely easy to configure)
|
||||
* Splits and tabs
|
||||
* Extremely good mouse support
|
||||
* This means mouse dragging to create a selection, double click to select by word, and triple click to select by line
|
||||
* Cross platform (It should work on all the platforms Go runs on)
|
||||
* Note that while Windows is supported, there are still some bugs that need to be worked out
|
||||
* Plugin system (plugins are written in Lua)
|
||||
* Persistent undo
|
||||
* Automatic linting and error notifications
|
||||
* Syntax highlighting (for over [75 languages](runtime/syntax)!)
|
||||
* Colorscheme support
|
||||
* By default, micro comes with 16, 256, and true color themes.
|
||||
* True color support (set the `MICRO_TRUECOLOR` env variable to 1 to enable it)
|
||||
* Copy and paste with the system clipboard
|
||||
* Small and simple
|
||||
* Easily configurable
|
||||
* Common editor things such as undo/redo, line numbers, unicode support...
|
||||
- [Features](#features)
|
||||
- [Installation](#installation)
|
||||
- [Prebuilt binaries](#prebuilt-binaries)
|
||||
- [Package Managers](#package-managers)
|
||||
- [Building from source](#building-from-source)
|
||||
- [Fully static binary](#fully-static-binary)
|
||||
- [macOS terminal](#macos-terminal)
|
||||
- [Linux clipboard support](#linux-clipboard-support)
|
||||
- [Colors and syntax highlighting](#colors-and-syntax-highlighting)
|
||||
- [Plan9, Cygwin, Mingw](#plan9-cygwin-mingw)
|
||||
- [Usage](#usage)
|
||||
- [Documentation and Help](#documentation-and-help)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
Although not yet implemented, I hope to add more features such as autocompletion, and multiple cursors in the future.
|
||||
- - -
|
||||
|
||||
# Installation
|
||||
## Features
|
||||
|
||||
- Easy to use and install.
|
||||
- No dependencies or external files are needed — just the binary you can download further down the page.
|
||||
- Multiple cursors.
|
||||
- Common keybindings (<kbd>Ctrl+S</kbd>, <kbd>Ctrl+C</kbd>, <kbd>Ctrl+V</kbd>, <kbd>Ctrl+Z</kbd>, …).
|
||||
- Keybindings can be rebound to your liking.
|
||||
- Sane defaults.
|
||||
- You shouldn't have to configure much out of the box (and it is extremely easy to configure).
|
||||
- Splits and tabs.
|
||||
- nano-like menu to help you remember the keybindings.
|
||||
- Extremely good mouse support.
|
||||
- This means mouse dragging to create a selection, double click to select by word, and triple click to select by line.
|
||||
- Cross-platform (it should work on all the platforms Go runs on).
|
||||
- Note that while Windows is supported Mingw/Cygwin is not (see below)
|
||||
- Plugin system (plugins are written in Lua).
|
||||
- micro has a built-in plugin manager to automatically install, remove, and update plugins.
|
||||
- Built-in diff gutter
|
||||
- Simple autocompletion
|
||||
- Persistent undo.
|
||||
- Automatic linting and error notifications
|
||||
- Syntax highlighting for over [130 languages](runtime/syntax).
|
||||
- Color scheme support.
|
||||
- By default, micro comes with 16, 256, and true color themes.
|
||||
- True color support (set the `MICRO_TRUECOLOR` environment variable to 1 to enable it).
|
||||
- Copy and paste with the system clipboard.
|
||||
- Small and simple.
|
||||
- Easily configurable.
|
||||
- Macros.
|
||||
- Common editor features such as undo/redo, line numbers, Unicode support, soft wrapping, …
|
||||
|
||||
## Installation
|
||||
|
||||
To install micro, you can download a [prebuilt binary](https://github.com/zyedidia/micro/releases), or you can build it from source.
|
||||
|
||||
If you want more information about ways to install micro, see this [wiki page](https://github.com/zyedidia/micro/wiki/Installing-Micro)
|
||||
If you want more information about ways to install micro, see this [wiki page](https://github.com/zyedidia/micro/wiki/Installing-Micro).
|
||||
|
||||
### Prebuilt binaries
|
||||
|
||||
@@ -59,48 +85,132 @@ Download the binary from the [releases](https://github.com/zyedidia/micro/releas
|
||||
On that page you'll see the nightly release, which contains binaries for micro which are built every night,
|
||||
and you'll see all the stable releases with the corresponding binaries.
|
||||
|
||||
If you'd like to see more information after installing micro, run `micro -version`.
|
||||
Running `micro -version` will give you the version information.
|
||||
|
||||
### Installation script
|
||||
|
||||
There is a script which can install micro for you by downloading the latest prebuilt binary. You can find it at <https://getmic.ro>.
|
||||
|
||||
You can easily install micro by running
|
||||
|
||||
```bash
|
||||
curl https://getmic.ro | bash
|
||||
```
|
||||
|
||||
The script will install the micro binary to the current directory. See its [GitHub repository](https://github.com/benweissmann/getmic.ro) for more information.
|
||||
|
||||
### Package managers
|
||||
|
||||
You can install micro using Homebrew on Mac:
|
||||
|
||||
```
|
||||
brew install micro
|
||||
```
|
||||
|
||||
On Linux, you can install micro through [snap](https://snapcraft.io/docs/core/install)
|
||||
|
||||
```
|
||||
snap install micro --classic
|
||||
```
|
||||
|
||||
On Debian `unstable | testing | buster-backports` and Ubuntu `focal` (20.04), micro is available
|
||||
via `apt`:
|
||||
|
||||
```
|
||||
sudo apt install micro
|
||||
```
|
||||
|
||||
Micro is also available through other package managers on Linux such as AUR, Nix, and package managers
|
||||
for other operating systems:
|
||||
|
||||
* Windows: [Chocolatey](https://chocolatey.org) and [Scoop](https://github.com/lukesampson/scoop)
|
||||
* `choco install micro`
|
||||
* `scoop install micro`
|
||||
* OpenBSD: Available in the ports tree and also available as a binary package
|
||||
* `pkd_add -v micro`
|
||||
* Arch Linux, CRUX, Termux for Android
|
||||
* See details in the [wiki page](https://github.com/zyedidia/micro/wiki/Installing-Micro)
|
||||
|
||||
### Building from source
|
||||
|
||||
If your operating system does not have binary, but does run Go, you can build from source.
|
||||
If your operating system does not have a binary release, but does run Go, you can build from source.
|
||||
|
||||
Make sure that you have Go version 1.5 or greater (Go 1.4 will work if your version supports CGO).
|
||||
Make sure that you have Go version 1.11 or greater and Go modules are enabled.
|
||||
|
||||
```sh
|
||||
go get -u github.com/zyedidia/micro/...
|
||||
```
|
||||
git clone https://github.com/zyedidia/micro
|
||||
cd micro
|
||||
make build
|
||||
sudo mv micro /usr/local/bin # optional
|
||||
```
|
||||
|
||||
The binary will be placed in the current directory and can be moved to
|
||||
anywhere you like (for example `/usr/local/bin`).
|
||||
|
||||
The command `make install` will install the binary to `$GOPATH/bin` or `$GOBIN`.
|
||||
|
||||
You can install directly with `go get` (`go get github.com/zyedidia/micro/cmd/micro`) but this isn't
|
||||
recommended because it doesn't build micro with version information (necessary for the plugin manager),
|
||||
and doesn't disable debug mode.
|
||||
|
||||
### Fully static binary
|
||||
|
||||
By default, the micro binary will dynamically link with core system libraries (this is generally
|
||||
recommended for security and portability). However, there is a fully static prebuilt binary that
|
||||
is provided for amd64 as `linux-static.tar.gz`, and to build a fully static binary from source, run
|
||||
|
||||
```
|
||||
CGO_ENABLED=0 make build
|
||||
```
|
||||
|
||||
### macOS terminal
|
||||
|
||||
If you are using macOS, you should consider using [iTerm2](http://iterm2.com/) instead of the default terminal (Terminal.app). The iTerm2 terminal has much better mouse support as well as better handling of key events. For best keybinding behavior, choose `xterm defaults` under `Preferences->Profiles->Keys->Load Preset`. The newest versions also support true color.
|
||||
|
||||
If you still insist on using the default Mac terminal, be sure to set `Use Option key as Meta key` under
|
||||
`Preferences->Profiles->Keyboard` to use <kbd>option</kbd> as <kbd>alt</kbd>.
|
||||
|
||||
### Linux clipboard support
|
||||
|
||||
On Linux, clipboard support requires 'xclip' or 'xsel' command to be installed.
|
||||
On Linux, clipboard support requires:
|
||||
|
||||
For Ubuntu:
|
||||
- On X11, the `xclip` or `xsel` commands (for Ubuntu: `sudo apt install xclip`)
|
||||
- On Wayland, the `wl-clipboard` command
|
||||
|
||||
```sh
|
||||
sudo apt-get install xclip
|
||||
```
|
||||
If you don't have these commands, micro will use an internal clipboard for copy and paste, but it won't work with external applications.
|
||||
|
||||
If you don't have xclip or xsel, micro will use an internal clipboard for copy and paste, but it won't work with external applications.
|
||||
### Colors and syntax highlighting
|
||||
|
||||
### Windows colors
|
||||
If you open micro and it doesn't seem like syntax highlighting is working, this is probably because
|
||||
you are using a terminal which does not support 256 color mode. Try changing the color scheme to `simple`
|
||||
by pressing <kbd>Ctrl+E</kbd> in micro and typing `set colorscheme simple`.
|
||||
|
||||
If you are using the default Ubuntu terminal, to enable 256 make sure your `TERM` variable is set
|
||||
to `xterm-256color`.
|
||||
|
||||
Many of the Windows terminals don't support more than 16 colors, which means
|
||||
that micro's default colorscheme won't look very good. You can either set
|
||||
the colorscheme to `simple`, or download a better terminal emulator, like
|
||||
mintty or cmder.
|
||||
that micro's default color scheme won't look very good. You can either set
|
||||
the color scheme to `simple`, or download and configure a better terminal emulator
|
||||
than the Windows default.
|
||||
|
||||
### Plan9, NaCl, Cygwin
|
||||
### Cygwin, Mingw, Plan9
|
||||
|
||||
Please note that micro uses the amazing [tcell library](https://github.com/gdamore/tcell), but this
|
||||
Cygwin, Mingw, and Plan9 are unfortunately not officially supported. In Cygwin and Mingw, micro will often work when run using
|
||||
the `winpty` utility:
|
||||
|
||||
```
|
||||
winpty micro.exe ...
|
||||
```
|
||||
|
||||
Micro uses the amazing [tcell library](https://github.com/gdamore/tcell), but this
|
||||
means that micro is restricted to the platforms tcell supports. As a result, micro does not support
|
||||
Plan9, NaCl, and Cygwin (although this may change in the future).
|
||||
Plan9, and Cygwin (although this may change in the future). Micro also doesn't support NaCl (which is deprecated anyway).
|
||||
|
||||
# Usage
|
||||
## Usage
|
||||
|
||||
Once you have built the editor, simply start it by running `micro path/to/file.txt` or simply `micro` to open an empty buffer.
|
||||
Once you have built the editor, start it by running `micro path/to/file.txt` or `micro` to open an empty buffer.
|
||||
|
||||
Micro also supports creating buffers from `stdin`:
|
||||
micro also supports creating buffers from `stdin`:
|
||||
|
||||
```sh
|
||||
ifconfig | micro
|
||||
@@ -112,16 +222,26 @@ You can also use the mouse to manipulate the text. Simply clicking and dragging
|
||||
will select text. You can also double click to enable word selection, and triple
|
||||
click to enable line selection.
|
||||
|
||||
# Documentation and Help
|
||||
## Documentation and Help
|
||||
|
||||
Micro has a built-in help system which you can access by pressing `CtrlE` and typing `help`. Additionally, you can
|
||||
view the help files online [here](https://github.com/zyedidia/micro/tree/master/runtime/help).
|
||||
micro has a built-in help system which you can access by pressing <kbd>Ctrl+E</kbd> and typing `help`. Additionally, you can
|
||||
view the help files here:
|
||||
|
||||
- [main help](https://github.com/zyedidia/micro/tree/master/runtime/help/help.md)
|
||||
- [keybindings](https://github.com/zyedidia/micro/tree/master/runtime/help/keybindings.md)
|
||||
- [commands](https://github.com/zyedidia/micro/tree/master/runtime/help/commands.md)
|
||||
- [colors](https://github.com/zyedidia/micro/tree/master/runtime/help/colors.md)
|
||||
- [options](https://github.com/zyedidia/micro/tree/master/runtime/help/options.md)
|
||||
- [plugins](https://github.com/zyedidia/micro/tree/master/runtime/help/plugins.md)
|
||||
|
||||
I also recommend reading the [tutorial](https://github.com/zyedidia/micro/tree/master/runtime/help/tutorial.md) for
|
||||
a brief introduction to the more powerful configuration features micro offers.
|
||||
|
||||
# Contributing
|
||||
## Contributing
|
||||
|
||||
If you find any bugs, please report them! I am also happy to accept pull requests from anyone.
|
||||
|
||||
You can use the Github issue tracker to report bugs, ask questions, or suggest new features.
|
||||
You can use the [GitHub issue tracker](https://github.com/zyedidia/micro/issues)
|
||||
to report bugs, ask questions, or suggest new features.
|
||||
|
||||
For a more informal setting to discuss the editor, you can join the [Gitter chat](https://gitter.im/zyedidia/micro).
|
||||
|
||||
BIN
assets/logo.png
BIN
assets/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 8.5 KiB |
70
assets/micro-logo.svg
Normal file
70
assets/micro-logo.svg
Normal file
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 299.89999 103.2"
|
||||
enable-background="new 0 0 960 560"
|
||||
xml:space="preserve"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="micro-logo.svg"
|
||||
width="299.89999"
|
||||
height="103.2"><metadata
|
||||
id="metadata21"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs19" /><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1237"
|
||||
inkscape:window-height="867"
|
||||
id="namedview17"
|
||||
showgrid="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:zoom="1.1416667"
|
||||
inkscape:cx="75.655934"
|
||||
inkscape:cy="-4"
|
||||
inkscape:window-x="1097"
|
||||
inkscape:window-y="185"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="Layer_1" /><g
|
||||
id="g3"
|
||||
transform="translate(-178,-172.8)"><path
|
||||
d="m 306.8,213.8 0,-2.6 c 1.6,-0.1 2.9,-0.4 4.1,-0.8 1.2,-0.4 2.5,-1 4,-1.8 l 2.3,0 0,5.2 c 2.4,-1.9 4.2,-3.1 5.5,-3.8 2,-1 4,-1.5 5.8,-1.5 1.3,0 2.5,0.2 3.7,0.7 1.2,0.5 2.2,1 2.9,1.7 0.7,0.7 1.4,1.6 1.9,2.8 2.2,-1.9 4.2,-3.3 6,-4 1.9,-0.8 3.7,-1.2 5.6,-1.2 1.8,0 3.4,0.4 4.8,1.1 1.4,0.8 2.4,1.7 3,2.8 0.6,1.1 0.9,2.8 0.9,5 l 0,14.4 c 0,1.5 0,2.4 0.1,2.6 0.1,0.4 0.3,0.8 0.7,1.1 0.3,0.4 0.7,0.6 1.2,0.7 0.4,0.1 1.2,0.2 2.4,0.2 l 1,0 0,2.6 -15.5,0 0,-2.6 c 1.8,0 2.9,-0.1 3.5,-0.4 0.5,-0.2 0.9,-0.6 1.2,-1.2 0.3,-0.6 0.4,-1.6 0.4,-3.2 l 0,-13.7 c 0,-1.7 -0.2,-2.9 -0.5,-3.6 -0.3,-0.7 -0.9,-1.2 -1.7,-1.7 -0.8,-0.4 -1.8,-0.7 -3,-0.7 -1.5,0 -3,0.4 -4.6,1.2 -2.2,1.1 -3.9,2.3 -5.1,3.6 l 0,14.8 c 0,1.4 0.1,2.4 0.2,2.8 0.1,0.4 0.4,0.8 0.7,1.1 0.3,0.3 0.7,0.5 1.1,0.6 0.4,0.1 1.5,0.2 3.1,0.2 l 0,2.6 -15.3,0 0,-2.6 0.9,0 c 1.2,0 2.1,-0.1 2.6,-0.4 0.5,-0.3 0.9,-0.7 1.2,-1.3 0.2,-0.5 0.3,-1.5 0.3,-2.9 l 0,-13.2 c 0,-1.9 -0.2,-3.3 -0.5,-3.9 -0.3,-0.7 -0.9,-1.3 -1.7,-1.7 -0.8,-0.5 -1.8,-0.7 -3,-0.7 -1.3,0 -2.7,0.3 -4.1,1 -2,1 -3.9,2.2 -5.6,3.8 l 0,15.9 c 0,1 0.1,1.6 0.4,2.1 0.3,0.4 0.7,0.8 1.2,1.1 0.6,0.3 1.3,0.4 2.3,0.4 l 1.1,0 0,2.6 -15.6,0 0,-2.6 0.8,0 c 1.4,0 2.4,-0.1 2.8,-0.3 0.7,-0.3 1.1,-0.8 1.4,-1.5 0.2,-0.4 0.2,-1.3 0.2,-2.9 l 0,-18.1 -5.1,0 z"
|
||||
id="path5"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 366.4,213.7 0,-2.6 c 1.7,-0.2 3.2,-0.5 4.3,-0.9 1.2,-0.4 2.5,-1 4,-1.7 l 2.3,0 0,24.9 c 0,0.9 0.1,1.5 0.4,2 0.2,0.4 0.6,0.8 1,0.9 0.4,0.2 1.3,0.3 2.4,0.3 l 1.5,0 0,2.6 -15.9,0 0,-2.6 1.3,0 c 1.4,0 2.3,-0.1 2.8,-0.4 0.5,-0.2 0.8,-0.6 1,-1.1 0.2,-0.5 0.3,-1.5 0.3,-3.2 l 0,-18.3 -5.4,0 z m 7.9,-19.2 c 1,0 1.8,0.3 2.5,1 0.7,0.7 1.1,1.5 1.1,2.5 0,1 -0.4,1.8 -1.1,2.5 -0.7,0.7 -1.6,1.1 -2.5,1.1 -1,0 -1.8,-0.4 -2.5,-1.1 -0.7,-0.7 -1.1,-1.6 -1.1,-2.5 0,-1 0.4,-1.8 1.1,-2.5 0.6,-0.6 1.5,-1 2.5,-1 z"
|
||||
id="path7"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 413.1,230.6 2,1.6 c -3.9,5.2 -8.6,7.8 -14,7.8 -4.2,0 -7.8,-1.5 -10.7,-4.5 -2.9,-3 -4.4,-6.8 -4.4,-11.3 0,-3 0.7,-5.7 2,-8.1 1.3,-2.4 3.2,-4.2 5.6,-5.6 2.4,-1.3 5.2,-2 8.3,-2 3.6,0 6.5,0.9 8.9,2.6 2.4,1.7 3.6,3.5 3.6,5.3 0,1 -0.3,1.7 -0.8,2.2 -0.5,0.5 -1.2,0.8 -1.9,0.8 -0.4,0 -0.7,-0.1 -1.1,-0.3 -0.4,-0.2 -0.7,-0.5 -1.1,-0.9 -0.2,-0.2 -0.5,-0.8 -0.9,-1.7 -0.6,-1.2 -1,-2 -1.3,-2.4 -0.6,-0.8 -1.4,-1.5 -2.4,-2 -0.9,-0.5 -2,-0.7 -3.1,-0.7 -1.8,0 -3.4,0.5 -4.9,1.5 -1.5,1 -2.7,2.4 -3.6,4.3 -0.9,1.9 -1.3,4.2 -1.3,6.8 0,4.1 1.1,7.3 3.3,9.7 1.9,2.1 4.1,3.1 6.7,3.1 1.2,0 2.4,-0.2 3.6,-0.6 1.2,-0.4 2.4,-1 3.5,-1.8 0.9,-0.6 2.2,-1.8 4,-3.8 z"
|
||||
id="path9"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 418.7,213.7 0,-2.6 c 1.5,-0.1 2.8,-0.4 4,-0.8 1.2,-0.4 2.5,-1 4,-1.9 l 2.3,0 0,5.9 c 1.5,-1.8 3.2,-3.2 5.1,-4.3 1.9,-1.1 3.7,-1.6 5.2,-1.6 1.5,0 2.7,0.4 3.6,1.1 0.9,0.7 1.3,1.6 1.3,2.6 0,0.7 -0.3,1.4 -0.9,2 -0.6,0.6 -1.3,0.9 -2.1,0.9 -0.4,0 -0.7,-0.1 -1,-0.2 -0.3,-0.1 -0.7,-0.3 -1.2,-0.7 -1.1,-0.7 -2.1,-1.1 -2.9,-1.1 -1,0 -2.2,0.4 -3.4,1.3 -1.6,1.1 -2.8,2.2 -3.7,3.3 l 0,14.4 c 0,1.2 0.1,2.1 0.2,2.5 0.1,0.4 0.4,0.8 0.7,1.1 0.3,0.3 0.7,0.6 1.1,0.7 0.5,0.1 1.3,0.2 2.4,0.2 l 1,0 0,2.6 -16,0 0,-2.6 1.3,0 c 1.3,0 2.1,-0.1 2.5,-0.3 0.5,-0.3 0.9,-0.7 1.2,-1.3 0.3,-0.5 0.4,-1.5 0.4,-3 l 0,-18.3 -5.1,0 z"
|
||||
id="path11"
|
||||
inkscape:connector-curvature="0" /><path
|
||||
d="m 462.8,208.5 c 3,0 5.7,0.6 7.9,1.9 2.2,1.3 4,3.1 5.3,5.5 1.3,2.4 1.9,5.2 1.9,8.3 0,3.1 -0.7,5.9 -2,8.3 -1.3,2.4 -3.1,4.3 -5.4,5.5 -2.3,1.3 -5,1.9 -8.1,1.9 -5,0 -8.8,-1.6 -11.3,-4.7 -2.5,-3.1 -3.8,-6.8 -3.8,-11 0,-3.1 0.7,-5.8 2,-8.2 1.3,-2.4 3.1,-4.2 5.5,-5.6 2.4,-1.2 5.1,-1.9 8,-1.9 z m -0.2,3 c -2.4,0 -4.4,0.9 -6,2.8 -2.1,2.3 -3.1,5.7 -3.1,10.1 0,4.3 0.9,7.5 2.6,9.7 1.6,2 3.8,3 6.5,3 1.8,0 3.3,-0.5 4.7,-1.4 1.4,-0.9 2.5,-2.4 3.3,-4.5 0.8,-2 1.3,-4.4 1.3,-7.2 0,-2.7 -0.5,-5.1 -1.4,-7.2 -0.7,-1.7 -1.8,-3 -3.2,-4 -1.3,-0.8 -2.9,-1.3 -4.7,-1.3 z"
|
||||
id="path13"
|
||||
inkscape:connector-curvature="0" /></g><path
|
||||
d="M 51.6,0 C 23.1,0 0,23.1 0,51.6 c 0,28.5 23.1,51.6 51.6,51.6 28.5,0 51.6,-23.1 51.6,-51.6 C 103.2,23.1 80.1,0 51.6,0 Z m 24.5,58.6 c -0.5,2 -1.3,3.6 -2.4,4.9 -1,1.3 -2,2.1 -3.1,2.5 -1.1,0.4 -2.2,0.6 -3.4,0.6 -1.2,0 -2.2,-0.2 -3,-0.7 C 63.4,65.5 62.8,64.8 62.3,64 61.8,63.2 61.5,62.2 61.3,61.1 61.1,60 61,58.8 61,57.5 c 0,-0.5 0,-1 0.1,-1.7 0.1,-0.7 0.2,-1.6 0.3,-1.6 l -0.2,0 c -1.6,4 -3.8,6.9 -6.6,9.2 -2.8,2.3 -5.9,3.4 -9.3,3.4 -2.3,0 -4.2,-0.9 -5.5,-2.6 -1.4,-1.7 -2.1,-4.3 -2.1,-7.7 0,-0.5 0,-1 0.1,-1.6 0.1,-0.5 0.1,-0.7 0.2,-1.7 l -0.7,0 c -0.9,2 -1.7,4.8 -2.3,7.3 -0.6,2.5 -1.1,4.8 -1.4,6.9 -0.4,2.1 -0.6,4 -0.8,5.6 -0.2,1.6 -0.3,2.7 -0.4,3.3 0.1,0.5 0.2,1 0.3,1.6 0.2,0.6 0.3,1.2 0.5,1.7 0.2,0.5 0.3,1.1 0.4,1.6 0.1,0.5 0.2,0.9 0.2,1.2 0,1.4 -0.3,2.5 -0.9,3.2 -0.6,0.7 -1.3,1.1 -2,1.1 -0.9,0 -1.7,-0.3 -2.3,-0.8 -0.7,-0.6 -1,-1.5 -1,-2.7 0,-1.7 0.3,-3.9 0.9,-6.5 0.6,-2.6 1.5,-5.9 2.6,-9.8 0.6,-1.8 1.1,-3.6 1.7,-5.4 0.6,-1.8 1.1,-3.5 1.6,-5 0.5,-1.5 0.9,-2.9 1.3,-4.1 0.4,-1.2 0.6,-2.1 0.7,-2.8 0.1,-0.3 0.2,-1 0.3,-2 0.1,-1 0.2,-2.1 0.4,-3.4 0.2,-1.3 0.3,-2.7 0.5,-4.1 0.2,-1.5 0.4,-2.8 0.5,-4 0.2,-0.9 0.3,-1.9 0.5,-3 0.2,-1.1 0.5,-2.2 0.9,-3.1 0.4,-1 1,-1.8 1.7,-2.5 0.7,-0.7 1.6,-1 2.7,-1 1.2,0 2,0.4 2.4,1.1 0.4,0.7 0.6,1.6 0.5,2.6 -0.1,1 -0.2,2.1 -0.5,3.2 -0.3,1.1 -0.6,2.1 -0.9,2.9 -0.8,2.5 -1.6,4.8 -2.5,6.7 -0.9,1.9 -1.7,4 -2.4,6.2 -0.6,1.5 -0.8,2.9 -0.8,4.1 0,2.2 0.7,3.8 2,5 1.4,1.2 3,1.7 4.9,1.7 1.5,0 3,-0.5 4.4,-1.6 1.4,-1.1 2.7,-2.4 3.9,-3.9 1.2,-1.5 2.2,-3.1 3,-4.9 0.8,-1.7 1.4,-3.3 1.8,-4.6 0.1,-0.2 0.2,-0.6 0.3,-1.4 0.2,-0.8 0.3,-1.7 0.5,-2.7 0.2,-1 0.4,-2 0.6,-3.1 0.2,-1.1 0.4,-2 0.5,-2.7 0.2,-0.8 0.3,-1.6 0.5,-2.6 0.2,-1 0.5,-1.9 0.9,-2.8 0.4,-0.9 1,-1.6 1.6,-2.2 0.7,-0.6 1.5,-0.9 2.6,-0.9 1.3,0 2.1,0.4 2.6,1.1 0.4,0.7 0.6,1.6 0.6,2.6 -0.1,1 -0.2,2 -0.5,3 -0.3,1 -0.5,1.8 -0.7,2.4 -0.8,2.5 -1.6,4.7 -2.4,6.7 -0.8,2 -1.5,3.8 -2.2,5.2 -0.6,1.5 -1.1,2.6 -1.5,3.5 -0.4,0.9 -0.6,1.5 -0.6,1.8 0,2.6 0.6,4.5 1.7,5.6 1.1,1.1 2.3,1.7 3.6,1.7 2.2,0 3.9,-0.7 5.2,-2 1.3,-1.4 2.3,-3.9 2.9,-6.9 l 0.8,0 c 0.2,2.9 -0.1,5.3 -0.6,7.3 z"
|
||||
id="path15"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2e3192" /></svg>
|
||||
|
After Width: | Height: | Size: 7.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 253 KiB |
123
assets/packaging/micro.1
Normal file
123
assets/packaging/micro.1
Normal file
@@ -0,0 +1,123 @@
|
||||
.TH micro 1 "2020-02-10"
|
||||
.SH NAME
|
||||
micro \- A modern and intuitive terminal-based text editor
|
||||
.SH SYNOPSIS
|
||||
.B micro
|
||||
.RB [OPTIONS]
|
||||
[FILE]\&...
|
||||
|
||||
.SH DESCRIPTION
|
||||
|
||||
Micro is a terminal-based text editor that aims to be easy to use and intuitive, while also taking advantage of the full capabilities
|
||||
of modern terminals. It comes as one single, batteries-included, static binary with no dependencies.
|
||||
|
||||
As the name indicates, micro aims to be somewhat of a successor to the nano editor by being easy to install and use in a pinch, but micro also aims to be
|
||||
enjoyable to use full time, whether you work in the terminal because you prefer it (like me), or because you need to (over ssh).
|
||||
|
||||
.SH OPTIONS
|
||||
.PP
|
||||
\-clean
|
||||
.RS 4
|
||||
Cleans the configuration directory
|
||||
.RE
|
||||
|
||||
.PP
|
||||
\-config-dir dir
|
||||
.RS 4
|
||||
Specify a custom location for the configuration directory
|
||||
.RE
|
||||
|
||||
.PP
|
||||
[FILE]:LINE:COL
|
||||
.RS 4
|
||||
Specify a line and column to start the cursor at when opening a buffer
|
||||
.RE
|
||||
|
||||
.PP
|
||||
\-options
|
||||
.RS 4
|
||||
Show all option help
|
||||
.RE
|
||||
|
||||
.PP
|
||||
\-debug
|
||||
.RS 4
|
||||
Enable debug mode (enables logging to ./log.txt)
|
||||
.RE
|
||||
|
||||
.PP
|
||||
\-version
|
||||
.RS 4
|
||||
Show the version number and information
|
||||
.RE
|
||||
|
||||
Micro's plugin's can be managed at the command line with the following commands.
|
||||
.RS 4
|
||||
|
||||
.PP
|
||||
\-plugin remove [PLUGIN]...
|
||||
.RS 4
|
||||
Remove plugin(s)
|
||||
.RE
|
||||
|
||||
.PP
|
||||
\-plugin update [PLUGIN]...
|
||||
.RS 4
|
||||
Update plugin(s) (if no argument is given, updates all plugins)
|
||||
.RE
|
||||
|
||||
.PP
|
||||
\-plugin search [PLUGIN]...
|
||||
.RS 4
|
||||
Search for a plugin
|
||||
.RE
|
||||
|
||||
.PP
|
||||
\-plugin list
|
||||
.RS 4
|
||||
List installed plugins
|
||||
.RE
|
||||
|
||||
.PP
|
||||
\-plugin available
|
||||
.RS 4
|
||||
List available plugins
|
||||
.RE
|
||||
.RE
|
||||
|
||||
Micro's options can also be set via command line arguments for quick
|
||||
adjustments. For real configuration, please use the settings.json
|
||||
file (see 'help options').
|
||||
.RS 4
|
||||
|
||||
.PP
|
||||
\-option value
|
||||
.RS 4
|
||||
Set `option` to `value` for this session
|
||||
For example: `micro -syntax off file.c`
|
||||
.RE
|
||||
|
||||
|
||||
.SH CONFIGURATION
|
||||
|
||||
Micro uses $MICRO_CONFIG_HOME as the configuration directory.
|
||||
If this environment variable is not set, it uses $XDG_CONFIG_HOME/micro instead.
|
||||
If that environment variable is not set, it uses ~/.config/micro as the configuration directory.
|
||||
In the documentation, we use ~/.config/micro to refer to the configuration directory
|
||||
(even if it may in fact be somewhere else if you have set either of the above environment variables).
|
||||
|
||||
.SH NOTICE
|
||||
This manpage is intended only to serve as a quick guide to the invocation of
|
||||
micro and is not intended to replace the full documentation included with micro
|
||||
which can be accessed from within micro. Micro tells you what key combination to
|
||||
press to get help in the lower right.
|
||||
|
||||
.SH BUGS
|
||||
A comprehensive list of bugs will not be listed in this manpage. See the Github
|
||||
page at \fBhttps://github.com/zyedidia/micro/issues\fP for a list of known bugs
|
||||
and to report any newly encountered bugs you may find. We strive to correct
|
||||
bugs as swiftly as possible.
|
||||
|
||||
.SH COPYRIGHT
|
||||
Copyright \(co 2020 Zachary Yedidia, et al.
|
||||
See /usr/share/doc/micro/LICENSE and /usr/share/doc/micro/AUTHORS for more information.
|
||||
15
assets/packaging/micro.desktop
Normal file
15
assets/packaging/micro.desktop
Normal file
@@ -0,0 +1,15 @@
|
||||
[Desktop Entry]
|
||||
|
||||
Name=Micro
|
||||
GenericName=Text Editor
|
||||
Comment=Edit text files in a terminal
|
||||
|
||||
Icon=micro
|
||||
Type=Application
|
||||
Categories=terminal;TextEditor;
|
||||
Keywords=text;editor;syntax;terminal;
|
||||
|
||||
Exec=micro %U
|
||||
StartupNotify=false
|
||||
Terminal=true
|
||||
MimeType=text/plain;text/x-chdr;text/x-csrc;text/x-c++hdr;text/x-c++src;text/x-java;text/x-dsrc;text/x-pascal;text/x-perl;text/x-python;application/x-php;application/x-httpd-php3;application/x-httpd-php4;application/x-httpd-php5;application/xml;text/html;text/css;text/x-sql;text/x-diff;
|
||||
1426
cmd/micro/actions.go
1426
cmd/micro/actions.go
File diff suppressed because it is too large
Load Diff
@@ -1,124 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
// This file is meant (for now) for autocompletion in command mode, not
|
||||
// while coding. This helps micro autocomplete commands and then filenames
|
||||
// for example with `vsplit filename`.
|
||||
|
||||
// FileComplete autocompletes filenames
|
||||
func FileComplete(input string) (string, []string) {
|
||||
dirs := strings.Split(input, "/")
|
||||
var files []os.FileInfo
|
||||
var err error
|
||||
if len(dirs) > 1 {
|
||||
home, _ := homedir.Dir()
|
||||
|
||||
directories := strings.Join(dirs[:len(dirs)-1], "/")
|
||||
if strings.HasPrefix(directories, "~") {
|
||||
directories = strings.Replace(directories, "~", home, 1)
|
||||
}
|
||||
files, err = ioutil.ReadDir(directories)
|
||||
} else {
|
||||
files, err = ioutil.ReadDir(".")
|
||||
}
|
||||
var suggestions []string
|
||||
if err != nil {
|
||||
return "", suggestions
|
||||
}
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
if f.IsDir() {
|
||||
name += "/"
|
||||
}
|
||||
if strings.HasPrefix(name, dirs[len(dirs)-1]) {
|
||||
suggestions = append(suggestions, name)
|
||||
}
|
||||
}
|
||||
|
||||
var chosen string
|
||||
if len(suggestions) == 1 {
|
||||
if len(dirs) > 1 {
|
||||
chosen = strings.Join(dirs[:len(dirs)-1], "/") + "/" + suggestions[0]
|
||||
} else {
|
||||
chosen = suggestions[0]
|
||||
}
|
||||
} else {
|
||||
if len(dirs) > 1 {
|
||||
chosen = strings.Join(dirs[:len(dirs)-1], "/") + "/"
|
||||
}
|
||||
}
|
||||
|
||||
return chosen, suggestions
|
||||
}
|
||||
|
||||
// CommandComplete autocompletes commands
|
||||
func CommandComplete(input string) (string, []string) {
|
||||
var suggestions []string
|
||||
for cmd := range commands {
|
||||
if strings.HasPrefix(cmd, input) {
|
||||
suggestions = append(suggestions, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
var chosen string
|
||||
if len(suggestions) == 1 {
|
||||
chosen = suggestions[0]
|
||||
}
|
||||
return chosen, suggestions
|
||||
}
|
||||
|
||||
// HelpComplete autocompletes help topics
|
||||
func HelpComplete(input string) (string, []string) {
|
||||
var suggestions []string
|
||||
|
||||
for _, topic := range helpFiles {
|
||||
if strings.HasPrefix(topic, input) {
|
||||
|
||||
suggestions = append(suggestions, topic)
|
||||
}
|
||||
}
|
||||
|
||||
var chosen string
|
||||
if len(suggestions) == 1 {
|
||||
chosen = suggestions[0]
|
||||
}
|
||||
return chosen, suggestions
|
||||
}
|
||||
|
||||
func contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OptionComplete autocompletes options
|
||||
func OptionComplete(input string) (string, []string) {
|
||||
var suggestions []string
|
||||
localSettings := DefaultLocalSettings()
|
||||
for option := range globalSettings {
|
||||
if strings.HasPrefix(option, input) {
|
||||
suggestions = append(suggestions, option)
|
||||
}
|
||||
}
|
||||
for option := range localSettings {
|
||||
if strings.HasPrefix(option, input) && !contains(suggestions, option) {
|
||||
suggestions = append(suggestions, option)
|
||||
}
|
||||
}
|
||||
|
||||
var chosen string
|
||||
if len(suggestions) == 1 {
|
||||
chosen = suggestions[0]
|
||||
}
|
||||
return chosen, suggestions
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
var bindings map[Key][]func(*View, bool) bool
|
||||
var helpBinding string
|
||||
|
||||
var bindingActions = map[string]func(*View, bool) bool{
|
||||
"CursorUp": (*View).CursorUp,
|
||||
"CursorDown": (*View).CursorDown,
|
||||
"CursorPageUp": (*View).CursorPageUp,
|
||||
"CursorPageDown": (*View).CursorPageDown,
|
||||
"CursorLeft": (*View).CursorLeft,
|
||||
"CursorRight": (*View).CursorRight,
|
||||
"CursorStart": (*View).CursorStart,
|
||||
"CursorEnd": (*View).CursorEnd,
|
||||
"SelectToStart": (*View).SelectToStart,
|
||||
"SelectToEnd": (*View).SelectToEnd,
|
||||
"SelectUp": (*View).SelectUp,
|
||||
"SelectDown": (*View).SelectDown,
|
||||
"SelectLeft": (*View).SelectLeft,
|
||||
"SelectRight": (*View).SelectRight,
|
||||
"WordRight": (*View).WordRight,
|
||||
"WordLeft": (*View).WordLeft,
|
||||
"SelectWordRight": (*View).SelectWordRight,
|
||||
"SelectWordLeft": (*View).SelectWordLeft,
|
||||
"DeleteWordRight": (*View).DeleteWordRight,
|
||||
"DeleteWordLeft": (*View).DeleteWordLeft,
|
||||
"SelectToStartOfLine": (*View).SelectToStartOfLine,
|
||||
"SelectToEndOfLine": (*View).SelectToEndOfLine,
|
||||
"InsertNewline": (*View).InsertNewline,
|
||||
"InsertSpace": (*View).InsertSpace,
|
||||
"Backspace": (*View).Backspace,
|
||||
"Delete": (*View).Delete,
|
||||
"InsertTab": (*View).InsertTab,
|
||||
"Save": (*View).Save,
|
||||
"Find": (*View).Find,
|
||||
"FindNext": (*View).FindNext,
|
||||
"FindPrevious": (*View).FindPrevious,
|
||||
"Center": (*View).Center,
|
||||
"Undo": (*View).Undo,
|
||||
"Redo": (*View).Redo,
|
||||
"Copy": (*View).Copy,
|
||||
"Cut": (*View).Cut,
|
||||
"CutLine": (*View).CutLine,
|
||||
"DuplicateLine": (*View).DuplicateLine,
|
||||
"DeleteLine": (*View).DeleteLine,
|
||||
"IndentSelection": (*View).IndentSelection,
|
||||
"OutdentSelection": (*View).OutdentSelection,
|
||||
"Paste": (*View).Paste,
|
||||
"SelectAll": (*View).SelectAll,
|
||||
"OpenFile": (*View).OpenFile,
|
||||
"Start": (*View).Start,
|
||||
"End": (*View).End,
|
||||
"PageUp": (*View).PageUp,
|
||||
"PageDown": (*View).PageDown,
|
||||
"HalfPageUp": (*View).HalfPageUp,
|
||||
"HalfPageDown": (*View).HalfPageDown,
|
||||
"StartOfLine": (*View).StartOfLine,
|
||||
"EndOfLine": (*View).EndOfLine,
|
||||
"ToggleHelp": (*View).ToggleHelp,
|
||||
"ToggleRuler": (*View).ToggleRuler,
|
||||
"JumpLine": (*View).JumpLine,
|
||||
"ClearStatus": (*View).ClearStatus,
|
||||
"ShellMode": (*View).ShellMode,
|
||||
"CommandMode": (*View).CommandMode,
|
||||
"Quit": (*View).Quit,
|
||||
"AddTab": (*View).AddTab,
|
||||
"PreviousTab": (*View).PreviousTab,
|
||||
"NextTab": (*View).NextTab,
|
||||
"NextSplit": (*View).NextSplit,
|
||||
"PreviousSplit": (*View).PreviousSplit,
|
||||
|
||||
// This was changed to InsertNewline but I don't want to break backwards compatibility
|
||||
"InsertEnter": (*View).InsertNewline,
|
||||
}
|
||||
|
||||
var bindingKeys = map[string]tcell.Key{
|
||||
"Up": tcell.KeyUp,
|
||||
"Down": tcell.KeyDown,
|
||||
"Right": tcell.KeyRight,
|
||||
"Left": tcell.KeyLeft,
|
||||
"UpLeft": tcell.KeyUpLeft,
|
||||
"UpRight": tcell.KeyUpRight,
|
||||
"DownLeft": tcell.KeyDownLeft,
|
||||
"DownRight": tcell.KeyDownRight,
|
||||
"Center": tcell.KeyCenter,
|
||||
"PageUp": tcell.KeyPgUp,
|
||||
"PageDown": tcell.KeyPgDn,
|
||||
"Home": tcell.KeyHome,
|
||||
"End": tcell.KeyEnd,
|
||||
"Insert": tcell.KeyInsert,
|
||||
"Delete": tcell.KeyDelete,
|
||||
"Help": tcell.KeyHelp,
|
||||
"Exit": tcell.KeyExit,
|
||||
"Clear": tcell.KeyClear,
|
||||
"Cancel": tcell.KeyCancel,
|
||||
"Print": tcell.KeyPrint,
|
||||
"Pause": tcell.KeyPause,
|
||||
"Backtab": tcell.KeyBacktab,
|
||||
"F1": tcell.KeyF1,
|
||||
"F2": tcell.KeyF2,
|
||||
"F3": tcell.KeyF3,
|
||||
"F4": tcell.KeyF4,
|
||||
"F5": tcell.KeyF5,
|
||||
"F6": tcell.KeyF6,
|
||||
"F7": tcell.KeyF7,
|
||||
"F8": tcell.KeyF8,
|
||||
"F9": tcell.KeyF9,
|
||||
"F10": tcell.KeyF10,
|
||||
"F11": tcell.KeyF11,
|
||||
"F12": tcell.KeyF12,
|
||||
"F13": tcell.KeyF13,
|
||||
"F14": tcell.KeyF14,
|
||||
"F15": tcell.KeyF15,
|
||||
"F16": tcell.KeyF16,
|
||||
"F17": tcell.KeyF17,
|
||||
"F18": tcell.KeyF18,
|
||||
"F19": tcell.KeyF19,
|
||||
"F20": tcell.KeyF20,
|
||||
"F21": tcell.KeyF21,
|
||||
"F22": tcell.KeyF22,
|
||||
"F23": tcell.KeyF23,
|
||||
"F24": tcell.KeyF24,
|
||||
"F25": tcell.KeyF25,
|
||||
"F26": tcell.KeyF26,
|
||||
"F27": tcell.KeyF27,
|
||||
"F28": tcell.KeyF28,
|
||||
"F29": tcell.KeyF29,
|
||||
"F30": tcell.KeyF30,
|
||||
"F31": tcell.KeyF31,
|
||||
"F32": tcell.KeyF32,
|
||||
"F33": tcell.KeyF33,
|
||||
"F34": tcell.KeyF34,
|
||||
"F35": tcell.KeyF35,
|
||||
"F36": tcell.KeyF36,
|
||||
"F37": tcell.KeyF37,
|
||||
"F38": tcell.KeyF38,
|
||||
"F39": tcell.KeyF39,
|
||||
"F40": tcell.KeyF40,
|
||||
"F41": tcell.KeyF41,
|
||||
"F42": tcell.KeyF42,
|
||||
"F43": tcell.KeyF43,
|
||||
"F44": tcell.KeyF44,
|
||||
"F45": tcell.KeyF45,
|
||||
"F46": tcell.KeyF46,
|
||||
"F47": tcell.KeyF47,
|
||||
"F48": tcell.KeyF48,
|
||||
"F49": tcell.KeyF49,
|
||||
"F50": tcell.KeyF50,
|
||||
"F51": tcell.KeyF51,
|
||||
"F52": tcell.KeyF52,
|
||||
"F53": tcell.KeyF53,
|
||||
"F54": tcell.KeyF54,
|
||||
"F55": tcell.KeyF55,
|
||||
"F56": tcell.KeyF56,
|
||||
"F57": tcell.KeyF57,
|
||||
"F58": tcell.KeyF58,
|
||||
"F59": tcell.KeyF59,
|
||||
"F60": tcell.KeyF60,
|
||||
"F61": tcell.KeyF61,
|
||||
"F62": tcell.KeyF62,
|
||||
"F63": tcell.KeyF63,
|
||||
"F64": tcell.KeyF64,
|
||||
"CtrlSpace": tcell.KeyCtrlSpace,
|
||||
"CtrlA": tcell.KeyCtrlA,
|
||||
"CtrlB": tcell.KeyCtrlB,
|
||||
"CtrlC": tcell.KeyCtrlC,
|
||||
"CtrlD": tcell.KeyCtrlD,
|
||||
"CtrlE": tcell.KeyCtrlE,
|
||||
"CtrlF": tcell.KeyCtrlF,
|
||||
"CtrlG": tcell.KeyCtrlG,
|
||||
"CtrlH": tcell.KeyCtrlH,
|
||||
"CtrlI": tcell.KeyCtrlI,
|
||||
"CtrlJ": tcell.KeyCtrlJ,
|
||||
"CtrlK": tcell.KeyCtrlK,
|
||||
"CtrlL": tcell.KeyCtrlL,
|
||||
"CtrlM": tcell.KeyCtrlM,
|
||||
"CtrlN": tcell.KeyCtrlN,
|
||||
"CtrlO": tcell.KeyCtrlO,
|
||||
"CtrlP": tcell.KeyCtrlP,
|
||||
"CtrlQ": tcell.KeyCtrlQ,
|
||||
"CtrlR": tcell.KeyCtrlR,
|
||||
"CtrlS": tcell.KeyCtrlS,
|
||||
"CtrlT": tcell.KeyCtrlT,
|
||||
"CtrlU": tcell.KeyCtrlU,
|
||||
"CtrlV": tcell.KeyCtrlV,
|
||||
"CtrlW": tcell.KeyCtrlW,
|
||||
"CtrlX": tcell.KeyCtrlX,
|
||||
"CtrlY": tcell.KeyCtrlY,
|
||||
"CtrlZ": tcell.KeyCtrlZ,
|
||||
"CtrlLeftSq": tcell.KeyCtrlLeftSq,
|
||||
"CtrlBackslash": tcell.KeyCtrlBackslash,
|
||||
"CtrlRightSq": tcell.KeyCtrlRightSq,
|
||||
"CtrlCarat": tcell.KeyCtrlCarat,
|
||||
"CtrlUnderscore": tcell.KeyCtrlUnderscore,
|
||||
"Backspace": tcell.KeyBackspace,
|
||||
"Tab": tcell.KeyTab,
|
||||
"Esc": tcell.KeyEsc,
|
||||
"Escape": tcell.KeyEscape,
|
||||
"Enter": tcell.KeyEnter,
|
||||
"Backspace2": tcell.KeyBackspace2,
|
||||
|
||||
// I renamed these keys to PageUp and PageDown but I don't want to break someone's keybindings
|
||||
"PgUp": tcell.KeyPgUp,
|
||||
"PgDown": tcell.KeyPgDn,
|
||||
}
|
||||
|
||||
// The Key struct holds the data for a keypress (keycode + modifiers)
|
||||
type Key struct {
|
||||
keyCode tcell.Key
|
||||
modifiers tcell.ModMask
|
||||
r rune
|
||||
}
|
||||
|
||||
// InitBindings initializes the keybindings for micro
|
||||
func InitBindings() {
|
||||
bindings = make(map[Key][]func(*View, bool) bool)
|
||||
|
||||
var parsed map[string]string
|
||||
defaults := DefaultBindings()
|
||||
|
||||
filename := configDir + "/bindings.json"
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
TermMessage("Error reading bindings.json file: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
TermMessage("Error reading bindings.json:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
parseBindings(defaults)
|
||||
parseBindings(parsed)
|
||||
}
|
||||
|
||||
func parseBindings(userBindings map[string]string) {
|
||||
for k, v := range userBindings {
|
||||
BindKey(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// findKey will find binding Key 'b' using string 'k'
|
||||
func findKey(k string) (b Key, ok bool) {
|
||||
modifiers := tcell.ModNone
|
||||
|
||||
// First, we'll strip off all the modifiers in the name and add them to the
|
||||
// ModMask
|
||||
modSearch:
|
||||
for {
|
||||
switch {
|
||||
case strings.HasPrefix(k, "-"):
|
||||
// We optionally support dashes between modifiers
|
||||
k = k[1:]
|
||||
case strings.HasPrefix(k, "Ctrl"):
|
||||
k = k[4:]
|
||||
modifiers |= tcell.ModCtrl
|
||||
case strings.HasPrefix(k, "Alt"):
|
||||
k = k[3:]
|
||||
modifiers |= tcell.ModAlt
|
||||
case strings.HasPrefix(k, "Shift"):
|
||||
k = k[5:]
|
||||
modifiers |= tcell.ModShift
|
||||
default:
|
||||
break modSearch
|
||||
}
|
||||
}
|
||||
|
||||
// Control is handled specially, since some character codes in bindingKeys
|
||||
// are different when Control is depressed. We should check for Control keys
|
||||
// first.
|
||||
if modifiers&tcell.ModCtrl != 0 {
|
||||
// see if the key is in bindingKeys with the Ctrl prefix.
|
||||
if code, ok := bindingKeys["Ctrl"+k]; ok {
|
||||
// It is, we're done.
|
||||
return Key{
|
||||
keyCode: code,
|
||||
modifiers: modifiers,
|
||||
r: 0,
|
||||
}, true
|
||||
}
|
||||
}
|
||||
|
||||
// See if we can find the key in bindingKeys
|
||||
if code, ok := bindingKeys[k]; ok {
|
||||
return Key{
|
||||
keyCode: code,
|
||||
modifiers: modifiers,
|
||||
r: 0,
|
||||
}, true
|
||||
}
|
||||
|
||||
// If we were given one character, then we've got a rune.
|
||||
if len(k) == 1 {
|
||||
return Key{
|
||||
keyCode: tcell.KeyRune,
|
||||
modifiers: modifiers,
|
||||
r: rune(k[0]),
|
||||
}, true
|
||||
}
|
||||
|
||||
// We don't know what happened.
|
||||
return Key{}, false
|
||||
}
|
||||
|
||||
// findAction will find 'action' using string 'v'
|
||||
func findAction(v string) (action func(*View, bool) bool) {
|
||||
action, ok := bindingActions[v]
|
||||
if !ok {
|
||||
// If the user seems to be binding a function that doesn't exist
|
||||
// We hope that it's a lua function that exists and bind it to that
|
||||
action = LuaFunctionBinding(v)
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
// BindKey takes a key and an action and binds the two together
|
||||
func BindKey(k, v string) {
|
||||
key, ok := findKey(k)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if v == "ToggleHelp" {
|
||||
helpBinding = k
|
||||
}
|
||||
|
||||
actionNames := strings.Split(v, ",")
|
||||
actions := make([]func(*View, bool) bool, 0, len(actionNames))
|
||||
for _, actionName := range actionNames {
|
||||
actions = append(actions, findAction(actionName))
|
||||
}
|
||||
|
||||
bindings[key] = actions
|
||||
}
|
||||
|
||||
// DefaultBindings returns a map containing micro's default keybindings
|
||||
func DefaultBindings() map[string]string {
|
||||
return map[string]string{
|
||||
"Up": "CursorUp",
|
||||
"Down": "CursorDown",
|
||||
"Right": "CursorRight",
|
||||
"Left": "CursorLeft",
|
||||
"ShiftUp": "SelectUp",
|
||||
"ShiftDown": "SelectDown",
|
||||
"ShiftLeft": "SelectLeft",
|
||||
"ShiftRight": "SelectRight",
|
||||
"AltLeft": "WordLeft",
|
||||
"AltRight": "WordRight",
|
||||
"AltShiftRight": "SelectWordRight",
|
||||
"AltShiftLeft": "SelectWordLeft",
|
||||
"CtrlLeft": "StartOfLine",
|
||||
"CtrlRight": "EndOfLine",
|
||||
"CtrlShiftLeft": "SelectToStartOfLine",
|
||||
"CtrlShiftRight": "SelectToEndOfLine",
|
||||
"CtrlUp": "CursorStart",
|
||||
"CtrlDown": "CursorEnd",
|
||||
"CtrlShiftUp": "SelectToStart",
|
||||
"CtrlShiftDown": "SelectToEnd",
|
||||
"Enter": "InsertNewline",
|
||||
"Space": "InsertSpace",
|
||||
"Backspace": "Backspace",
|
||||
"Backspace2": "Backspace",
|
||||
"Alt-Backspace": "DeleteWordLeft",
|
||||
"Alt-Backspace2": "DeleteWordLeft",
|
||||
"Tab": "IndentSelection,InsertTab",
|
||||
"Backtab": "OutdentSelection",
|
||||
"CtrlO": "OpenFile",
|
||||
"CtrlS": "Save",
|
||||
"CtrlF": "Find",
|
||||
"CtrlN": "FindNext",
|
||||
"CtrlP": "FindPrevious",
|
||||
"CtrlZ": "Undo",
|
||||
"CtrlY": "Redo",
|
||||
"CtrlC": "Copy",
|
||||
"CtrlX": "Cut",
|
||||
"CtrlK": "CutLine",
|
||||
"CtrlD": "DuplicateLine",
|
||||
"CtrlV": "Paste",
|
||||
"CtrlA": "SelectAll",
|
||||
"CtrlT": "AddTab",
|
||||
"CtrlRightSq": "PreviousTab",
|
||||
"CtrlBackslash": "NextTab",
|
||||
"Home": "StartOfLine",
|
||||
"End": "EndOfLine",
|
||||
"PageUp": "CursorPageUp",
|
||||
"PageDown": "CursorPageDown",
|
||||
"CtrlG": "ToggleHelp",
|
||||
"CtrlR": "ToggleRuler",
|
||||
"CtrlL": "JumpLine",
|
||||
"Delete": "Delete",
|
||||
"Esc": "ClearStatus",
|
||||
"CtrlB": "ShellMode",
|
||||
"CtrlQ": "Quit",
|
||||
"CtrlE": "CommandMode",
|
||||
"CtrlW": "NextSplit",
|
||||
|
||||
// Emacs-style keybindings
|
||||
"Alt-f": "WordRight",
|
||||
"Alt-b": "WordLeft",
|
||||
"Alt-a": "StartOfLine",
|
||||
"Alt-e": "EndOfLine",
|
||||
"Alt-p": "CursorUp",
|
||||
"Alt-n": "CursorDown",
|
||||
}
|
||||
}
|
||||
@@ -1,325 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Buffer stores the text for files that are loaded into the text editor
|
||||
// It uses a rope to efficiently store the string and contains some
|
||||
// simple functions for saving and wrapper functions for modifying the rope
|
||||
type Buffer struct {
|
||||
// The eventhandler for undo/redo
|
||||
*EventHandler
|
||||
// This stores all the text in the buffer as an array of lines
|
||||
*LineArray
|
||||
|
||||
Cursor Cursor
|
||||
|
||||
// Path to the file on disk
|
||||
Path string
|
||||
// Name of the buffer on the status line
|
||||
Name string
|
||||
|
||||
// Whether or not the buffer has been modified since it was opened
|
||||
IsModified bool
|
||||
|
||||
// Stores the last modification time of the file the buffer is pointing to
|
||||
ModTime time.Time
|
||||
|
||||
NumLines int
|
||||
|
||||
// Syntax highlighting rules
|
||||
rules []SyntaxRule
|
||||
|
||||
// Buffer local settings
|
||||
Settings map[string]interface{}
|
||||
}
|
||||
|
||||
// The SerializedBuffer holds the types that get serialized when a buffer is saved
|
||||
// These are used for the savecursor and saveundo options
|
||||
type SerializedBuffer struct {
|
||||
EventHandler *EventHandler
|
||||
Cursor Cursor
|
||||
ModTime time.Time
|
||||
}
|
||||
|
||||
// NewBuffer creates a new buffer from `txt` with path and name `path`
|
||||
func NewBuffer(txt []byte, path string) *Buffer {
|
||||
b := new(Buffer)
|
||||
b.LineArray = NewLineArray(txt)
|
||||
|
||||
b.Settings = DefaultLocalSettings()
|
||||
for k, v := range globalSettings {
|
||||
if _, ok := b.Settings[k]; ok {
|
||||
b.Settings[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
b.Path = path
|
||||
b.Name = path
|
||||
|
||||
// If the file doesn't have a path to disk then we give it no name
|
||||
if path == "" {
|
||||
b.Name = "No name"
|
||||
}
|
||||
|
||||
// The last time this file was modified
|
||||
b.ModTime, _ = GetModTime(b.Path)
|
||||
|
||||
b.EventHandler = NewEventHandler(b)
|
||||
|
||||
b.Update()
|
||||
b.FindFileType()
|
||||
b.UpdateRules()
|
||||
|
||||
if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
|
||||
os.Mkdir(configDir+"/buffers/", os.ModePerm)
|
||||
}
|
||||
|
||||
// Put the cursor at the first spot
|
||||
b.Cursor = Cursor{
|
||||
Loc: Loc{
|
||||
X: 0,
|
||||
Y: 0,
|
||||
},
|
||||
buf: b,
|
||||
}
|
||||
|
||||
InitLocalSettings(b)
|
||||
|
||||
if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
|
||||
// If either savecursor or saveundo is turned on, we need to load the serialized information
|
||||
// from ~/.config/micro/buffers
|
||||
absPath, _ := filepath.Abs(b.Path)
|
||||
file, err := os.Open(configDir + "/buffers/" + EscapePath(absPath))
|
||||
if err == nil {
|
||||
var buffer SerializedBuffer
|
||||
decoder := gob.NewDecoder(file)
|
||||
gob.Register(TextEvent{})
|
||||
err = decoder.Decode(&buffer)
|
||||
if err != nil {
|
||||
TermMessage(err.Error(), "\n", "You may want to remove the files in ~/.config/micro/buffers (these files store the information for the 'saveundo' and 'savecursor' options) if this problem persists.")
|
||||
}
|
||||
if b.Settings["savecursor"].(bool) {
|
||||
b.Cursor = buffer.Cursor
|
||||
b.Cursor.buf = b
|
||||
b.Cursor.Relocate()
|
||||
}
|
||||
|
||||
if b.Settings["saveundo"].(bool) {
|
||||
// We should only use last time's eventhandler if the file wasn't by someone else in the meantime
|
||||
if b.ModTime == buffer.ModTime {
|
||||
b.EventHandler = buffer.EventHandler
|
||||
b.EventHandler.buf = b
|
||||
}
|
||||
}
|
||||
}
|
||||
file.Close()
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// UpdateRules updates the syntax rules and filetype for this buffer
|
||||
// This is called when the colorscheme changes
|
||||
func (b *Buffer) UpdateRules() {
|
||||
b.rules = GetRules(b)
|
||||
}
|
||||
|
||||
// FindFileType identifies this buffer's filetype based on the extension or header
|
||||
func (b *Buffer) FindFileType() {
|
||||
b.Settings["filetype"] = FindFileType(b)
|
||||
}
|
||||
|
||||
// FileType returns the buffer's filetype
|
||||
func (b *Buffer) FileType() string {
|
||||
return b.Settings["filetype"].(string)
|
||||
}
|
||||
|
||||
// CheckModTime makes sure that the file this buffer points to hasn't been updated
|
||||
// by an external program since it was last read
|
||||
// If it has, we ask the user if they would like to reload the file
|
||||
func (b *Buffer) CheckModTime() {
|
||||
modTime, ok := GetModTime(b.Path)
|
||||
if ok {
|
||||
if modTime != b.ModTime {
|
||||
choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
|
||||
messenger.Reset()
|
||||
messenger.Clear()
|
||||
if !choice || canceled {
|
||||
// Don't load new changes -- do nothing
|
||||
b.ModTime, _ = GetModTime(b.Path)
|
||||
} else {
|
||||
// Load new changes
|
||||
b.ReOpen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReOpen reloads the current buffer from disk
|
||||
func (b *Buffer) ReOpen() {
|
||||
data, err := ioutil.ReadFile(b.Path)
|
||||
txt := string(data)
|
||||
|
||||
if err != nil {
|
||||
messenger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
b.EventHandler.ApplyDiff(txt)
|
||||
|
||||
b.ModTime, _ = GetModTime(b.Path)
|
||||
b.IsModified = false
|
||||
b.Update()
|
||||
b.Cursor.Relocate()
|
||||
}
|
||||
|
||||
// Update fetches the string from the rope and updates the `text` and `lines` in the buffer
|
||||
func (b *Buffer) Update() {
|
||||
b.NumLines = len(b.lines)
|
||||
}
|
||||
|
||||
// Save saves the buffer to its default path
|
||||
func (b *Buffer) Save() error {
|
||||
return b.SaveAs(b.Path)
|
||||
}
|
||||
|
||||
// SaveWithSudo saves the buffer to the default path with sudo
|
||||
func (b *Buffer) SaveWithSudo() error {
|
||||
return b.SaveAsWithSudo(b.Path)
|
||||
}
|
||||
|
||||
// Serialize serializes the buffer to configDir/buffers
|
||||
func (b *Buffer) Serialize() error {
|
||||
if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
|
||||
absPath, _ := filepath.Abs(b.Path)
|
||||
file, err := os.Create(configDir + "/buffers/" + EscapePath(absPath))
|
||||
if err == nil {
|
||||
enc := gob.NewEncoder(file)
|
||||
gob.Register(TextEvent{})
|
||||
err = enc.Encode(SerializedBuffer{
|
||||
b.EventHandler,
|
||||
b.Cursor,
|
||||
b.ModTime,
|
||||
})
|
||||
}
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
|
||||
func (b *Buffer) SaveAs(filename string) error {
|
||||
b.UpdateRules()
|
||||
b.Name = filename
|
||||
b.Path = filename
|
||||
data := []byte(b.String())
|
||||
err := ioutil.WriteFile(filename, data, 0644)
|
||||
if err == nil {
|
||||
b.IsModified = false
|
||||
b.ModTime, _ = GetModTime(filename)
|
||||
return b.Serialize()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// SaveAsWithSudo is the same as SaveAs except it uses a neat trick
|
||||
// with tee to use sudo so the user doesn't have to reopen micro with sudo
|
||||
func (b *Buffer) SaveAsWithSudo(filename string) error {
|
||||
b.UpdateRules()
|
||||
b.Name = filename
|
||||
b.Path = filename
|
||||
|
||||
// The user may have already used sudo in which case we won't need the password
|
||||
// It's a bit nicer for them if they don't have to enter the password every time
|
||||
_, err := RunShellCommand("sudo -v")
|
||||
needPassword := err != nil
|
||||
|
||||
// If we need the password, we have to close the screen and ask using the shell
|
||||
if needPassword {
|
||||
// Shut down the screen because we're going to interact directly with the shell
|
||||
screen.Fini()
|
||||
screen = nil
|
||||
}
|
||||
|
||||
// Set up everything for the command
|
||||
cmd := exec.Command("sudo", "tee", filename)
|
||||
cmd.Stdin = bytes.NewBufferString(b.String())
|
||||
|
||||
// This is a trap for Ctrl-C so that it doesn't kill micro
|
||||
// Instead we trap Ctrl-C to kill the program we're running
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
for range c {
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the command
|
||||
cmd.Start()
|
||||
err = cmd.Wait()
|
||||
|
||||
// If we needed the password, we closed the screen, so we have to initialize it again
|
||||
if needPassword {
|
||||
// Start the screen back up
|
||||
InitScreen()
|
||||
}
|
||||
if err == nil {
|
||||
b.IsModified = false
|
||||
b.ModTime, _ = GetModTime(filename)
|
||||
b.Serialize()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *Buffer) insert(pos Loc, value []byte) {
|
||||
b.IsModified = true
|
||||
b.LineArray.insert(pos, value)
|
||||
b.Update()
|
||||
}
|
||||
func (b *Buffer) remove(start, end Loc) string {
|
||||
b.IsModified = true
|
||||
sub := b.LineArray.remove(start, end)
|
||||
b.Update()
|
||||
return sub
|
||||
}
|
||||
|
||||
// Start returns the location of the first character in the buffer
|
||||
func (b *Buffer) Start() Loc {
|
||||
return Loc{0, 0}
|
||||
}
|
||||
|
||||
// End returns the location of the last character in the buffer
|
||||
func (b *Buffer) End() Loc {
|
||||
return Loc{utf8.RuneCount(b.lines[b.NumLines-1]), b.NumLines - 1}
|
||||
}
|
||||
|
||||
// Line returns a single line
|
||||
func (b *Buffer) Line(n int) string {
|
||||
return string(b.lines[n])
|
||||
}
|
||||
|
||||
// Lines returns an array of strings containing the lines from start to end
|
||||
func (b *Buffer) Lines(start, end int) []string {
|
||||
lines := b.lines[start:end]
|
||||
var slice []string
|
||||
for _, line := range lines {
|
||||
slice = append(slice, string(line))
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// Len gives the length of the buffer
|
||||
func (b *Buffer) Len() int {
|
||||
return Count(b.String())
|
||||
}
|
||||
143
cmd/micro/clean.go
Normal file
143
cmd/micro/clean.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
)
|
||||
|
||||
func shouldContinue() bool {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print("Continue [Y/n]: ")
|
||||
text, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return false
|
||||
}
|
||||
|
||||
if len(text) <= 1 {
|
||||
// default continue
|
||||
return true
|
||||
}
|
||||
|
||||
return strings.ToLower(text)[0] == 'y'
|
||||
}
|
||||
|
||||
// CleanConfig performs cleanup in the user's configuration directory
|
||||
func CleanConfig() {
|
||||
fmt.Println("Cleaning your configuration directory at", config.ConfigDir)
|
||||
fmt.Printf("Please consider backing up %s before continuing\n", config.ConfigDir)
|
||||
|
||||
if !shouldContinue() {
|
||||
fmt.Println("Stopping early")
|
||||
return
|
||||
}
|
||||
|
||||
// detect unused options
|
||||
var unusedOptions []string
|
||||
defaultSettings := config.DefaultAllSettings()
|
||||
for k := range config.GlobalSettings {
|
||||
if _, ok := defaultSettings[k]; !ok {
|
||||
valid := false
|
||||
for _, p := range config.Plugins {
|
||||
if strings.HasPrefix(k, p.Name+".") || k == p.Name {
|
||||
valid = true
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
unusedOptions = append(unusedOptions, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(unusedOptions) > 0 {
|
||||
fmt.Println("The following options are unused:")
|
||||
|
||||
sort.Strings(unusedOptions)
|
||||
|
||||
for _, s := range unusedOptions {
|
||||
fmt.Printf("%s (value: %v)\n", s, config.GlobalSettings[s])
|
||||
}
|
||||
|
||||
fmt.Printf("These options will be removed from %s\n", filepath.Join(config.ConfigDir, "settings.json"))
|
||||
|
||||
if shouldContinue() {
|
||||
for _, s := range unusedOptions {
|
||||
delete(config.GlobalSettings, s)
|
||||
}
|
||||
|
||||
err := config.OverwriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
|
||||
if err != nil {
|
||||
fmt.Println("Error writing settings.json file: " + err.Error())
|
||||
}
|
||||
|
||||
fmt.Println("Removed unused options")
|
||||
fmt.Print("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
// detect incorrectly formatted buffer/ files
|
||||
files, err := ioutil.ReadDir(filepath.Join(config.ConfigDir, "buffers"))
|
||||
if err == nil {
|
||||
var badFiles []string
|
||||
var buffer buffer.SerializedBuffer
|
||||
for _, f := range files {
|
||||
fname := filepath.Join(config.ConfigDir, "buffers", f.Name())
|
||||
file, e := os.Open(fname)
|
||||
|
||||
if e == nil {
|
||||
defer file.Close()
|
||||
|
||||
decoder := gob.NewDecoder(file)
|
||||
err = decoder.Decode(&buffer)
|
||||
|
||||
if err != nil && f.Name() != "history" {
|
||||
badFiles = append(badFiles, fname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(badFiles) > 0 {
|
||||
fmt.Printf("Detected %d files with an invalid format in %s\n", len(badFiles), filepath.Join(config.ConfigDir, "buffers"))
|
||||
fmt.Println("These files store cursor and undo history.")
|
||||
fmt.Printf("Removing badly formatted files in %s\n", filepath.Join(config.ConfigDir, "buffers"))
|
||||
|
||||
if shouldContinue() {
|
||||
for _, f := range badFiles {
|
||||
err := os.Remove(f)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Removed badly formatted files")
|
||||
fmt.Print("\n\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// detect plugins/ directory
|
||||
plugins := filepath.Join(config.ConfigDir, "plugins")
|
||||
if stat, err := os.Stat(plugins); err == nil && stat.IsDir() {
|
||||
fmt.Printf("Found directory %s\n", plugins)
|
||||
fmt.Printf("Plugins should now be stored in %s\n", filepath.Join(config.ConfigDir, "plug"))
|
||||
fmt.Printf("Removing %s\n", plugins)
|
||||
|
||||
if shouldContinue() {
|
||||
os.RemoveAll(plugins)
|
||||
}
|
||||
|
||||
fmt.Print("\n\n")
|
||||
}
|
||||
|
||||
fmt.Println("Done cleaning")
|
||||
}
|
||||
@@ -1,425 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
action func([]string)
|
||||
completions []Completion
|
||||
}
|
||||
|
||||
type StrCommand struct {
|
||||
action string
|
||||
completions []Completion
|
||||
}
|
||||
|
||||
var commands map[string]Command
|
||||
|
||||
var commandActions = map[string]func([]string){
|
||||
"Set": Set,
|
||||
"SetLocal": SetLocal,
|
||||
"Show": Show,
|
||||
"Run": Run,
|
||||
"Bind": Bind,
|
||||
"Quit": Quit,
|
||||
"Save": Save,
|
||||
"Replace": Replace,
|
||||
"VSplit": VSplit,
|
||||
"HSplit": HSplit,
|
||||
"Tab": NewTab,
|
||||
"Help": Help,
|
||||
}
|
||||
|
||||
// InitCommands initializes the default commands
|
||||
func InitCommands() {
|
||||
commands = make(map[string]Command)
|
||||
|
||||
defaults := DefaultCommands()
|
||||
parseCommands(defaults)
|
||||
}
|
||||
|
||||
func parseCommands(userCommands map[string]StrCommand) {
|
||||
for k, v := range userCommands {
|
||||
MakeCommand(k, v.action, v.completions...)
|
||||
}
|
||||
}
|
||||
|
||||
// MakeCommand is a function to easily create new commands
|
||||
// This can be called by plugins in Lua so that plugins can define their own commands
|
||||
func MakeCommand(name, function string, completions ...Completion) {
|
||||
action := commandActions[function]
|
||||
if _, ok := commandActions[function]; !ok {
|
||||
// If the user seems to be binding a function that doesn't exist
|
||||
// We hope that it's a lua function that exists and bind it to that
|
||||
action = LuaFunctionCommand(function)
|
||||
}
|
||||
|
||||
commands[name] = Command{action, completions}
|
||||
}
|
||||
|
||||
// DefaultCommands returns a map containing micro's default commands
|
||||
func DefaultCommands() map[string]StrCommand {
|
||||
return map[string]StrCommand{
|
||||
"set": {"Set", []Completion{OptionCompletion, NoCompletion}},
|
||||
"setlocal": {"SetLocal", []Completion{OptionCompletion, NoCompletion}},
|
||||
"show": {"Show", []Completion{OptionCompletion, NoCompletion}},
|
||||
"bind": {"Bind", []Completion{NoCompletion}},
|
||||
"run": {"Run", []Completion{NoCompletion}},
|
||||
"quit": {"Quit", []Completion{NoCompletion}},
|
||||
"save": {"Save", []Completion{NoCompletion}},
|
||||
"replace": {"Replace", []Completion{NoCompletion}},
|
||||
"vsplit": {"VSplit", []Completion{FileCompletion, NoCompletion}},
|
||||
"hsplit": {"HSplit", []Completion{FileCompletion, NoCompletion}},
|
||||
"tab": {"Tab", []Completion{FileCompletion, NoCompletion}},
|
||||
"help": {"Help", []Completion{HelpCompletion, NoCompletion}},
|
||||
}
|
||||
}
|
||||
|
||||
// Help tries to open the given help page in a horizontal split
|
||||
func Help(args []string) {
|
||||
if len(args) < 1 {
|
||||
// Open the default help if the user just typed "> help"
|
||||
CurView().openHelp("help")
|
||||
} else {
|
||||
helpPage := args[0]
|
||||
if _, ok := helpPages[helpPage]; ok {
|
||||
CurView().openHelp(helpPage)
|
||||
} else {
|
||||
messenger.Error("Sorry, no help for ", helpPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VSplit opens a vertical split with file given in the first argument
|
||||
// If no file is given, it opens an empty buffer in a new split
|
||||
func VSplit(args []string) {
|
||||
if len(args) == 0 {
|
||||
CurView().VSplit(NewBuffer([]byte{}, ""))
|
||||
} else {
|
||||
filename := args[0]
|
||||
home, _ := homedir.Dir()
|
||||
filename = strings.Replace(filename, "~", home, 1)
|
||||
file, err := ioutil.ReadFile(filename)
|
||||
|
||||
var buf *Buffer
|
||||
if err != nil {
|
||||
// File does not exist -- create an empty buffer with that name
|
||||
buf = NewBuffer([]byte{}, filename)
|
||||
} else {
|
||||
buf = NewBuffer(file, filename)
|
||||
}
|
||||
CurView().VSplit(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// HSplit opens a horizontal split with file given in the first argument
|
||||
// If no file is given, it opens an empty buffer in a new split
|
||||
func HSplit(args []string) {
|
||||
if len(args) == 0 {
|
||||
CurView().HSplit(NewBuffer([]byte{}, ""))
|
||||
} else {
|
||||
filename := args[0]
|
||||
home, _ := homedir.Dir()
|
||||
filename = strings.Replace(filename, "~", home, 1)
|
||||
file, err := ioutil.ReadFile(filename)
|
||||
|
||||
var buf *Buffer
|
||||
if err != nil {
|
||||
// File does not exist -- create an empty buffer with that name
|
||||
buf = NewBuffer([]byte{}, filename)
|
||||
} else {
|
||||
buf = NewBuffer(file, filename)
|
||||
}
|
||||
CurView().HSplit(buf)
|
||||
}
|
||||
}
|
||||
|
||||
// NewTab opens the given file in a new tab
|
||||
func NewTab(args []string) {
|
||||
if len(args) == 0 {
|
||||
CurView().AddTab(true)
|
||||
} else {
|
||||
filename := args[0]
|
||||
home, _ := homedir.Dir()
|
||||
filename = strings.Replace(filename, "~", home, 1)
|
||||
file, _ := ioutil.ReadFile(filename)
|
||||
|
||||
tab := NewTabFromView(NewView(NewBuffer(file, filename)))
|
||||
tab.SetNum(len(tabs))
|
||||
tabs = append(tabs, tab)
|
||||
curTab++
|
||||
if len(tabs) == 2 {
|
||||
for _, t := range tabs {
|
||||
for _, v := range t.views {
|
||||
v.ToggleTabbar()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets an option
|
||||
func Set(args []string) {
|
||||
if len(args) < 2 {
|
||||
messenger.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
option := strings.TrimSpace(args[0])
|
||||
value := strings.TrimSpace(args[1])
|
||||
|
||||
SetOptionAndSettings(option, value)
|
||||
}
|
||||
|
||||
// SetLocal sets an option local to the buffer
|
||||
func SetLocal(args []string) {
|
||||
if len(args) < 2 {
|
||||
messenger.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
option := strings.TrimSpace(args[0])
|
||||
value := strings.TrimSpace(args[1])
|
||||
|
||||
err := SetLocalOption(option, value, CurView())
|
||||
if err != nil {
|
||||
messenger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Show shows the value of the given option
|
||||
func Show(args []string) {
|
||||
if len(args) < 1 {
|
||||
messenger.Error("Please provide an option to show")
|
||||
return
|
||||
}
|
||||
|
||||
option := GetOption(args[0])
|
||||
|
||||
if option == nil {
|
||||
messenger.Error(args[0], " is not a valid option")
|
||||
return
|
||||
}
|
||||
|
||||
messenger.Message(option)
|
||||
}
|
||||
|
||||
// Bind creates a new keybinding
|
||||
func Bind(args []string) {
|
||||
if len(args) < 2 {
|
||||
messenger.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
BindKey(args[0], args[1])
|
||||
}
|
||||
|
||||
// Run runs a shell command in the background
|
||||
func Run(args []string) {
|
||||
// Run a shell command in the background (openTerm is false)
|
||||
HandleShellCommand(strings.Join(args, " "), false)
|
||||
}
|
||||
|
||||
// Quit closes the main view
|
||||
func Quit(args []string) {
|
||||
// Close the main view
|
||||
CurView().Quit(true)
|
||||
}
|
||||
|
||||
// Save saves the buffer in the main view
|
||||
func Save(args []string) {
|
||||
// Save the main view
|
||||
CurView().Save(true)
|
||||
}
|
||||
|
||||
// Replace runs search and replace
|
||||
func Replace(args []string) {
|
||||
// This is a regex to parse the replace expression
|
||||
// We allow no quotes if there are no spaces, but if you want to search
|
||||
// for or replace an expression with spaces, you can add double quotes
|
||||
r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|[^\s]*`)
|
||||
replaceCmd := r.FindAllString(strings.Join(args, " "), -1)
|
||||
if len(replaceCmd) < 2 {
|
||||
// We need to find both a search and replace expression
|
||||
messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
|
||||
return
|
||||
}
|
||||
|
||||
var flags string
|
||||
if len(replaceCmd) == 3 {
|
||||
// The user included some flags
|
||||
flags = replaceCmd[2]
|
||||
}
|
||||
|
||||
search := string(replaceCmd[0])
|
||||
replace := string(replaceCmd[1])
|
||||
|
||||
// If the search and replace expressions have quotes, we need to remove those
|
||||
if strings.HasPrefix(search, `"`) && strings.HasSuffix(search, `"`) {
|
||||
search = search[1 : len(search)-1]
|
||||
}
|
||||
if strings.HasPrefix(replace, `"`) && strings.HasSuffix(replace, `"`) {
|
||||
replace = replace[1 : len(replace)-1]
|
||||
}
|
||||
|
||||
// We replace all escaped double quotes to real double quotes
|
||||
search = strings.Replace(search, `\"`, `"`, -1)
|
||||
replace = strings.Replace(replace, `\"`, `"`, -1)
|
||||
// Replace some things so users can actually insert newlines and tabs in replacements
|
||||
replace = strings.Replace(replace, "\\n", "\n", -1)
|
||||
replace = strings.Replace(replace, "\\t", "\t", -1)
|
||||
|
||||
regex, err := regexp.Compile(search)
|
||||
if err != nil {
|
||||
// There was an error with the user's regex
|
||||
messenger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
view := CurView()
|
||||
|
||||
found := 0
|
||||
for {
|
||||
match := regex.FindStringIndex(view.Buf.String())
|
||||
if match == nil {
|
||||
break
|
||||
}
|
||||
found++
|
||||
if strings.Contains(flags, "c") {
|
||||
// The 'check' flag was used
|
||||
Search(search, view, true)
|
||||
view.Relocate()
|
||||
if view.Buf.Settings["syntax"].(bool) {
|
||||
view.matches = Match(view)
|
||||
}
|
||||
RedrawAll()
|
||||
choice, canceled := messenger.YesNoPrompt("Perform replacement? (y,n)")
|
||||
if canceled {
|
||||
if view.Cursor.HasSelection() {
|
||||
view.Cursor.Loc = view.Cursor.CurSelection[0]
|
||||
view.Cursor.ResetSelection()
|
||||
}
|
||||
messenger.Reset()
|
||||
return
|
||||
}
|
||||
if choice {
|
||||
view.Cursor.DeleteSelection()
|
||||
view.Buf.Insert(FromCharPos(match[0], view.Buf), replace)
|
||||
view.Cursor.ResetSelection()
|
||||
messenger.Reset()
|
||||
} else {
|
||||
if view.Cursor.HasSelection() {
|
||||
searchStart = ToCharPos(view.Cursor.CurSelection[1], view.Buf)
|
||||
} else {
|
||||
searchStart = ToCharPos(view.Cursor.Loc, view.Buf)
|
||||
}
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
view.Buf.Replace(FromCharPos(match[0], view.Buf), FromCharPos(match[1], view.Buf), replace)
|
||||
}
|
||||
}
|
||||
view.Cursor.Relocate()
|
||||
|
||||
if found > 1 {
|
||||
messenger.Message("Replaced ", found, " occurrences of ", search)
|
||||
} else if found == 1 {
|
||||
messenger.Message("Replaced ", found, " occurrence of ", search)
|
||||
} else {
|
||||
messenger.Message("Nothing matched ", search)
|
||||
}
|
||||
}
|
||||
|
||||
// RunShellCommand executes a shell command and returns the output/error
|
||||
func RunShellCommand(input string) (string, error) {
|
||||
inputCmd := strings.Split(input, " ")[0]
|
||||
args := strings.Split(input, " ")[1:]
|
||||
|
||||
cmd := exec.Command(inputCmd, args...)
|
||||
outputBytes := &bytes.Buffer{}
|
||||
cmd.Stdout = outputBytes
|
||||
cmd.Stderr = outputBytes
|
||||
cmd.Start()
|
||||
err := cmd.Wait() // wait for command to finish
|
||||
outstring := outputBytes.String()
|
||||
return outstring, err
|
||||
}
|
||||
|
||||
// HandleShellCommand runs the shell command
|
||||
// The openTerm argument specifies whether a terminal should be opened (for viewing output
|
||||
// or interacting with stdin)
|
||||
func HandleShellCommand(input string, openTerm bool) {
|
||||
inputCmd := strings.Split(input, " ")[0]
|
||||
if !openTerm {
|
||||
// Simply run the command in the background and notify the user when it's done
|
||||
messenger.Message("Running...")
|
||||
go func() {
|
||||
output, err := RunShellCommand(input)
|
||||
totalLines := strings.Split(output, "\n")
|
||||
|
||||
if len(totalLines) < 3 {
|
||||
if err == nil {
|
||||
messenger.Message(inputCmd, " exited without error")
|
||||
} else {
|
||||
messenger.Message(inputCmd, " exited with error: ", err, ": ", output)
|
||||
}
|
||||
} else {
|
||||
messenger.Message(output)
|
||||
}
|
||||
// We have to make sure to redraw
|
||||
RedrawAll()
|
||||
}()
|
||||
} else {
|
||||
// Shut down the screen because we're going to interact directly with the shell
|
||||
screen.Fini()
|
||||
screen = nil
|
||||
|
||||
args := strings.Split(input, " ")[1:]
|
||||
|
||||
// Set up everything for the command
|
||||
cmd := exec.Command(inputCmd, args...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
// This is a trap for Ctrl-C so that it doesn't kill micro
|
||||
// Instead we trap Ctrl-C to kill the program we're running
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
for range c {
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the command
|
||||
cmd.Start()
|
||||
cmd.Wait()
|
||||
|
||||
// This is just so we don't return right away and let the user press enter to return
|
||||
TermMessage("")
|
||||
|
||||
// Start the screen back up
|
||||
InitScreen()
|
||||
}
|
||||
}
|
||||
|
||||
// HandleCommand handles input from the user
|
||||
func HandleCommand(input string) {
|
||||
inputCmd := strings.Split(input, " ")[0]
|
||||
args := strings.Split(input, " ")[1:]
|
||||
|
||||
if _, ok := commands[inputCmd]; !ok {
|
||||
messenger.Error("Unknown command ", inputCmd)
|
||||
} else {
|
||||
commands[inputCmd].action(args)
|
||||
}
|
||||
}
|
||||
@@ -1,330 +0,0 @@
|
||||
package main
|
||||
|
||||
// The Cursor struct stores the location of the cursor in the view
|
||||
// The complicated part about the cursor is storing its location.
|
||||
// The cursor must be displayed at an x, y location, but since the buffer
|
||||
// uses a rope to store text, to insert text we must have an index. It
|
||||
// is also simpler to use character indicies for other tasks such as
|
||||
// selection.
|
||||
type Cursor struct {
|
||||
buf *Buffer
|
||||
Loc
|
||||
|
||||
// Last cursor x position
|
||||
LastVisualX int
|
||||
|
||||
// The current selection as a range of character numbers (inclusive)
|
||||
CurSelection [2]Loc
|
||||
// The original selection as a range of character numbers
|
||||
// This is used for line and word selection where it is necessary
|
||||
// to know what the original selection was
|
||||
OrigSelection [2]Loc
|
||||
}
|
||||
|
||||
// Goto puts the cursor at the given cursor's location and gives the current cursor its selection too
|
||||
func (c *Cursor) Goto(b Cursor) {
|
||||
c.X, c.Y, c.LastVisualX = b.X, b.Y, b.LastVisualX
|
||||
c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
|
||||
}
|
||||
|
||||
// ResetSelection resets the user's selection
|
||||
func (c *Cursor) ResetSelection() {
|
||||
c.CurSelection[0] = c.buf.Start()
|
||||
c.CurSelection[1] = c.buf.Start()
|
||||
}
|
||||
|
||||
// HasSelection returns whether or not the user has selected anything
|
||||
func (c *Cursor) HasSelection() bool {
|
||||
return c.CurSelection[0] != c.CurSelection[1]
|
||||
}
|
||||
|
||||
// DeleteSelection deletes the currently selected text
|
||||
func (c *Cursor) DeleteSelection() {
|
||||
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
|
||||
c.buf.Remove(c.CurSelection[1], c.CurSelection[0])
|
||||
c.Loc = c.CurSelection[1]
|
||||
} else if c.GetSelection() == "" {
|
||||
return
|
||||
} else {
|
||||
c.buf.Remove(c.CurSelection[0], c.CurSelection[1])
|
||||
c.Loc = c.CurSelection[0]
|
||||
}
|
||||
}
|
||||
|
||||
// GetSelection returns the cursor's selection
|
||||
func (c *Cursor) GetSelection() string {
|
||||
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
|
||||
return c.buf.Substr(c.CurSelection[1], c.CurSelection[0])
|
||||
}
|
||||
return c.buf.Substr(c.CurSelection[0], c.CurSelection[1])
|
||||
}
|
||||
|
||||
// SelectLine selects the current line
|
||||
func (c *Cursor) SelectLine() {
|
||||
c.Start()
|
||||
c.CurSelection[0] = c.Loc
|
||||
c.End()
|
||||
if c.buf.NumLines-1 > c.Y {
|
||||
c.CurSelection[1] = c.Loc.Move(1, c.buf)
|
||||
} else {
|
||||
c.CurSelection[1] = c.Loc
|
||||
}
|
||||
|
||||
c.OrigSelection = c.CurSelection
|
||||
}
|
||||
|
||||
// AddLineToSelection adds the current line to the selection
|
||||
func (c *Cursor) AddLineToSelection() {
|
||||
if c.Loc.LessThan(c.OrigSelection[0]) {
|
||||
c.Start()
|
||||
c.CurSelection[0] = c.Loc
|
||||
c.CurSelection[1] = c.OrigSelection[1]
|
||||
}
|
||||
if c.Loc.GreaterThan(c.OrigSelection[1]) {
|
||||
c.End()
|
||||
c.CurSelection[1] = c.Loc.Move(1, c.buf)
|
||||
c.CurSelection[0] = c.OrigSelection[0]
|
||||
}
|
||||
|
||||
if c.Loc.LessThan(c.OrigSelection[1]) && c.Loc.GreaterThan(c.OrigSelection[0]) {
|
||||
c.CurSelection = c.OrigSelection
|
||||
}
|
||||
}
|
||||
|
||||
// SelectWord selects the word the cursor is currently on
|
||||
func (c *Cursor) SelectWord() {
|
||||
if len(c.buf.Line(c.Y)) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !IsWordChar(string(c.RuneUnder(c.X))) {
|
||||
c.CurSelection[0] = c.Loc
|
||||
c.CurSelection[1] = c.Loc.Move(1, c.buf)
|
||||
c.OrigSelection = c.CurSelection
|
||||
return
|
||||
}
|
||||
|
||||
forward, backward := c.X, c.X
|
||||
|
||||
for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
|
||||
backward--
|
||||
}
|
||||
|
||||
c.CurSelection[0] = Loc{backward, c.Y}
|
||||
c.OrigSelection[0] = c.CurSelection[0]
|
||||
|
||||
for forward < Count(c.buf.Line(c.Y))-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
|
||||
forward++
|
||||
}
|
||||
|
||||
c.CurSelection[1] = Loc{forward, c.Y}.Move(1, c.buf)
|
||||
c.OrigSelection[1] = c.CurSelection[1]
|
||||
c.Loc = c.CurSelection[1]
|
||||
}
|
||||
|
||||
// AddWordToSelection adds the word the cursor is currently on to the selection
|
||||
func (c *Cursor) AddWordToSelection() {
|
||||
if c.Loc.GreaterThan(c.OrigSelection[0]) && c.Loc.LessThan(c.OrigSelection[1]) {
|
||||
c.CurSelection = c.OrigSelection
|
||||
return
|
||||
}
|
||||
|
||||
if c.Loc.LessThan(c.OrigSelection[0]) {
|
||||
backward := c.X
|
||||
|
||||
for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
|
||||
backward--
|
||||
}
|
||||
|
||||
c.CurSelection[0] = Loc{backward, c.Y}
|
||||
c.CurSelection[1] = c.OrigSelection[1]
|
||||
}
|
||||
|
||||
if c.Loc.GreaterThan(c.OrigSelection[1]) {
|
||||
forward := c.X
|
||||
|
||||
for forward < Count(c.buf.Line(c.Y))-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
|
||||
forward++
|
||||
}
|
||||
|
||||
c.CurSelection[1] = Loc{forward, c.Y}.Move(1, c.buf)
|
||||
c.CurSelection[0] = c.OrigSelection[0]
|
||||
}
|
||||
|
||||
c.Loc = c.CurSelection[1]
|
||||
}
|
||||
|
||||
// SelectTo selects from the current cursor location to the given location
|
||||
func (c *Cursor) SelectTo(loc Loc) {
|
||||
if loc.GreaterThan(c.OrigSelection[0]) {
|
||||
c.CurSelection[0] = c.OrigSelection[0]
|
||||
c.CurSelection[1] = loc
|
||||
} else {
|
||||
c.CurSelection[0] = loc
|
||||
c.CurSelection[1] = c.OrigSelection[0]
|
||||
}
|
||||
}
|
||||
|
||||
// WordRight moves the cursor one word to the right
|
||||
func (c *Cursor) WordRight() {
|
||||
for IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == Count(c.buf.Line(c.Y)) {
|
||||
c.Right()
|
||||
return
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
c.Right()
|
||||
for IsWordChar(string(c.RuneUnder(c.X))) {
|
||||
if c.X == Count(c.buf.Line(c.Y)) {
|
||||
return
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
}
|
||||
|
||||
// WordLeft moves the cursor one word to the left
|
||||
func (c *Cursor) WordLeft() {
|
||||
c.Left()
|
||||
for IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == 0 {
|
||||
return
|
||||
}
|
||||
c.Left()
|
||||
}
|
||||
c.Left()
|
||||
for IsWordChar(string(c.RuneUnder(c.X))) {
|
||||
if c.X == 0 {
|
||||
return
|
||||
}
|
||||
c.Left()
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
|
||||
// RuneUnder returns the rune under the given x position
|
||||
func (c *Cursor) RuneUnder(x int) rune {
|
||||
line := []rune(c.buf.Line(c.Y))
|
||||
if len(line) == 0 {
|
||||
return '\n'
|
||||
}
|
||||
if x >= len(line) {
|
||||
return '\n'
|
||||
} else if x < 0 {
|
||||
x = 0
|
||||
}
|
||||
return line[x]
|
||||
}
|
||||
|
||||
// UpN moves the cursor up N lines (if possible)
|
||||
func (c *Cursor) UpN(amount int) {
|
||||
proposedY := c.Y - amount
|
||||
if proposedY < 0 {
|
||||
proposedY = 0
|
||||
} else if proposedY >= c.buf.NumLines {
|
||||
proposedY = c.buf.NumLines - 1
|
||||
}
|
||||
if proposedY == c.Y {
|
||||
return
|
||||
}
|
||||
|
||||
c.Y = proposedY
|
||||
runes := []rune(c.buf.Line(c.Y))
|
||||
c.X = c.GetCharPosInLine(c.Y, c.LastVisualX)
|
||||
if c.X > len(runes) {
|
||||
c.X = len(runes)
|
||||
}
|
||||
}
|
||||
|
||||
// DownN moves the cursor down N lines (if possible)
|
||||
func (c *Cursor) DownN(amount int) {
|
||||
c.UpN(-amount)
|
||||
}
|
||||
|
||||
// Up moves the cursor up one line (if possible)
|
||||
func (c *Cursor) Up() {
|
||||
c.UpN(1)
|
||||
}
|
||||
|
||||
// Down moves the cursor down one line (if possible)
|
||||
func (c *Cursor) Down() {
|
||||
c.DownN(1)
|
||||
}
|
||||
|
||||
// Left moves the cursor left one cell (if possible) or to the last line if it is at the beginning
|
||||
func (c *Cursor) Left() {
|
||||
if c.Loc == c.buf.Start() {
|
||||
return
|
||||
}
|
||||
if c.X > 0 {
|
||||
c.X--
|
||||
} else {
|
||||
c.Up()
|
||||
c.End()
|
||||
}
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
|
||||
// Right moves the cursor right one cell (if possible) or to the next line if it is at the end
|
||||
func (c *Cursor) Right() {
|
||||
if c.Loc == c.buf.End() {
|
||||
return
|
||||
}
|
||||
if c.X < Count(c.buf.Line(c.Y)) {
|
||||
c.X++
|
||||
} else {
|
||||
c.Down()
|
||||
c.Start()
|
||||
}
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
|
||||
// End moves the cursor to the end of the line it is on
|
||||
func (c *Cursor) End() {
|
||||
c.X = Count(c.buf.Line(c.Y))
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
|
||||
// Start moves the cursor to the start of the line it is on
|
||||
func (c *Cursor) Start() {
|
||||
c.X = 0
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
|
||||
// GetCharPosInLine gets the char position of a visual x y coordinate (this is necessary because tabs are 1 char but 4 visual spaces)
|
||||
func (c *Cursor) GetCharPosInLine(lineNum, visualPos int) int {
|
||||
// Get the tab size
|
||||
tabSize := int(c.buf.Settings["tabsize"].(float64))
|
||||
visualLineLen := StringWidth(c.buf.Line(lineNum), tabSize)
|
||||
if visualPos > visualLineLen {
|
||||
visualPos = visualLineLen
|
||||
}
|
||||
width := WidthOfLargeRunes(c.buf.Line(lineNum), tabSize)
|
||||
if visualPos >= width {
|
||||
return visualPos - width
|
||||
}
|
||||
return visualPos / tabSize
|
||||
}
|
||||
|
||||
// GetVisualX returns the x value of the cursor in visual spaces
|
||||
func (c *Cursor) GetVisualX() int {
|
||||
runes := []rune(c.buf.Line(c.Y))
|
||||
tabSize := int(c.buf.Settings["tabsize"].(float64))
|
||||
return StringWidth(string(runes[:c.X]), tabSize)
|
||||
}
|
||||
|
||||
// Relocate makes sure that the cursor is inside the bounds of the buffer
|
||||
// If it isn't, it moves it to be within the buffer's lines
|
||||
func (c *Cursor) Relocate() {
|
||||
if c.Y < 0 {
|
||||
c.Y = 0
|
||||
} else if c.Y >= c.buf.NumLines {
|
||||
c.Y = c.buf.NumLines - 1
|
||||
}
|
||||
|
||||
if c.X < 0 {
|
||||
c.X = 0
|
||||
} else if c.X > Count(c.buf.Line(c.Y)) {
|
||||
c.X = Count(c.buf.Line(c.Y))
|
||||
}
|
||||
}
|
||||
31
cmd/micro/debug.go
Normal file
31
cmd/micro/debug.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// NullWriter simply sends writes into the void
|
||||
type NullWriter struct{}
|
||||
|
||||
// Write is empty
|
||||
func (NullWriter) Write(data []byte) (n int, err error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// InitLog sets up the debug log system for micro if it has been enabled by compile-time variables
|
||||
func InitLog() {
|
||||
if util.Debug == "ON" {
|
||||
f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
|
||||
if err != nil {
|
||||
log.Fatalf("error opening file: %v", err)
|
||||
}
|
||||
|
||||
log.SetOutput(f)
|
||||
log.Println("Micro started")
|
||||
} else {
|
||||
log.SetOutput(NullWriter{})
|
||||
}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
dmp "github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
const (
|
||||
// Opposite and undoing events must have opposite values
|
||||
|
||||
// TextEventInsert repreasents an insertion event
|
||||
TextEventInsert = 1
|
||||
// TextEventRemove represents a deletion event
|
||||
TextEventRemove = -1
|
||||
)
|
||||
|
||||
// TextEvent holds data for a manipulation on some text that can be undone
|
||||
type TextEvent struct {
|
||||
C Cursor
|
||||
|
||||
EventType int
|
||||
Text string
|
||||
Start Loc
|
||||
End Loc
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// ExecuteTextEvent runs a text event
|
||||
func ExecuteTextEvent(t *TextEvent, buf *Buffer) {
|
||||
if t.EventType == TextEventInsert {
|
||||
buf.insert(t.Start, []byte(t.Text))
|
||||
} else if t.EventType == TextEventRemove {
|
||||
t.Text = buf.remove(t.Start, t.End)
|
||||
}
|
||||
}
|
||||
|
||||
// UndoTextEvent undoes a text event
|
||||
func UndoTextEvent(t *TextEvent, buf *Buffer) {
|
||||
t.EventType = -t.EventType
|
||||
ExecuteTextEvent(t, buf)
|
||||
}
|
||||
|
||||
// EventHandler executes text manipulations and allows undoing and redoing
|
||||
type EventHandler struct {
|
||||
buf *Buffer
|
||||
UndoStack *Stack
|
||||
RedoStack *Stack
|
||||
}
|
||||
|
||||
// NewEventHandler returns a new EventHandler
|
||||
func NewEventHandler(buf *Buffer) *EventHandler {
|
||||
eh := new(EventHandler)
|
||||
eh.UndoStack = new(Stack)
|
||||
eh.RedoStack = new(Stack)
|
||||
eh.buf = buf
|
||||
return eh
|
||||
}
|
||||
|
||||
// ApplyDiff takes a string and runs the necessary insertion and deletion events to make
|
||||
// the buffer equal to that string
|
||||
// This means that we can transform the buffer into any string and still preserve undo/redo
|
||||
// through insert and delete events
|
||||
func (eh *EventHandler) ApplyDiff(new string) {
|
||||
differ := dmp.New()
|
||||
diff := differ.DiffMain(eh.buf.String(), new, false)
|
||||
loc := eh.buf.Start()
|
||||
for _, d := range diff {
|
||||
if d.Type == dmp.DiffDelete {
|
||||
eh.Remove(loc, loc.Move(Count(d.Text), eh.buf))
|
||||
} else {
|
||||
if d.Type == dmp.DiffInsert {
|
||||
eh.Insert(loc, d.Text)
|
||||
}
|
||||
loc = loc.Move(Count(d.Text), eh.buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert creates an insert text event and executes it
|
||||
func (eh *EventHandler) Insert(start Loc, text string) {
|
||||
e := &TextEvent{
|
||||
C: eh.buf.Cursor,
|
||||
EventType: TextEventInsert,
|
||||
Text: text,
|
||||
Start: start,
|
||||
Time: time.Now(),
|
||||
}
|
||||
eh.Execute(e)
|
||||
e.End = start.Move(Count(text), eh.buf)
|
||||
}
|
||||
|
||||
// Remove creates a remove text event and executes it
|
||||
func (eh *EventHandler) Remove(start, end Loc) {
|
||||
e := &TextEvent{
|
||||
C: eh.buf.Cursor,
|
||||
EventType: TextEventRemove,
|
||||
Start: start,
|
||||
End: end,
|
||||
Time: time.Now(),
|
||||
}
|
||||
eh.Execute(e)
|
||||
}
|
||||
|
||||
// Replace deletes from start to end and replaces it with the given string
|
||||
func (eh *EventHandler) Replace(start, end Loc, replace string) {
|
||||
eh.Remove(start, end)
|
||||
eh.Insert(start, replace)
|
||||
}
|
||||
|
||||
// Execute a textevent and add it to the undo stack
|
||||
func (eh *EventHandler) Execute(t *TextEvent) {
|
||||
if eh.RedoStack.Len() > 0 {
|
||||
eh.RedoStack = new(Stack)
|
||||
}
|
||||
eh.UndoStack.Push(t)
|
||||
ExecuteTextEvent(t, eh.buf)
|
||||
}
|
||||
|
||||
// Undo the first event in the undo stack
|
||||
func (eh *EventHandler) Undo() {
|
||||
t := eh.UndoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
||||
|
||||
eh.UndoOneEvent()
|
||||
|
||||
for {
|
||||
t = eh.UndoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if startTime-(t.Time.UnixNano()/int64(time.Millisecond)) > undoThreshold {
|
||||
return
|
||||
}
|
||||
startTime = t.Time.UnixNano() / int64(time.Millisecond)
|
||||
|
||||
eh.UndoOneEvent()
|
||||
}
|
||||
}
|
||||
|
||||
// UndoOneEvent undoes one event
|
||||
func (eh *EventHandler) UndoOneEvent() {
|
||||
// This event should be undone
|
||||
// Pop it off the stack
|
||||
t := eh.UndoStack.Pop()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Undo it
|
||||
// Modifies the text event
|
||||
UndoTextEvent(t, eh.buf)
|
||||
|
||||
// Set the cursor in the right place
|
||||
teCursor := t.C
|
||||
t.C = eh.buf.Cursor
|
||||
eh.buf.Cursor.Goto(teCursor)
|
||||
|
||||
// Push it to the redo stack
|
||||
eh.RedoStack.Push(t)
|
||||
}
|
||||
|
||||
// Redo the first event in the redo stack
|
||||
func (eh *EventHandler) Redo() {
|
||||
t := eh.RedoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
||||
|
||||
eh.RedoOneEvent()
|
||||
|
||||
for {
|
||||
t = eh.RedoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if (t.Time.UnixNano()/int64(time.Millisecond))-startTime > undoThreshold {
|
||||
return
|
||||
}
|
||||
|
||||
eh.RedoOneEvent()
|
||||
}
|
||||
}
|
||||
|
||||
// RedoOneEvent redoes one event
|
||||
func (eh *EventHandler) RedoOneEvent() {
|
||||
t := eh.RedoStack.Pop()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Modifies the text event
|
||||
UndoTextEvent(t, eh.buf)
|
||||
|
||||
teCursor := t.C
|
||||
t.C = eh.buf.Cursor
|
||||
eh.buf.Cursor.Goto(teCursor)
|
||||
|
||||
eh.UndoStack.Push(t)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package main
|
||||
|
||||
var helpPages map[string]string
|
||||
|
||||
var helpFiles = []string{
|
||||
"help",
|
||||
"keybindings",
|
||||
"plugins",
|
||||
"colors",
|
||||
"options",
|
||||
"commands",
|
||||
}
|
||||
|
||||
// LoadHelp loads the help text from inside the binary
|
||||
func LoadHelp() {
|
||||
helpPages = make(map[string]string)
|
||||
for _, file := range helpFiles {
|
||||
data, err := Asset("runtime/help/" + file + ".md")
|
||||
if err != nil {
|
||||
TermMessage("Unable to load help text", file)
|
||||
}
|
||||
helpPages[file] = string(data)
|
||||
}
|
||||
}
|
||||
@@ -1,495 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/tcell"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileTypeRules represents a complete set of syntax rules for a filetype
|
||||
type FileTypeRules struct {
|
||||
filetype string
|
||||
filename string
|
||||
text string
|
||||
}
|
||||
|
||||
// SyntaxRule represents a regex to highlight in a certain style
|
||||
type SyntaxRule struct {
|
||||
// What to highlight
|
||||
regex *regexp.Regexp
|
||||
// Any flags
|
||||
flags string
|
||||
// Whether this regex is a start=... end=... regex
|
||||
startend bool
|
||||
// How to highlight it
|
||||
style tcell.Style
|
||||
}
|
||||
|
||||
var syntaxFiles map[[2]*regexp.Regexp]FileTypeRules
|
||||
|
||||
// These syntax files are pre installed and embedded in the resulting binary by go-bindata
|
||||
var preInstalledSynFiles = []string{
|
||||
"Dockerfile",
|
||||
"apacheconf",
|
||||
"arduino",
|
||||
"asciidoc",
|
||||
"asm",
|
||||
"awk",
|
||||
"c",
|
||||
"cmake",
|
||||
"coffeescript",
|
||||
"colortest",
|
||||
"conf",
|
||||
"conky",
|
||||
"csharp",
|
||||
"css",
|
||||
"cython",
|
||||
"d",
|
||||
"dot",
|
||||
"erb",
|
||||
"fish",
|
||||
"fortran",
|
||||
"gdscript",
|
||||
"gentoo-ebuild",
|
||||
"gentoo-etc-portage",
|
||||
"git-commit",
|
||||
"git-config",
|
||||
"git-rebase-todo",
|
||||
"glsl",
|
||||
"go",
|
||||
"golo",
|
||||
"groff",
|
||||
"haml",
|
||||
"haskell",
|
||||
"html",
|
||||
"ini",
|
||||
"inputrc",
|
||||
"java",
|
||||
"javascript",
|
||||
"json",
|
||||
"keymap",
|
||||
"kickstart",
|
||||
"ledger",
|
||||
"lilypond",
|
||||
"lisp",
|
||||
"lua",
|
||||
"makefile",
|
||||
"man",
|
||||
"markdown",
|
||||
"mpdconf",
|
||||
"nanorc",
|
||||
"nginx",
|
||||
"ocaml",
|
||||
"patch",
|
||||
"peg",
|
||||
"perl",
|
||||
"perl6",
|
||||
"php",
|
||||
"pkg-config",
|
||||
"pkgbuild",
|
||||
"po",
|
||||
"pov",
|
||||
"privoxy-action",
|
||||
"privoxy-config",
|
||||
"privoxy-filter",
|
||||
"puppet",
|
||||
"python",
|
||||
"r",
|
||||
"reST",
|
||||
"rpmspec",
|
||||
"ruby",
|
||||
"rust",
|
||||
"scala",
|
||||
"sed",
|
||||
"sh",
|
||||
"sls",
|
||||
"sql",
|
||||
"swift",
|
||||
"systemd",
|
||||
"tcl",
|
||||
"tex",
|
||||
"vala",
|
||||
"vi",
|
||||
"xml",
|
||||
"xresources",
|
||||
"yaml",
|
||||
"yum",
|
||||
"zsh",
|
||||
}
|
||||
|
||||
// LoadSyntaxFiles loads the syntax files from the default directory (configDir)
|
||||
func LoadSyntaxFiles() {
|
||||
// Load the user's custom syntax files, if there are any
|
||||
LoadSyntaxFilesFromDir(configDir + "/syntax")
|
||||
|
||||
// Load the pre-installed syntax files from inside the binary
|
||||
for _, filetype := range preInstalledSynFiles {
|
||||
data, err := Asset("runtime/syntax/" + filetype + ".micro")
|
||||
if err != nil {
|
||||
TermMessage("Unable to load pre-installed syntax file " + filetype)
|
||||
continue
|
||||
}
|
||||
|
||||
LoadSyntaxFile(string(data), filetype+".micro")
|
||||
}
|
||||
}
|
||||
|
||||
// LoadSyntaxFilesFromDir loads the syntax files from a specified directory
|
||||
// To load the syntax files, we must fill the `syntaxFiles` map
|
||||
// This involves finding the regex for syntax and if it exists, the regex
|
||||
// for the header. Then we must get the text for the file and the filetype.
|
||||
func LoadSyntaxFilesFromDir(dir string) {
|
||||
colorscheme = make(Colorscheme)
|
||||
InitColorscheme()
|
||||
|
||||
// Default style
|
||||
defStyle = tcell.StyleDefault.
|
||||
Foreground(tcell.ColorDefault).
|
||||
Background(tcell.ColorDefault)
|
||||
|
||||
// There may be another default style defined in the colorscheme
|
||||
// In that case we should use that one
|
||||
if style, ok := colorscheme["default"]; ok {
|
||||
defStyle = style
|
||||
}
|
||||
if screen != nil {
|
||||
screen.SetStyle(defStyle)
|
||||
}
|
||||
|
||||
syntaxFiles = make(map[[2]*regexp.Regexp]FileTypeRules)
|
||||
files, _ := ioutil.ReadDir(dir)
|
||||
for _, f := range files {
|
||||
if filepath.Ext(f.Name()) == ".micro" {
|
||||
filename := dir + "/" + f.Name()
|
||||
text, err := ioutil.ReadFile(filename)
|
||||
|
||||
if err != nil {
|
||||
TermMessage("Error loading syntax file " + filename + ": " + err.Error())
|
||||
return
|
||||
}
|
||||
LoadSyntaxFile(string(text), filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JoinRule takes a syntax rule (which can be multiple regular expressions)
|
||||
// and joins it into one regular expression by ORing everything together
|
||||
func JoinRule(rule string) string {
|
||||
split := strings.Split(rule, `" "`)
|
||||
joined := strings.Join(split, ")|(")
|
||||
joined = "(" + joined + ")"
|
||||
return joined
|
||||
}
|
||||
|
||||
// LoadSyntaxFile simply gets the filetype of a the syntax file and the source for the
|
||||
// file and creates FileTypeRules out of it. If this filetype is the one opened by the user
|
||||
// the rules will be loaded and compiled later
|
||||
// In this function we are only concerned with loading the syntax and header regexes
|
||||
func LoadSyntaxFile(text, filename string) {
|
||||
var err error
|
||||
lines := strings.Split(string(text), "\n")
|
||||
|
||||
// Regex for parsing syntax statements
|
||||
syntaxParser := regexp.MustCompile(`syntax "(.*?)"\s+"(.*)"+`)
|
||||
// Regex for parsing header statements
|
||||
headerParser := regexp.MustCompile(`header "(.*)"`)
|
||||
|
||||
// Is there a syntax definition in this file?
|
||||
hasSyntax := syntaxParser.MatchString(text)
|
||||
// Is there a header definition in this file?
|
||||
hasHeader := headerParser.MatchString(text)
|
||||
|
||||
var syntaxRegex *regexp.Regexp
|
||||
var headerRegex *regexp.Regexp
|
||||
var filetype string
|
||||
for lineNum, line := range lines {
|
||||
if (hasSyntax == (syntaxRegex != nil)) && (hasHeader == (headerRegex != nil)) {
|
||||
// We found what we we're supposed to find
|
||||
break
|
||||
}
|
||||
|
||||
if strings.TrimSpace(line) == "" ||
|
||||
strings.TrimSpace(line)[0] == '#' {
|
||||
// Ignore this line
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "syntax") {
|
||||
// Syntax statement
|
||||
syntaxMatches := syntaxParser.FindSubmatch([]byte(line))
|
||||
if len(syntaxMatches) == 3 {
|
||||
if syntaxRegex != nil {
|
||||
TermError(filename, lineNum, "Syntax statement redeclaration")
|
||||
}
|
||||
|
||||
filetype = string(syntaxMatches[1])
|
||||
extensions := JoinRule(string(syntaxMatches[2]))
|
||||
|
||||
syntaxRegex, err = regexp.Compile(extensions)
|
||||
if err != nil {
|
||||
TermError(filename, lineNum, err.Error())
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
TermError(filename, lineNum, "Syntax statement is not valid: "+line)
|
||||
continue
|
||||
}
|
||||
} else if strings.HasPrefix(line, "header") {
|
||||
// Header statement
|
||||
headerMatches := headerParser.FindSubmatch([]byte(line))
|
||||
if len(headerMatches) == 2 {
|
||||
header := JoinRule(string(headerMatches[1]))
|
||||
|
||||
headerRegex, err = regexp.Compile(header)
|
||||
if err != nil {
|
||||
TermError(filename, lineNum, "Regex error: "+err.Error())
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
TermError(filename, lineNum, "Header statement is not valid: "+line)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if syntaxRegex != nil {
|
||||
// Add the current rules to the syntaxFiles variable
|
||||
regexes := [2]*regexp.Regexp{syntaxRegex, headerRegex}
|
||||
syntaxFiles[regexes] = FileTypeRules{filetype, filename, text}
|
||||
}
|
||||
}
|
||||
|
||||
// LoadRulesFromFile loads just the syntax rules from a given file
|
||||
// Only the necessary rules are loaded when the buffer is opened.
|
||||
// If we load all the rules for every filetype when micro starts, there's a bit of lag
|
||||
// A rule just explains how to color certain regular expressions
|
||||
// Example: color comment "//.*"
|
||||
// This would color all strings that match the regex "//.*" in the comment color defined
|
||||
// by the colorscheme
|
||||
func LoadRulesFromFile(text, filename string) []SyntaxRule {
|
||||
lines := strings.Split(string(text), "\n")
|
||||
|
||||
// Regex for parsing standard syntax rules
|
||||
ruleParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?"(.*)"`)
|
||||
// Regex for parsing syntax rules with start="..." end="..."
|
||||
ruleStartEndParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?start="(.*)"\s+end="(.*)"`)
|
||||
|
||||
var rules []SyntaxRule
|
||||
for lineNum, line := range lines {
|
||||
if strings.TrimSpace(line) == "" ||
|
||||
strings.TrimSpace(line)[0] == '#' ||
|
||||
strings.HasPrefix(line, "syntax") ||
|
||||
strings.HasPrefix(line, "header") {
|
||||
// Ignore this line
|
||||
continue
|
||||
}
|
||||
|
||||
// Syntax rule, but it could be standard or start-end
|
||||
if ruleParser.MatchString(line) {
|
||||
// Standard syntax rule
|
||||
// Parse the line
|
||||
submatch := ruleParser.FindSubmatch([]byte(line))
|
||||
var color string
|
||||
var regexStr string
|
||||
var flags string
|
||||
if len(submatch) == 4 {
|
||||
// If len is 4 then the user specified some additional flags to use
|
||||
color = string(submatch[1])
|
||||
flags = string(submatch[2])
|
||||
regexStr = "(?" + flags + ")" + JoinRule(string(submatch[3]))
|
||||
} else if len(submatch) == 3 {
|
||||
// If len is 3, no additional flags were given
|
||||
color = string(submatch[1])
|
||||
regexStr = JoinRule(string(submatch[2]))
|
||||
} else {
|
||||
// If len is not 3 or 4 there is a problem
|
||||
TermError(filename, lineNum, "Invalid statement: "+line)
|
||||
continue
|
||||
}
|
||||
// Compile the regex
|
||||
regex, err := regexp.Compile(regexStr)
|
||||
if err != nil {
|
||||
TermError(filename, lineNum, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the style
|
||||
// The user could give us a "color" that is really a part of the colorscheme
|
||||
// in which case we should look that up in the colorscheme
|
||||
// They can also just give us a straight up color
|
||||
st := defStyle
|
||||
groups := strings.Split(color, ".")
|
||||
if len(groups) > 1 {
|
||||
curGroup := ""
|
||||
for i, g := range groups {
|
||||
if i != 0 {
|
||||
curGroup += "."
|
||||
}
|
||||
curGroup += g
|
||||
if style, ok := colorscheme[curGroup]; ok {
|
||||
st = style
|
||||
}
|
||||
}
|
||||
} else if style, ok := colorscheme[color]; ok {
|
||||
st = style
|
||||
} else {
|
||||
st = StringToStyle(color)
|
||||
}
|
||||
// Add the regex, flags, and style
|
||||
// False because this is not start-end
|
||||
rules = append(rules, SyntaxRule{regex, flags, false, st})
|
||||
} else if ruleStartEndParser.MatchString(line) {
|
||||
// Start-end syntax rule
|
||||
submatch := ruleStartEndParser.FindSubmatch([]byte(line))
|
||||
var color string
|
||||
var start string
|
||||
var end string
|
||||
// Use m and s flags by default
|
||||
flags := "ms"
|
||||
if len(submatch) == 5 {
|
||||
// If len is 5 the user provided some additional flags
|
||||
color = string(submatch[1])
|
||||
flags += string(submatch[2])
|
||||
start = string(submatch[3])
|
||||
end = string(submatch[4])
|
||||
} else if len(submatch) == 4 {
|
||||
// If len is 4 the user did not provide additional flags
|
||||
color = string(submatch[1])
|
||||
start = string(submatch[2])
|
||||
end = string(submatch[3])
|
||||
} else {
|
||||
// If len is not 4 or 5 there is a problem
|
||||
TermError(filename, lineNum, "Invalid statement: "+line)
|
||||
continue
|
||||
}
|
||||
|
||||
// Compile the regex
|
||||
regex, err := regexp.Compile("(?" + flags + ")" + "(" + start + ").*?(" + end + ")")
|
||||
if err != nil {
|
||||
TermError(filename, lineNum, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the style
|
||||
// The user could give us a "color" that is really a part of the colorscheme
|
||||
// in which case we should look that up in the colorscheme
|
||||
// They can also just give us a straight up color
|
||||
st := defStyle
|
||||
if _, ok := colorscheme[color]; ok {
|
||||
st = colorscheme[color]
|
||||
} else {
|
||||
st = StringToStyle(color)
|
||||
}
|
||||
// Add the regex, flags, and style
|
||||
// True because this is start-end
|
||||
rules = append(rules, SyntaxRule{regex, flags, true, st})
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
// FindFileType finds the filetype for the given buffer
|
||||
func FindFileType(buf *Buffer) string {
|
||||
for r := range syntaxFiles {
|
||||
if r[0] != nil && r[0].MatchString(buf.Path) {
|
||||
// The syntax statement matches the extension
|
||||
return syntaxFiles[r].filetype
|
||||
} else if r[1] != nil && r[1].MatchString(buf.Line(0)) {
|
||||
// The header statement matches the first line
|
||||
return syntaxFiles[r].filetype
|
||||
}
|
||||
}
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// GetRules finds the syntax rules that should be used for the buffer
|
||||
// and returns them. It also returns the filetype of the file
|
||||
func GetRules(buf *Buffer) []SyntaxRule {
|
||||
for r := range syntaxFiles {
|
||||
if syntaxFiles[r].filetype == buf.FileType() {
|
||||
return LoadRulesFromFile(syntaxFiles[r].text, syntaxFiles[r].filename)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyntaxMatches is an alias to a map from character numbers to styles,
|
||||
// so map[3] represents the style of the third character
|
||||
type SyntaxMatches [][]tcell.Style
|
||||
|
||||
// Match takes a buffer and returns the syntax matches: a 2d array specifying how it should be syntax highlighted
|
||||
// We match the rules from up `synLinesUp` lines and down `synLinesDown` lines
|
||||
func Match(v *View) SyntaxMatches {
|
||||
buf := v.Buf
|
||||
rules := v.Buf.rules
|
||||
|
||||
viewStart := v.Topline
|
||||
viewEnd := v.Topline + v.height
|
||||
if viewEnd > buf.NumLines {
|
||||
viewEnd = buf.NumLines
|
||||
}
|
||||
|
||||
lines := buf.Lines(viewStart, viewEnd)
|
||||
matches := make(SyntaxMatches, len(lines))
|
||||
|
||||
for i, line := range lines {
|
||||
matches[i] = make([]tcell.Style, len(line)+1)
|
||||
for j := range matches[i] {
|
||||
matches[i][j] = defStyle
|
||||
}
|
||||
}
|
||||
|
||||
// We don't actually check the entire buffer, just from synLinesUp to synLinesDown
|
||||
totalStart := v.Topline - synLinesUp
|
||||
totalEnd := v.Topline + v.height + synLinesDown
|
||||
if totalStart < 0 {
|
||||
totalStart = 0
|
||||
}
|
||||
if totalEnd > buf.NumLines {
|
||||
totalEnd = buf.NumLines
|
||||
}
|
||||
|
||||
str := strings.Join(buf.Lines(totalStart, totalEnd), "\n")
|
||||
startNum := ToCharPos(Loc{0, totalStart}, v.Buf)
|
||||
|
||||
for _, rule := range rules {
|
||||
if rule.startend {
|
||||
if indicies := rule.regex.FindAllStringIndex(str, -1); indicies != nil {
|
||||
for _, value := range indicies {
|
||||
value[0] = runePos(value[0], str) + startNum
|
||||
value[1] = runePos(value[1], str) + startNum
|
||||
startLoc := FromCharPos(value[0], buf)
|
||||
endLoc := FromCharPos(value[1], buf)
|
||||
for curLoc := startLoc; curLoc.LessThan(endLoc); curLoc = curLoc.Move(1, buf) {
|
||||
if curLoc.Y < v.Topline {
|
||||
continue
|
||||
}
|
||||
colNum, lineNum := curLoc.X, curLoc.Y
|
||||
if lineNum == -1 || colNum == -1 {
|
||||
continue
|
||||
}
|
||||
lineNum -= viewStart
|
||||
if lineNum >= 0 && lineNum < v.height {
|
||||
matches[lineNum][colNum] = rule.style
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for lineN, line := range lines {
|
||||
if indicies := rule.regex.FindAllStringIndex(line, -1); indicies != nil {
|
||||
for _, value := range indicies {
|
||||
start := runePos(value[0], line)
|
||||
end := runePos(value[1], line)
|
||||
for i := start; i < end; i++ {
|
||||
matches[lineN][i] = rule.style
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
149
cmd/micro/initlua.go
Normal file
149
cmd/micro/initlua.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
luar "layeh.com/gopher-luar"
|
||||
|
||||
"github.com/zyedidia/micro/internal/action"
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/display"
|
||||
ulua "github.com/zyedidia/micro/internal/lua"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/shell"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ulua.L = lua.NewState()
|
||||
ulua.L.SetGlobal("import", luar.New(ulua.L, LuaImport))
|
||||
}
|
||||
|
||||
// LuaImport is meant to be called from lua by a plugin and will import the given micro package
|
||||
func LuaImport(pkg string) *lua.LTable {
|
||||
switch pkg {
|
||||
case "micro":
|
||||
return luaImportMicro()
|
||||
case "micro/shell":
|
||||
return luaImportMicroShell()
|
||||
case "micro/buffer":
|
||||
return luaImportMicroBuffer()
|
||||
case "micro/config":
|
||||
return luaImportMicroConfig()
|
||||
case "micro/util":
|
||||
return luaImportMicroUtil()
|
||||
default:
|
||||
return ulua.Import(pkg)
|
||||
}
|
||||
}
|
||||
|
||||
func luaImportMicro() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "TermMessage", luar.New(ulua.L, screen.TermMessage))
|
||||
ulua.L.SetField(pkg, "TermError", luar.New(ulua.L, screen.TermError))
|
||||
ulua.L.SetField(pkg, "InfoBar", luar.New(ulua.L, action.GetInfoBar))
|
||||
ulua.L.SetField(pkg, "Log", luar.New(ulua.L, log.Println))
|
||||
ulua.L.SetField(pkg, "SetStatusInfoFn", luar.New(ulua.L, display.SetStatusInfoFnLua))
|
||||
ulua.L.SetField(pkg, "CurPane", luar.New(ulua.L, func() action.Pane {
|
||||
return action.MainTab().CurPane()
|
||||
}))
|
||||
ulua.L.SetField(pkg, "CurTab", luar.New(ulua.L, func() *action.Tab {
|
||||
return action.MainTab()
|
||||
}))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func luaImportMicroConfig() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "MakeCommand", luar.New(ulua.L, action.MakeCommand))
|
||||
ulua.L.SetField(pkg, "FileComplete", luar.New(ulua.L, buffer.FileComplete))
|
||||
ulua.L.SetField(pkg, "HelpComplete", luar.New(ulua.L, action.HelpComplete))
|
||||
ulua.L.SetField(pkg, "OptionComplete", luar.New(ulua.L, action.OptionComplete))
|
||||
ulua.L.SetField(pkg, "OptionValueComplete", luar.New(ulua.L, action.OptionValueComplete))
|
||||
ulua.L.SetField(pkg, "NoComplete", luar.New(ulua.L, nil))
|
||||
ulua.L.SetField(pkg, "TryBindKey", luar.New(ulua.L, action.TryBindKey))
|
||||
ulua.L.SetField(pkg, "Reload", luar.New(ulua.L, action.ReloadConfig))
|
||||
ulua.L.SetField(pkg, "AddRuntimeFileFromMemory", luar.New(ulua.L, config.PluginAddRuntimeFileFromMemory))
|
||||
ulua.L.SetField(pkg, "AddRuntimeFilesFromDirectory", luar.New(ulua.L, config.PluginAddRuntimeFilesFromDirectory))
|
||||
ulua.L.SetField(pkg, "AddRuntimeFile", luar.New(ulua.L, config.PluginAddRuntimeFile))
|
||||
ulua.L.SetField(pkg, "ListRuntimeFiles", luar.New(ulua.L, config.PluginListRuntimeFiles))
|
||||
ulua.L.SetField(pkg, "ReadRuntimeFile", luar.New(ulua.L, config.PluginReadRuntimeFile))
|
||||
ulua.L.SetField(pkg, "NewRTFiletype", luar.New(ulua.L, config.NewRTFiletype))
|
||||
ulua.L.SetField(pkg, "RTColorscheme", luar.New(ulua.L, config.RTColorscheme))
|
||||
ulua.L.SetField(pkg, "RTSyntax", luar.New(ulua.L, config.RTSyntax))
|
||||
ulua.L.SetField(pkg, "RTHelp", luar.New(ulua.L, config.RTHelp))
|
||||
ulua.L.SetField(pkg, "RTPlugin", luar.New(ulua.L, config.RTPlugin))
|
||||
ulua.L.SetField(pkg, "RegisterCommonOption", luar.New(ulua.L, config.RegisterCommonOptionPlug))
|
||||
ulua.L.SetField(pkg, "RegisterGlobalOption", luar.New(ulua.L, config.RegisterGlobalOptionPlug))
|
||||
ulua.L.SetField(pkg, "GetGlobalOption", luar.New(ulua.L, config.GetGlobalOption))
|
||||
ulua.L.SetField(pkg, "SetGlobalOption", luar.New(ulua.L, action.SetGlobalOption))
|
||||
ulua.L.SetField(pkg, "SetGlobalOptionNative", luar.New(ulua.L, action.SetGlobalOptionNative))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func luaImportMicroShell() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "ExecCommand", luar.New(ulua.L, shell.ExecCommand))
|
||||
ulua.L.SetField(pkg, "RunCommand", luar.New(ulua.L, shell.RunCommand))
|
||||
ulua.L.SetField(pkg, "RunBackgroundShell", luar.New(ulua.L, shell.RunBackgroundShell))
|
||||
ulua.L.SetField(pkg, "RunInteractiveShell", luar.New(ulua.L, shell.RunInteractiveShell))
|
||||
ulua.L.SetField(pkg, "JobStart", luar.New(ulua.L, shell.JobStart))
|
||||
ulua.L.SetField(pkg, "JobSpawn", luar.New(ulua.L, shell.JobSpawn))
|
||||
ulua.L.SetField(pkg, "JobStop", luar.New(ulua.L, shell.JobStop))
|
||||
ulua.L.SetField(pkg, "JobSend", luar.New(ulua.L, shell.JobSend))
|
||||
ulua.L.SetField(pkg, "RunTermEmulator", luar.New(ulua.L, action.RunTermEmulator))
|
||||
ulua.L.SetField(pkg, "TermEmuSupported", luar.New(ulua.L, action.TermEmuSupported))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func luaImportMicroBuffer() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "NewMessage", luar.New(ulua.L, buffer.NewMessage))
|
||||
ulua.L.SetField(pkg, "NewMessageAtLine", luar.New(ulua.L, buffer.NewMessageAtLine))
|
||||
ulua.L.SetField(pkg, "MTInfo", luar.New(ulua.L, buffer.MTInfo))
|
||||
ulua.L.SetField(pkg, "MTWarning", luar.New(ulua.L, buffer.MTWarning))
|
||||
ulua.L.SetField(pkg, "MTError", luar.New(ulua.L, buffer.MTError))
|
||||
ulua.L.SetField(pkg, "Loc", luar.New(ulua.L, func(x, y int) buffer.Loc {
|
||||
return buffer.Loc{x, y}
|
||||
}))
|
||||
ulua.L.SetField(pkg, "BTDefault", luar.New(ulua.L, buffer.BTDefault.Kind))
|
||||
ulua.L.SetField(pkg, "BTHelp", luar.New(ulua.L, buffer.BTHelp.Kind))
|
||||
ulua.L.SetField(pkg, "BTLog", luar.New(ulua.L, buffer.BTLog.Kind))
|
||||
ulua.L.SetField(pkg, "BTScratch", luar.New(ulua.L, buffer.BTScratch.Kind))
|
||||
ulua.L.SetField(pkg, "BTRaw", luar.New(ulua.L, buffer.BTRaw.Kind))
|
||||
ulua.L.SetField(pkg, "BTInfo", luar.New(ulua.L, buffer.BTInfo.Kind))
|
||||
ulua.L.SetField(pkg, "NewBuffer", luar.New(ulua.L, func(text, path string) *buffer.Buffer {
|
||||
return buffer.NewBufferFromString(text, path, buffer.BTDefault)
|
||||
}))
|
||||
ulua.L.SetField(pkg, "NewBufferFromFile", luar.New(ulua.L, func(path string) (*buffer.Buffer, error) {
|
||||
return buffer.NewBufferFromFile(path, buffer.BTDefault)
|
||||
}))
|
||||
ulua.L.SetField(pkg, "ByteOffset", luar.New(ulua.L, buffer.ByteOffset))
|
||||
ulua.L.SetField(pkg, "Log", luar.New(ulua.L, buffer.WriteLog))
|
||||
ulua.L.SetField(pkg, "LogBuf", luar.New(ulua.L, buffer.GetLogBuf))
|
||||
|
||||
return pkg
|
||||
}
|
||||
|
||||
func luaImportMicroUtil() *lua.LTable {
|
||||
pkg := ulua.L.NewTable()
|
||||
|
||||
ulua.L.SetField(pkg, "RuneAt", luar.New(ulua.L, util.LuaRuneAt))
|
||||
ulua.L.SetField(pkg, "GetLeadingWhitespace", luar.New(ulua.L, util.LuaGetLeadingWhitespace))
|
||||
ulua.L.SetField(pkg, "IsWordChar", luar.New(ulua.L, util.LuaIsWordChar))
|
||||
ulua.L.SetField(pkg, "String", luar.New(ulua.L, util.String))
|
||||
ulua.L.SetField(pkg, "RuneStr", luar.New(ulua.L, func(r rune) string {
|
||||
return string(r)
|
||||
}))
|
||||
|
||||
return pkg
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func runeToByteIndex(n int, txt []byte) int {
|
||||
if n == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
count := 0
|
||||
i := 0
|
||||
for len(txt) > 0 {
|
||||
_, size := utf8.DecodeRune(txt)
|
||||
|
||||
txt = txt[size:]
|
||||
count += size
|
||||
i++
|
||||
|
||||
if i == n {
|
||||
break
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// A LineArray simply stores and array of lines and makes it easy to insert
|
||||
// and delete in it
|
||||
type LineArray struct {
|
||||
lines [][]byte
|
||||
}
|
||||
|
||||
// NewLineArray returns a new line array from an array of bytes
|
||||
func NewLineArray(text []byte) *LineArray {
|
||||
la := new(LineArray)
|
||||
// Split the bytes into lines
|
||||
split := bytes.Split(text, []byte("\n"))
|
||||
la.lines = make([][]byte, len(split))
|
||||
for i := range split {
|
||||
la.lines[i] = make([]byte, len(split[i]))
|
||||
copy(la.lines[i], split[i])
|
||||
}
|
||||
|
||||
return la
|
||||
}
|
||||
|
||||
// Returns the String representation of the LineArray
|
||||
func (la *LineArray) String() string {
|
||||
return string(bytes.Join(la.lines, []byte("\n")))
|
||||
}
|
||||
|
||||
// NewlineBelow adds a newline below the given line number
|
||||
func (la *LineArray) NewlineBelow(y int) {
|
||||
la.lines = append(la.lines, []byte(" "))
|
||||
copy(la.lines[y+2:], la.lines[y+1:])
|
||||
la.lines[y+1] = []byte("")
|
||||
}
|
||||
|
||||
// inserts a byte array at a given location
|
||||
func (la *LineArray) insert(pos Loc, value []byte) {
|
||||
x, y := runeToByteIndex(pos.X, la.lines[pos.Y]), pos.Y
|
||||
// x, y := pos.x, pos.y
|
||||
for i := 0; i < len(value); i++ {
|
||||
if value[i] == '\n' {
|
||||
la.Split(Loc{x, y})
|
||||
x = 0
|
||||
y++
|
||||
continue
|
||||
}
|
||||
la.insertByte(Loc{x, y}, value[i])
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
// inserts a byte at a given location
|
||||
func (la *LineArray) insertByte(pos Loc, value byte) {
|
||||
la.lines[pos.Y] = append(la.lines[pos.Y], 0)
|
||||
copy(la.lines[pos.Y][pos.X+1:], la.lines[pos.Y][pos.X:])
|
||||
la.lines[pos.Y][pos.X] = value
|
||||
}
|
||||
|
||||
// JoinLines joins the two lines a and b
|
||||
func (la *LineArray) JoinLines(a, b int) {
|
||||
la.insert(Loc{len(la.lines[a]), a}, la.lines[b])
|
||||
la.DeleteLine(b)
|
||||
}
|
||||
|
||||
// Split splits a line at a given position
|
||||
func (la *LineArray) Split(pos Loc) {
|
||||
la.NewlineBelow(pos.Y)
|
||||
la.insert(Loc{0, pos.Y + 1}, la.lines[pos.Y][pos.X:])
|
||||
la.DeleteToEnd(Loc{pos.X, pos.Y})
|
||||
}
|
||||
|
||||
// removes from start to end
|
||||
func (la *LineArray) remove(start, end Loc) string {
|
||||
sub := la.Substr(start, end)
|
||||
startX := runeToByteIndex(start.X, la.lines[start.Y])
|
||||
endX := runeToByteIndex(end.X, la.lines[end.Y])
|
||||
if start.Y == end.Y {
|
||||
la.lines[start.Y] = append(la.lines[start.Y][:startX], la.lines[start.Y][endX:]...)
|
||||
} else {
|
||||
for i := start.Y + 1; i <= end.Y-1; i++ {
|
||||
la.DeleteLine(start.Y + 1)
|
||||
}
|
||||
la.DeleteToEnd(Loc{startX, start.Y})
|
||||
la.DeleteFromStart(Loc{endX - 1, start.Y + 1})
|
||||
la.JoinLines(start.Y, start.Y+1)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
// DeleteToEnd deletes from the end of a line to the position
|
||||
func (la *LineArray) DeleteToEnd(pos Loc) {
|
||||
la.lines[pos.Y] = la.lines[pos.Y][:pos.X]
|
||||
}
|
||||
|
||||
// DeleteFromStart deletes from the start of a line to the position
|
||||
func (la *LineArray) DeleteFromStart(pos Loc) {
|
||||
la.lines[pos.Y] = la.lines[pos.Y][pos.X+1:]
|
||||
}
|
||||
|
||||
// DeleteLine deletes the line number
|
||||
func (la *LineArray) DeleteLine(y int) {
|
||||
la.lines = la.lines[:y+copy(la.lines[y:], la.lines[y+1:])]
|
||||
}
|
||||
|
||||
// DeleteByte deletes the byte at a position
|
||||
func (la *LineArray) DeleteByte(pos Loc) {
|
||||
la.lines[pos.Y] = la.lines[pos.Y][:pos.X+copy(la.lines[pos.Y][pos.X:], la.lines[pos.Y][pos.X+1:])]
|
||||
}
|
||||
|
||||
// Substr returns the string representation between two locations
|
||||
func (la *LineArray) Substr(start, end Loc) string {
|
||||
startX := runeToByteIndex(start.X, la.lines[start.Y])
|
||||
endX := runeToByteIndex(end.X, la.lines[end.Y])
|
||||
if start.Y == end.Y {
|
||||
return string(la.lines[start.Y][startX:endX])
|
||||
}
|
||||
var str string
|
||||
str += string(la.lines[start.Y][startX:]) + "\n"
|
||||
for i := start.Y + 1; i <= end.Y-1; i++ {
|
||||
str += string(la.lines[i]) + "\n"
|
||||
}
|
||||
str += string(la.lines[end.Y][:endX])
|
||||
return str
|
||||
}
|
||||
127
cmd/micro/loc.go
127
cmd/micro/loc.go
@@ -1,127 +0,0 @@
|
||||
package main
|
||||
|
||||
// FromCharPos converts from a character position to an x, y position
|
||||
func FromCharPos(loc int, buf *Buffer) Loc {
|
||||
charNum := 0
|
||||
x, y := 0, 0
|
||||
|
||||
lineLen := Count(buf.Line(y)) + 1
|
||||
for charNum+lineLen <= loc {
|
||||
charNum += lineLen
|
||||
y++
|
||||
lineLen = Count(buf.Line(y)) + 1
|
||||
}
|
||||
x = loc - charNum
|
||||
|
||||
return Loc{x, y}
|
||||
}
|
||||
|
||||
// ToCharPos converts from an x, y position to a character position
|
||||
func ToCharPos(start Loc, buf *Buffer) int {
|
||||
x, y := start.X, start.Y
|
||||
loc := 0
|
||||
for i := 0; i < y; i++ {
|
||||
// + 1 for the newline
|
||||
loc += Count(buf.Line(i)) + 1
|
||||
}
|
||||
loc += x
|
||||
return loc
|
||||
}
|
||||
|
||||
// Loc stores a location
|
||||
type Loc struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
// LessThan returns true if b is smaller
|
||||
func (l Loc) LessThan(b Loc) bool {
|
||||
if l.Y < b.Y {
|
||||
return true
|
||||
}
|
||||
if l.Y == b.Y && l.X < b.X {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GreaterThan returns true if b is bigger
|
||||
func (l Loc) GreaterThan(b Loc) bool {
|
||||
if l.Y > b.Y {
|
||||
return true
|
||||
}
|
||||
if l.Y == b.Y && l.X > b.X {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GreaterEqual returns true if b is greater than or equal to b
|
||||
func (l Loc) GreaterEqual(b Loc) bool {
|
||||
if l.Y > b.Y {
|
||||
return true
|
||||
}
|
||||
if l.Y == b.Y && l.X > b.X {
|
||||
return true
|
||||
}
|
||||
if l == b {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// LessEqual returns true if b is less than or equal to b
|
||||
func (l Loc) LessEqual(b Loc) bool {
|
||||
if l.Y < b.Y {
|
||||
return true
|
||||
}
|
||||
if l.Y == b.Y && l.X < b.X {
|
||||
return true
|
||||
}
|
||||
if l == b {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// This moves the location one character to the right
|
||||
func (l Loc) right(buf *Buffer) Loc {
|
||||
if l == buf.End() {
|
||||
return Loc{l.X + 1, l.Y}
|
||||
}
|
||||
var res Loc
|
||||
if l.X < Count(buf.Line(l.Y)) {
|
||||
res = Loc{l.X + 1, l.Y}
|
||||
} else {
|
||||
res = Loc{0, l.Y + 1}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// This moves the given location one character to the left
|
||||
func (l Loc) left(buf *Buffer) Loc {
|
||||
if l == buf.Start() {
|
||||
return Loc{l.X - 1, l.Y}
|
||||
}
|
||||
var res Loc
|
||||
if l.X > 0 {
|
||||
res = Loc{l.X - 1, l.Y}
|
||||
} else {
|
||||
res = Loc{Count(buf.Line(l.Y - 1)), l.Y - 1}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Move moves the cursor n characters to the left or right
|
||||
// It moves the cursor left if n is negative
|
||||
func (l Loc) Move(n int, buf *Buffer) Loc {
|
||||
if n > 0 {
|
||||
for i := 0; i < n; i++ {
|
||||
l = l.right(buf)
|
||||
}
|
||||
return l
|
||||
}
|
||||
for i := 0; i < Abs(n); i++ {
|
||||
l = l.left(buf)
|
||||
}
|
||||
return l
|
||||
}
|
||||
@@ -1,376 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/zyedidia/clipboard"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
// TermMessage sends a message to the user in the terminal. This usually occurs before
|
||||
// micro has been fully initialized -- ie if there is an error in the syntax highlighting
|
||||
// regular expressions
|
||||
// The function must be called when the screen is not initialized
|
||||
// This will write the message, and wait for the user
|
||||
// to press and key to continue
|
||||
func TermMessage(msg ...interface{}) {
|
||||
screenWasNil := screen == nil
|
||||
if !screenWasNil {
|
||||
screen.Fini()
|
||||
}
|
||||
|
||||
fmt.Println(msg...)
|
||||
fmt.Print("\nPress enter to continue")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
reader.ReadString('\n')
|
||||
|
||||
if !screenWasNil {
|
||||
InitScreen()
|
||||
}
|
||||
}
|
||||
|
||||
// TermError sends an error to the user in the terminal. Like TermMessage except formatted
|
||||
// as an error
|
||||
func TermError(filename string, lineNum int, err string) {
|
||||
TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err)
|
||||
}
|
||||
|
||||
// Messenger is an object that makes it easy to send messages to the user
|
||||
// and get input from the user
|
||||
type Messenger struct {
|
||||
// Are we currently prompting the user?
|
||||
hasPrompt bool
|
||||
// Is there a message to print
|
||||
hasMessage bool
|
||||
|
||||
// Message to print
|
||||
message string
|
||||
// The user's response to a prompt
|
||||
response string
|
||||
// style to use when drawing the message
|
||||
style tcell.Style
|
||||
|
||||
// We have to keep track of the cursor for prompting
|
||||
cursorx int
|
||||
|
||||
// This map stores the history for all the different kinds of uses Prompt has
|
||||
// It's a map of history type -> history array
|
||||
history map[string][]string
|
||||
historyNum int
|
||||
|
||||
// Is the current message a message from the gutter
|
||||
gutterMessage bool
|
||||
}
|
||||
|
||||
// Message sends a message to the user
|
||||
func (m *Messenger) Message(msg ...interface{}) {
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprint(buf, msg...)
|
||||
m.message = buf.String()
|
||||
m.style = defStyle
|
||||
|
||||
if _, ok := colorscheme["message"]; ok {
|
||||
m.style = colorscheme["message"]
|
||||
}
|
||||
m.hasMessage = true
|
||||
}
|
||||
|
||||
// Error sends an error message to the user
|
||||
func (m *Messenger) Error(msg ...interface{}) {
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprint(buf, msg...)
|
||||
m.message = buf.String()
|
||||
m.style = defStyle.
|
||||
Foreground(tcell.ColorBlack).
|
||||
Background(tcell.ColorMaroon)
|
||||
|
||||
if _, ok := colorscheme["error-message"]; ok {
|
||||
m.style = colorscheme["error-message"]
|
||||
}
|
||||
m.hasMessage = true
|
||||
}
|
||||
|
||||
// YesNoPrompt asks the user a yes or no question (waits for y or n) and returns the result
|
||||
func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) {
|
||||
m.Message(prompt)
|
||||
|
||||
_, h := screen.Size()
|
||||
for {
|
||||
m.Clear()
|
||||
m.Display()
|
||||
screen.ShowCursor(Count(m.message), h-1)
|
||||
screen.Show()
|
||||
event := <-events
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
switch e.Key() {
|
||||
case tcell.KeyRune:
|
||||
if e.Rune() == 'y' {
|
||||
return true, false
|
||||
} else if e.Rune() == 'n' {
|
||||
return false, false
|
||||
}
|
||||
case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
|
||||
return false, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LetterPrompt gives the user a prompt and waits for a one letter response
|
||||
func (m *Messenger) LetterPrompt(prompt string, responses ...rune) (rune, bool) {
|
||||
m.Message(prompt)
|
||||
|
||||
_, h := screen.Size()
|
||||
for {
|
||||
m.Clear()
|
||||
m.Display()
|
||||
screen.ShowCursor(Count(m.message), h-1)
|
||||
screen.Show()
|
||||
event := <-events
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
switch e.Key() {
|
||||
case tcell.KeyRune:
|
||||
for _, r := range responses {
|
||||
if e.Rune() == r {
|
||||
m.Reset()
|
||||
return r, false
|
||||
}
|
||||
}
|
||||
case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
|
||||
return ' ', true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Completion int
|
||||
|
||||
const (
|
||||
NoCompletion Completion = iota
|
||||
FileCompletion
|
||||
CommandCompletion
|
||||
HelpCompletion
|
||||
OptionCompletion
|
||||
)
|
||||
|
||||
// Prompt sends the user a message and waits for a response to be typed in
|
||||
// This function blocks the main loop while waiting for input
|
||||
func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Completion) (string, bool) {
|
||||
m.hasPrompt = true
|
||||
m.Message(prompt)
|
||||
if _, ok := m.history[historyType]; !ok {
|
||||
m.history[historyType] = []string{""}
|
||||
} else {
|
||||
m.history[historyType] = append(m.history[historyType], "")
|
||||
}
|
||||
m.historyNum = len(m.history[historyType]) - 1
|
||||
|
||||
response, canceled := "", true
|
||||
|
||||
RedrawAll()
|
||||
for m.hasPrompt {
|
||||
var suggestions []string
|
||||
m.Clear()
|
||||
|
||||
event := <-events
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
switch e.Key() {
|
||||
case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
|
||||
// Cancel
|
||||
m.hasPrompt = false
|
||||
case tcell.KeyEnter:
|
||||
// User is done entering their response
|
||||
m.hasPrompt = false
|
||||
response, canceled = m.response, false
|
||||
m.history[historyType][len(m.history[historyType])-1] = response
|
||||
case tcell.KeyTab:
|
||||
args := strings.Split(m.response, " ")
|
||||
currentArgNum := len(args) - 1
|
||||
currentArg := args[currentArgNum]
|
||||
var completionType Completion
|
||||
|
||||
if completionTypes[0] == CommandCompletion && currentArgNum > 0 {
|
||||
if command, ok := commands[args[0]]; ok {
|
||||
completionTypes = append([]Completion{CommandCompletion}, command.completions...)
|
||||
}
|
||||
}
|
||||
|
||||
if currentArgNum >= len(completionTypes) {
|
||||
completionType = completionTypes[len(completionTypes)-1]
|
||||
} else {
|
||||
completionType = completionTypes[currentArgNum]
|
||||
}
|
||||
|
||||
var chosen string
|
||||
if completionType == FileCompletion {
|
||||
chosen, suggestions = FileComplete(currentArg)
|
||||
} else if completionType == CommandCompletion {
|
||||
chosen, suggestions = CommandComplete(currentArg)
|
||||
} else if completionType == HelpCompletion {
|
||||
chosen, suggestions = HelpComplete(currentArg)
|
||||
} else if completionType == OptionCompletion {
|
||||
chosen, suggestions = OptionComplete(currentArg)
|
||||
}
|
||||
|
||||
if len(suggestions) > 1 {
|
||||
chosen = chosen + CommonSubstring(suggestions...)
|
||||
}
|
||||
|
||||
if chosen != "" {
|
||||
if len(args) > 1 {
|
||||
chosen = " " + chosen
|
||||
}
|
||||
m.response = strings.Join(args[:len(args)-1], " ") + chosen
|
||||
m.cursorx = Count(m.response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.HandleEvent(event, m.history[historyType])
|
||||
|
||||
messenger.Clear()
|
||||
for _, v := range tabs[curTab].views {
|
||||
v.Display()
|
||||
}
|
||||
DisplayTabs()
|
||||
messenger.Display()
|
||||
if len(suggestions) > 1 {
|
||||
m.DisplaySuggestions(suggestions)
|
||||
}
|
||||
screen.Show()
|
||||
}
|
||||
|
||||
m.Reset()
|
||||
return response, canceled
|
||||
}
|
||||
|
||||
// HandleEvent handles an event for the prompter
|
||||
func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
switch e.Key() {
|
||||
case tcell.KeyUp:
|
||||
if m.historyNum > 0 {
|
||||
m.historyNum--
|
||||
m.response = history[m.historyNum]
|
||||
m.cursorx = Count(m.response)
|
||||
}
|
||||
case tcell.KeyDown:
|
||||
if m.historyNum < len(history)-1 {
|
||||
m.historyNum++
|
||||
m.response = history[m.historyNum]
|
||||
m.cursorx = Count(m.response)
|
||||
}
|
||||
case tcell.KeyLeft:
|
||||
if m.cursorx > 0 {
|
||||
m.cursorx--
|
||||
}
|
||||
case tcell.KeyRight:
|
||||
if m.cursorx < Count(m.response) {
|
||||
m.cursorx++
|
||||
}
|
||||
case tcell.KeyBackspace2, tcell.KeyBackspace:
|
||||
if m.cursorx > 0 {
|
||||
m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
|
||||
m.cursorx--
|
||||
}
|
||||
case tcell.KeyCtrlV:
|
||||
clip, _ := clipboard.ReadAll()
|
||||
m.response = Insert(m.response, m.cursorx, clip)
|
||||
m.cursorx += Count(clip)
|
||||
case tcell.KeyRune:
|
||||
m.response = Insert(m.response, m.cursorx, string(e.Rune()))
|
||||
m.cursorx++
|
||||
}
|
||||
history[m.historyNum] = m.response
|
||||
|
||||
case *tcell.EventPaste:
|
||||
clip := e.Text()
|
||||
m.response = Insert(m.response, m.cursorx, clip)
|
||||
m.cursorx += Count(clip)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset resets the messenger's cursor, message and response
|
||||
func (m *Messenger) Reset() {
|
||||
m.cursorx = 0
|
||||
m.message = ""
|
||||
m.response = ""
|
||||
}
|
||||
|
||||
// Clear clears the line at the bottom of the editor
|
||||
func (m *Messenger) Clear() {
|
||||
w, h := screen.Size()
|
||||
for x := 0; x < w; x++ {
|
||||
screen.SetContent(x, h-1, ' ', nil, defStyle)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Messenger) DisplaySuggestions(suggestions []string) {
|
||||
w, screenH := screen.Size()
|
||||
|
||||
y := screenH - 2
|
||||
|
||||
statusLineStyle := defStyle.Reverse(true)
|
||||
if style, ok := colorscheme["statusline"]; ok {
|
||||
statusLineStyle = style
|
||||
}
|
||||
|
||||
for x := 0; x < w; x++ {
|
||||
screen.SetContent(x, y, ' ', nil, statusLineStyle)
|
||||
}
|
||||
|
||||
x := 0
|
||||
for _, suggestion := range suggestions {
|
||||
for _, c := range suggestion {
|
||||
screen.SetContent(x, y, c, nil, statusLineStyle)
|
||||
x++
|
||||
}
|
||||
screen.SetContent(x, y, ' ', nil, statusLineStyle)
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
// Display displays messages or prompts
|
||||
func (m *Messenger) Display() {
|
||||
_, h := screen.Size()
|
||||
if m.hasMessage {
|
||||
runes := []rune(m.message + m.response)
|
||||
for x := 0; x < len(runes); x++ {
|
||||
screen.SetContent(x, h-1, runes[x], nil, m.style)
|
||||
}
|
||||
}
|
||||
if m.hasPrompt {
|
||||
screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
|
||||
screen.Show()
|
||||
}
|
||||
}
|
||||
|
||||
// A GutterMessage is a message displayed on the side of the editor
|
||||
type GutterMessage struct {
|
||||
lineNum int
|
||||
msg string
|
||||
kind int
|
||||
}
|
||||
|
||||
// These are the different types of messages
|
||||
const (
|
||||
// GutterInfo represents a simple info message
|
||||
GutterInfo = iota
|
||||
// GutterWarning represents a compiler warning
|
||||
GutterWarning
|
||||
// GutterError represents a compiler error
|
||||
GutterError
|
||||
)
|
||||
@@ -5,70 +5,136 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/layeh/gopher-luar"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/yuin/gopher-lua"
|
||||
"github.com/zyedidia/clipboard"
|
||||
isatty "github.com/mattn/go-isatty"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"github.com/zyedidia/micro/internal/action"
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/shell"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
"github.com/zyedidia/tcell"
|
||||
"github.com/zyedidia/tcell/encoding"
|
||||
)
|
||||
|
||||
const (
|
||||
synLinesUp = 75 // How many lines up to look to do syntax highlighting
|
||||
synLinesDown = 75 // How many lines down to look to do syntax highlighting
|
||||
doubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click
|
||||
undoThreshold = 500 // If two events are less than n milliseconds apart, undo both of them
|
||||
)
|
||||
|
||||
var (
|
||||
// The main screen
|
||||
screen tcell.Screen
|
||||
|
||||
// Object to send messages and prompts to the user
|
||||
messenger *Messenger
|
||||
|
||||
// The default highlighting style
|
||||
// This simply defines the default foreground and background colors
|
||||
defStyle tcell.Style
|
||||
|
||||
// Where the user's configuration is
|
||||
// This should be $XDG_CONFIG_HOME/micro
|
||||
// If $XDG_CONFIG_HOME is not set, it is ~/.config/micro
|
||||
configDir string
|
||||
|
||||
// Version is the version number or commit hash
|
||||
// These variables should be set by the linker when compiling
|
||||
Version = "Unknown"
|
||||
CommitHash = "Unknown"
|
||||
CompileDate = "Unknown"
|
||||
|
||||
// L is the lua state
|
||||
// This is the VM that runs the plugins
|
||||
L *lua.LState
|
||||
|
||||
// The list of views
|
||||
tabs []*Tab
|
||||
// This is the currently open tab
|
||||
// It's just an index to the tab in the tabs array
|
||||
curTab int
|
||||
|
||||
// Channel of jobs running in the background
|
||||
jobs chan JobFunction
|
||||
// Event channel
|
||||
events chan tcell.Event
|
||||
events chan tcell.Event
|
||||
autosave chan bool
|
||||
|
||||
// Command line flags
|
||||
flagVersion = flag.Bool("version", false, "Show the version number and information")
|
||||
flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
|
||||
flagOptions = flag.Bool("options", false, "Show all option help")
|
||||
flagDebug = flag.Bool("debug", false, "Enable debug mode (prints debug info to ./log.txt)")
|
||||
flagPlugin = flag.String("plugin", "", "Plugin command")
|
||||
flagClean = flag.Bool("clean", false, "Clean configuration directory")
|
||||
optionFlags map[string]*string
|
||||
)
|
||||
|
||||
func InitFlags() {
|
||||
flag.Usage = func() {
|
||||
fmt.Println("Usage: micro [OPTIONS] [FILE]...")
|
||||
fmt.Println("-clean")
|
||||
fmt.Println(" \tCleans the configuration directory")
|
||||
fmt.Println("-config-dir dir")
|
||||
fmt.Println(" \tSpecify a custom location for the configuration directory")
|
||||
fmt.Println("[FILE]:LINE:COL")
|
||||
fmt.Println("+LINE:COL")
|
||||
fmt.Println(" \tSpecify a line and column to start the cursor at when opening a buffer")
|
||||
fmt.Println("-options")
|
||||
fmt.Println(" \tShow all option help")
|
||||
fmt.Println("-debug")
|
||||
fmt.Println(" \tEnable debug mode (enables logging to ./log.txt)")
|
||||
fmt.Println("-version")
|
||||
fmt.Println(" \tShow the version number and information")
|
||||
|
||||
fmt.Print("\nMicro's plugin's can be managed at the command line with the following commands.\n")
|
||||
fmt.Println("-plugin install [PLUGIN]...")
|
||||
fmt.Println(" \tInstall plugin(s)")
|
||||
fmt.Println("-plugin remove [PLUGIN]...")
|
||||
fmt.Println(" \tRemove plugin(s)")
|
||||
fmt.Println("-plugin update [PLUGIN]...")
|
||||
fmt.Println(" \tUpdate plugin(s) (if no argument is given, updates all plugins)")
|
||||
fmt.Println("-plugin search [PLUGIN]...")
|
||||
fmt.Println(" \tSearch for a plugin")
|
||||
fmt.Println("-plugin list")
|
||||
fmt.Println(" \tList installed plugins")
|
||||
fmt.Println("-plugin available")
|
||||
fmt.Println(" \tList available plugins")
|
||||
|
||||
fmt.Print("\nMicro's options can also be set via command line arguments for quick\nadjustments. For real configuration, please use the settings.json\nfile (see 'help options').\n\n")
|
||||
fmt.Println("-option value")
|
||||
fmt.Println(" \tSet `option` to `value` for this session")
|
||||
fmt.Println(" \tFor example: `micro -syntax off file.c`")
|
||||
fmt.Println("\nUse `micro -options` to see the full list of configuration options")
|
||||
}
|
||||
|
||||
optionFlags = make(map[string]*string)
|
||||
|
||||
for k, v := range config.DefaultAllSettings() {
|
||||
optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'.", k, v))
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if *flagVersion {
|
||||
// If -version was passed
|
||||
fmt.Println("Version:", util.Version)
|
||||
fmt.Println("Commit hash:", util.CommitHash)
|
||||
fmt.Println("Compiled on", util.CompileDate)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if *flagOptions {
|
||||
// If -options was passed
|
||||
var keys []string
|
||||
m := config.DefaultAllSettings()
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
v := m[k]
|
||||
fmt.Printf("-%s value\n", k)
|
||||
fmt.Printf(" \tDefault value: '%v'\n", v)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if util.Debug == "OFF" && *flagDebug {
|
||||
util.Debug = "ON"
|
||||
}
|
||||
}
|
||||
|
||||
// DoPluginFlags parses and executes any flags that require LoadAllPlugins (-plugin and -clean)
|
||||
func DoPluginFlags() {
|
||||
if *flagClean || *flagPlugin != "" {
|
||||
config.LoadAllPlugins()
|
||||
|
||||
if *flagPlugin != "" {
|
||||
args := flag.Args()
|
||||
|
||||
config.PluginCommand(os.Stdout, *flagPlugin, args)
|
||||
} else if *flagClean {
|
||||
CleanConfig()
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadInput determines which files should be loaded into buffers
|
||||
// based on the input stored in os.Args
|
||||
func LoadInput() []*Buffer {
|
||||
// based on the input stored in flag.Args()
|
||||
func LoadInput() []*buffer.Buffer {
|
||||
// There are a number of ways micro should start given its input
|
||||
|
||||
// 1. If it is given a files in os.Args, it should open those
|
||||
// 1. If it is given a files in flag.Args(), it should open those
|
||||
|
||||
// 2. If there is no input file and the input is not a terminal, that means
|
||||
// something is being piped in and the stdin should be opened in an
|
||||
@@ -80,25 +146,41 @@ func LoadInput() []*Buffer {
|
||||
var filename string
|
||||
var input []byte
|
||||
var err error
|
||||
var buffers []*Buffer
|
||||
args := flag.Args()
|
||||
buffers := make([]*buffer.Buffer, 0, len(args))
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
btype := buffer.BTDefault
|
||||
if !isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
btype = buffer.BTStdout
|
||||
}
|
||||
|
||||
files := make([]string, 0, len(args))
|
||||
flagStartPos := ""
|
||||
flagr := regexp.MustCompile(`^\+\d+(:\d+)?$`)
|
||||
for _, a := range args {
|
||||
if flagr.MatchString(a) {
|
||||
flagStartPos = a[1:]
|
||||
} else {
|
||||
if flagStartPos != "" {
|
||||
files = append(files, a+":"+flagStartPos)
|
||||
flagStartPos = ""
|
||||
} else {
|
||||
files = append(files, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(files) > 0 {
|
||||
// Option 1
|
||||
// We go through each file and load it
|
||||
for i := 1; i < len(os.Args); i++ {
|
||||
filename = os.Args[i]
|
||||
// Check that the file exists
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
// If it exists we load it into a buffer
|
||||
input, err = ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
TermMessage(err)
|
||||
input = []byte{}
|
||||
filename = ""
|
||||
}
|
||||
for i := 0; i < len(files); i++ {
|
||||
buf, err := buffer.NewBufferFromFile(files[i], btype)
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
continue
|
||||
}
|
||||
// If the file didn't exist, input will be empty, and we'll open an empty buffer
|
||||
buffers = append(buffers, NewBuffer(input, filename))
|
||||
buffers = append(buffers, buf)
|
||||
}
|
||||
} else if !isatty.IsTerminal(os.Stdin.Fd()) {
|
||||
// Option 2
|
||||
@@ -106,271 +188,186 @@ func LoadInput() []*Buffer {
|
||||
// and we should read from stdin
|
||||
input, err = ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
TermMessage("Error reading from stdin: ", err)
|
||||
screen.TermMessage("Error reading from stdin: ", err)
|
||||
input = []byte{}
|
||||
}
|
||||
buffers = append(buffers, NewBuffer(input, filename))
|
||||
buffers = append(buffers, buffer.NewBufferFromString(string(input), filename, btype))
|
||||
} else {
|
||||
// Option 3, just open an empty buffer
|
||||
buffers = append(buffers, NewBuffer(input, filename))
|
||||
buffers = append(buffers, buffer.NewBufferFromString(string(input), filename, btype))
|
||||
}
|
||||
|
||||
return buffers
|
||||
}
|
||||
|
||||
// InitConfigDir finds the configuration directory for micro according to the XDG spec.
|
||||
// If no directory is found, it creates one.
|
||||
func InitConfigDir() {
|
||||
xdgHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if xdgHome == "" {
|
||||
// The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
TermMessage("Error finding your home directory\nCan't load config files")
|
||||
return
|
||||
}
|
||||
xdgHome = home + "/.config"
|
||||
}
|
||||
configDir = xdgHome + "/micro"
|
||||
|
||||
if _, err := os.Stat(xdgHome); os.IsNotExist(err) {
|
||||
// If the xdgHome doesn't exist we should create it
|
||||
err = os.Mkdir(xdgHome, os.ModePerm)
|
||||
if err != nil {
|
||||
TermMessage("Error creating XDG_CONFIG_HOME directory: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configDir); os.IsNotExist(err) {
|
||||
// If the micro specific config directory doesn't exist we should create that too
|
||||
err = os.Mkdir(configDir, os.ModePerm)
|
||||
if err != nil {
|
||||
TermMessage("Error creating configuration directory: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InitScreen creates and initializes the tcell screen
|
||||
func InitScreen() {
|
||||
// Should we enable true color?
|
||||
truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
|
||||
|
||||
// In order to enable true color, we have to set the TERM to `xterm-truecolor` when
|
||||
// initializing tcell, but after that, we can set the TERM back to whatever it was
|
||||
oldTerm := os.Getenv("TERM")
|
||||
if truecolor {
|
||||
os.Setenv("TERM", "xterm-truecolor")
|
||||
}
|
||||
|
||||
// Initilize tcell
|
||||
var err error
|
||||
screen, err = tcell.NewScreen()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err = screen.Init(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Now we can put the TERM back to what it was before
|
||||
if truecolor {
|
||||
os.Setenv("TERM", oldTerm)
|
||||
}
|
||||
|
||||
screen.SetStyle(defStyle)
|
||||
screen.EnableMouse()
|
||||
}
|
||||
|
||||
// RedrawAll redraws everything -- all the views and the messenger
|
||||
func RedrawAll() {
|
||||
messenger.Clear()
|
||||
for _, v := range tabs[curTab].views {
|
||||
v.Display()
|
||||
}
|
||||
DisplayTabs()
|
||||
messenger.Display()
|
||||
screen.Show()
|
||||
}
|
||||
|
||||
// Passing -version as a flag will have micro print out the version number
|
||||
var flagVersion = flag.Bool("version", false, "Show the version number")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *flagVersion {
|
||||
// If -version was passed
|
||||
fmt.Println("Version:", Version)
|
||||
fmt.Println("Commit hash:", CommitHash)
|
||||
fmt.Println("Compiled on", CompileDate)
|
||||
defer func() {
|
||||
if util.Stdout.Len() > 0 {
|
||||
fmt.Fprint(os.Stdout, util.Stdout.String())
|
||||
}
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// runtime.SetCPUProfileRate(400)
|
||||
// f, _ := os.Create("micro.prof")
|
||||
// pprof.StartCPUProfile(f)
|
||||
// defer pprof.StopCPUProfile()
|
||||
|
||||
var err error
|
||||
|
||||
InitFlags()
|
||||
|
||||
InitLog()
|
||||
|
||||
err = config.InitConfigDir(*flagConfigDir)
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
// Start the Lua VM for running plugins
|
||||
L = lua.NewState()
|
||||
defer L.Close()
|
||||
config.InitRuntimeFiles()
|
||||
err = config.ReadSettings()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
config.InitGlobalSettings()
|
||||
|
||||
// Some encoding stuff in case the user isn't using UTF-8
|
||||
encoding.Register()
|
||||
tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
|
||||
// flag options
|
||||
for k, v := range optionFlags {
|
||||
if *v != "" {
|
||||
nativeValue, err := config.GetNativeValue(k, config.DefaultAllSettings()[k], *v)
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
continue
|
||||
}
|
||||
config.GlobalSettings[k] = nativeValue
|
||||
}
|
||||
}
|
||||
|
||||
// Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
|
||||
InitConfigDir()
|
||||
DoPluginFlags()
|
||||
|
||||
// Load the user's settings
|
||||
InitGlobalSettings()
|
||||
InitCommands()
|
||||
InitBindings()
|
||||
screen.Init()
|
||||
|
||||
// Load the syntax files, including the colorscheme
|
||||
LoadSyntaxFiles()
|
||||
|
||||
// Load the help files
|
||||
LoadHelp()
|
||||
|
||||
// Start the screen
|
||||
InitScreen()
|
||||
|
||||
// This is just so if we have an error, we can exit cleanly and not completely
|
||||
// mess up the terminal being worked in
|
||||
// In other words we need to shut down tcell before the program crashes
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
screen.Fini()
|
||||
screen.Screen.Fini()
|
||||
fmt.Println("Micro encountered an error:", err)
|
||||
// backup all open buffers
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.Backup(false)
|
||||
}
|
||||
// Print the stack trace too
|
||||
fmt.Print(errors.Wrap(err, 2).ErrorStack())
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
// Create a new messenger
|
||||
// This is used for sending the user messages in the bottom of the editor
|
||||
messenger = new(Messenger)
|
||||
messenger.history = make(map[string][]string)
|
||||
|
||||
// Now we load the input
|
||||
buffers := LoadInput()
|
||||
for _, buf := range buffers {
|
||||
// For each buffer we create a new tab and place the view in that tab
|
||||
tab := NewTabFromView(NewView(buf))
|
||||
tab.SetNum(len(tabs))
|
||||
tabs = append(tabs, tab)
|
||||
for _, t := range tabs {
|
||||
for _, v := range t.views {
|
||||
v.Center(false)
|
||||
if globalSettings["syntax"].(bool) {
|
||||
v.matches = Match(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
err = config.LoadAllPlugins()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
// Load all the plugin stuff
|
||||
// We give plugins access to a bunch of variables here which could be useful to them
|
||||
L.SetGlobal("OS", luar.New(L, runtime.GOOS))
|
||||
L.SetGlobal("tabs", luar.New(L, tabs))
|
||||
L.SetGlobal("curTab", luar.New(L, curTab))
|
||||
L.SetGlobal("messenger", luar.New(L, messenger))
|
||||
L.SetGlobal("GetOption", luar.New(L, GetOption))
|
||||
L.SetGlobal("AddOption", luar.New(L, AddOption))
|
||||
L.SetGlobal("SetOption", luar.New(L, SetOption))
|
||||
L.SetGlobal("SetLocalOption", luar.New(L, SetLocalOption))
|
||||
L.SetGlobal("BindKey", luar.New(L, BindKey))
|
||||
L.SetGlobal("MakeCommand", luar.New(L, MakeCommand))
|
||||
L.SetGlobal("CurView", luar.New(L, CurView))
|
||||
L.SetGlobal("IsWordChar", luar.New(L, IsWordChar))
|
||||
L.SetGlobal("HandleCommand", luar.New(L, HandleCommand))
|
||||
L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand))
|
||||
L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
|
||||
action.InitBindings()
|
||||
action.InitCommands()
|
||||
|
||||
// Used for asynchronous jobs
|
||||
L.SetGlobal("JobStart", luar.New(L, JobStart))
|
||||
L.SetGlobal("JobSend", luar.New(L, JobSend))
|
||||
L.SetGlobal("JobStop", luar.New(L, JobStop))
|
||||
err = config.InitColorscheme()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
LoadPlugins()
|
||||
b := LoadInput()
|
||||
|
||||
if len(b) == 0 {
|
||||
// No buffers to open
|
||||
screen.Screen.Fini()
|
||||
runtime.Goexit()
|
||||
}
|
||||
|
||||
action.InitTabs(b)
|
||||
action.InitGlobals()
|
||||
|
||||
err = config.RunPluginFn("init")
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
jobs = make(chan JobFunction, 100)
|
||||
events = make(chan tcell.Event)
|
||||
|
||||
for _, t := range tabs {
|
||||
for _, v := range t.views {
|
||||
for _, pl := range loadedPlugins {
|
||||
_, err := Call(pl+".onViewOpen", v)
|
||||
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
|
||||
TermMessage(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if v.Buf.Settings["syntax"].(bool) {
|
||||
v.matches = Match(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Here is the event loop which runs in a separate thread
|
||||
go func() {
|
||||
for {
|
||||
events <- screen.PollEvent()
|
||||
screen.Lock()
|
||||
e := screen.Screen.PollEvent()
|
||||
screen.Unlock()
|
||||
if e != nil {
|
||||
events <- e
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// clear the drawchan so we don't redraw excessively
|
||||
// if someone requested a redraw before we started displaying
|
||||
for len(screen.DrawChan()) > 0 {
|
||||
<-screen.DrawChan()
|
||||
}
|
||||
|
||||
// wait for initial resize event
|
||||
select {
|
||||
case event := <-events:
|
||||
action.Tabs.HandleEvent(event)
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
// time out after 10ms
|
||||
}
|
||||
|
||||
// Since this loop is very slow (waits for user input every time) it's
|
||||
// okay to be inefficient and run it via a function every time
|
||||
// We do this so we can recover from panics without crashing the editor
|
||||
for {
|
||||
// Display everything
|
||||
RedrawAll()
|
||||
|
||||
var event tcell.Event
|
||||
|
||||
// Check for new events
|
||||
select {
|
||||
case f := <-jobs:
|
||||
// If a new job has finished while running in the background we should execute the callback
|
||||
f.function(f.output, f.args...)
|
||||
continue
|
||||
case event = <-events:
|
||||
}
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventMouse:
|
||||
if e.Buttons() == tcell.Button1 {
|
||||
// If the user left clicked we check a couple things
|
||||
_, h := screen.Size()
|
||||
x, y := e.Position()
|
||||
if y == h-1 && messenger.message != "" {
|
||||
// If the user clicked in the bottom bar, and there is a message down there
|
||||
// we copy it to the clipboard.
|
||||
// Often error messages are displayed down there so it can be useful to easily
|
||||
// copy the message
|
||||
clipboard.WriteAll(messenger.message)
|
||||
continue
|
||||
}
|
||||
|
||||
// We loop through each view in the current tab and make sure the current view
|
||||
// it the one being clicked in
|
||||
for _, v := range tabs[curTab].views {
|
||||
if x >= v.x && x < v.x+v.width && y >= v.y && y < v.y+v.height {
|
||||
tabs[curTab].curView = v.Num
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function checks the mouse event for the possibility of changing the current tab
|
||||
// If the tab was changed it returns true
|
||||
if TabbarHandleMouseEvent(event) {
|
||||
continue
|
||||
}
|
||||
|
||||
if searching {
|
||||
// Since searching is done in real time, we need to redraw every time
|
||||
// there is a new event in the search bar so we need a special function
|
||||
// to run instead of the standard HandleEvent.
|
||||
HandleSearchEvent(event, CurView())
|
||||
} else {
|
||||
// Send it to the view
|
||||
CurView().HandleEvent(event)
|
||||
}
|
||||
DoEvent()
|
||||
}
|
||||
}
|
||||
|
||||
// DoEvent runs the main action loop of the editor
|
||||
func DoEvent() {
|
||||
var event tcell.Event
|
||||
|
||||
// recover from errors without crashing the editor
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
if e, ok := err.(*lua.ApiError); ok {
|
||||
screen.TermMessage("Lua API error:", e)
|
||||
} else {
|
||||
screen.TermMessage("Micro encountered an error:", errors.Wrap(err, 2).ErrorStack(), "\nIf you can reproduce this error, please report it at https://github.com/zyedidia/micro/issues")
|
||||
}
|
||||
}
|
||||
}()
|
||||
// Display everything
|
||||
screen.Screen.Fill(' ', config.DefStyle)
|
||||
screen.Screen.HideCursor()
|
||||
action.Tabs.Display()
|
||||
for _, ep := range action.MainTab().Panes {
|
||||
ep.Display()
|
||||
}
|
||||
action.MainTab().Display()
|
||||
action.InfoBar.Display()
|
||||
screen.Screen.Show()
|
||||
|
||||
// Check for new events
|
||||
select {
|
||||
case f := <-shell.Jobs:
|
||||
// If a new job has finished while running in the background we should execute the callback
|
||||
f.Function(f.Output, f.Args)
|
||||
case <-config.Autosave:
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.Save()
|
||||
}
|
||||
case <-shell.CloseTerms:
|
||||
case event = <-events:
|
||||
case <-screen.DrawChan():
|
||||
}
|
||||
|
||||
if action.InfoBar.HasPrompt {
|
||||
action.InfoBar.HandleEvent(event)
|
||||
} else {
|
||||
action.Tabs.HandleEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/layeh/gopher-luar"
|
||||
"github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
var loadedPlugins []string
|
||||
|
||||
var preInstalledPlugins = []string{
|
||||
"go",
|
||||
"linter",
|
||||
"autoclose",
|
||||
}
|
||||
|
||||
// Call calls the lua function 'function'
|
||||
// If it does not exist nothing happens, if there is an error,
|
||||
// the error is returned
|
||||
func Call(function string, args ...interface{}) (lua.LValue, error) {
|
||||
var luaFunc lua.LValue
|
||||
if strings.Contains(function, ".") {
|
||||
plugin := L.GetGlobal(strings.Split(function, ".")[0])
|
||||
if plugin.String() == "nil" {
|
||||
return nil, errors.New("function does not exist: " + function)
|
||||
}
|
||||
luaFunc = L.GetField(plugin, strings.Split(function, ".")[1])
|
||||
} else {
|
||||
luaFunc = L.GetGlobal(function)
|
||||
}
|
||||
|
||||
if luaFunc.String() == "nil" {
|
||||
return nil, errors.New("function does not exist: " + function)
|
||||
}
|
||||
var luaArgs []lua.LValue
|
||||
for _, v := range args {
|
||||
luaArgs = append(luaArgs, luar.New(L, v))
|
||||
}
|
||||
err := L.CallByParam(lua.P{
|
||||
Fn: luaFunc,
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, luaArgs...)
|
||||
ret := L.Get(-1) // returned value
|
||||
if ret.String() != "nil" {
|
||||
L.Pop(1) // remove received value
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// LuaFunctionBinding is a function generator which takes the name of a lua function
|
||||
// and creates a function that will call that lua function
|
||||
// Specifically it creates a function that can be called as a binding because this is used
|
||||
// to bind keys to lua functions
|
||||
func LuaFunctionBinding(function string) func(*View, bool) bool {
|
||||
return func(v *View, _ bool) bool {
|
||||
_, err := Call(function, nil)
|
||||
if err != nil {
|
||||
TermMessage(err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func unpack(old []string) []interface{} {
|
||||
new := make([]interface{}, len(old))
|
||||
for i, v := range old {
|
||||
new[i] = v
|
||||
}
|
||||
return new
|
||||
}
|
||||
|
||||
// LuaFunctionCommand is the same as LuaFunctionBinding except it returns a normal function
|
||||
// so that a command can be bound to a lua function
|
||||
func LuaFunctionCommand(function string) func([]string) {
|
||||
return func(args []string) {
|
||||
_, err := Call(function, unpack(args)...)
|
||||
if err != nil {
|
||||
TermMessage(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func LuaFunctionJob(function string) func(string, ...string) {
|
||||
return func(output string, args ...string) {
|
||||
_, err := Call(function, unpack(append([]string{output}, args...))...)
|
||||
if err != nil {
|
||||
TermMessage(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LoadPlugins loads the pre-installed plugins and the plugins located in ~/.config/micro/plugins
|
||||
func LoadPlugins() {
|
||||
files, _ := ioutil.ReadDir(configDir + "/plugins")
|
||||
for _, plugin := range files {
|
||||
if plugin.IsDir() {
|
||||
pluginName := plugin.Name()
|
||||
files, _ := ioutil.ReadDir(configDir + "/plugins/" + pluginName)
|
||||
for _, f := range files {
|
||||
if f.Name() == pluginName+".lua" {
|
||||
data, _ := ioutil.ReadFile(configDir + "/plugins/" + pluginName + "/" + f.Name())
|
||||
pluginDef := "\nlocal P = {}\n" + pluginName + " = P\nsetmetatable(" + pluginName + ", {__index = _G})\nsetfenv(1, P)\n"
|
||||
|
||||
if err := L.DoString(pluginDef + string(data)); err != nil {
|
||||
TermMessage(err)
|
||||
continue
|
||||
}
|
||||
loadedPlugins = append(loadedPlugins, pluginName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, pluginName := range preInstalledPlugins {
|
||||
alreadyExists := false
|
||||
for _, pl := range loadedPlugins {
|
||||
if pl == pluginName {
|
||||
alreadyExists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !alreadyExists {
|
||||
plugin := "runtime/plugins/" + pluginName + "/" + pluginName + ".lua"
|
||||
data, err := Asset(plugin)
|
||||
if err != nil {
|
||||
TermMessage("Error loading pre-installed plugin: " + pluginName)
|
||||
continue
|
||||
}
|
||||
pluginDef := "\nlocal P = {}\n" + pluginName + " = P\nsetmetatable(" + pluginName + ", {__index = _G})\nsetfenv(1, P)\n"
|
||||
if err := L.DoString(pluginDef + string(data)); err != nil {
|
||||
TermMessage(err)
|
||||
continue
|
||||
}
|
||||
|
||||
loadedPlugins = append(loadedPlugins, pluginName)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(configDir + "/init.lua"); err == nil {
|
||||
pluginDef := "\nlocal P = {}\n" + "init" + " = P\nsetmetatable(" + "init" + ", {__index = _G})\nsetfenv(1, P)\n"
|
||||
data, _ := ioutil.ReadFile(configDir + "/init.lua")
|
||||
if err := L.DoString(pluginDef + string(data)); err != nil {
|
||||
TermMessage(err)
|
||||
}
|
||||
loadedPlugins = append(loadedPlugins, "init")
|
||||
}
|
||||
}
|
||||
2712
cmd/micro/runtime.go
2712
cmd/micro/runtime.go
File diff suppressed because one or more lines are too long
@@ -1,135 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
var (
|
||||
// What was the last search
|
||||
lastSearch string
|
||||
|
||||
// Where should we start the search down from (or up from)
|
||||
searchStart int
|
||||
|
||||
// Is there currently a search in progress
|
||||
searching bool
|
||||
|
||||
// Stores the history for searching
|
||||
searchHistory []string
|
||||
)
|
||||
|
||||
// BeginSearch starts a search
|
||||
func BeginSearch() {
|
||||
searchHistory = append(searchHistory, "")
|
||||
messenger.historyNum = len(searchHistory) - 1
|
||||
searching = true
|
||||
messenger.hasPrompt = true
|
||||
messenger.Message("Find: ")
|
||||
}
|
||||
|
||||
// EndSearch stops the current search
|
||||
func EndSearch() {
|
||||
searchHistory[len(searchHistory)-1] = messenger.response
|
||||
searching = false
|
||||
messenger.hasPrompt = false
|
||||
messenger.Clear()
|
||||
messenger.Reset()
|
||||
if lastSearch != "" {
|
||||
messenger.Message("^P Previous ^N Next")
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSearchEvent takes an event and a view and will do a real time match from the messenger's output
|
||||
// to the current buffer. It searches down the buffer.
|
||||
func HandleSearchEvent(event tcell.Event, v *View) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
switch e.Key() {
|
||||
case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape, tcell.KeyEnter:
|
||||
// Done
|
||||
EndSearch()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
messenger.HandleEvent(event, searchHistory)
|
||||
|
||||
if messenger.cursorx < 0 {
|
||||
// Done
|
||||
EndSearch()
|
||||
return
|
||||
}
|
||||
|
||||
if messenger.response == "" {
|
||||
v.Cursor.ResetSelection()
|
||||
// We don't end the search though
|
||||
return
|
||||
}
|
||||
|
||||
Search(messenger.response, v, true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Search searches in the view for the given regex. The down bool
|
||||
// specifies whether it should search down from the searchStart position
|
||||
// or up from there
|
||||
func Search(searchStr string, v *View, down bool) {
|
||||
if searchStr == "" {
|
||||
return
|
||||
}
|
||||
var str string
|
||||
var charPos int
|
||||
text := v.Buf.String()
|
||||
if down {
|
||||
str = string([]rune(text)[searchStart:])
|
||||
charPos = searchStart
|
||||
} else {
|
||||
str = string([]rune(text)[:searchStart])
|
||||
}
|
||||
r, err := regexp.Compile(searchStr)
|
||||
if v.Buf.Settings["ignorecase"].(bool) {
|
||||
r, err = regexp.Compile("(?i)" + searchStr)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
matches := r.FindAllStringIndex(str, -1)
|
||||
var match []int
|
||||
if matches == nil {
|
||||
// Search the entire buffer now
|
||||
matches = r.FindAllStringIndex(text, -1)
|
||||
charPos = 0
|
||||
if matches == nil {
|
||||
v.Cursor.ResetSelection()
|
||||
return
|
||||
}
|
||||
|
||||
if !down {
|
||||
match = matches[len(matches)-1]
|
||||
} else {
|
||||
match = matches[0]
|
||||
}
|
||||
str = text
|
||||
}
|
||||
|
||||
if !down {
|
||||
match = matches[len(matches)-1]
|
||||
} else {
|
||||
match = matches[0]
|
||||
}
|
||||
|
||||
if match[0] == match[1] {
|
||||
return
|
||||
}
|
||||
|
||||
v.Cursor.CurSelection[0] = FromCharPos(charPos+runePos(match[0], str), v.Buf)
|
||||
v.Cursor.CurSelection[1] = FromCharPos(charPos+runePos(match[1], str), v.Buf)
|
||||
v.Cursor.Loc = v.Cursor.CurSelection[1]
|
||||
if v.Relocate() {
|
||||
v.matches = Match(v)
|
||||
}
|
||||
lastSearch = searchStr
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/zyedidia/glob"
|
||||
)
|
||||
|
||||
// The options that the user can set
|
||||
var globalSettings map[string]interface{}
|
||||
|
||||
// InitGlobalSettings initializes the options map and sets all options to their default values
|
||||
func InitGlobalSettings() {
|
||||
defaults := DefaultGlobalSettings()
|
||||
var parsed map[string]interface{}
|
||||
|
||||
filename := configDir + "/settings.json"
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
TermMessage("Error reading settings.json file: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
TermMessage("Error reading settings.json:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
globalSettings = make(map[string]interface{})
|
||||
for k, v := range defaults {
|
||||
globalSettings[k] = v
|
||||
}
|
||||
for k, v := range parsed {
|
||||
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
globalSettings[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
err := WriteSettings(filename)
|
||||
if err != nil {
|
||||
TermMessage("Error writing settings.json file: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InitLocalSettings scans the json in settings.json and sets the options locally based
|
||||
// on whether the buffer matches the glob
|
||||
func InitLocalSettings(buf *Buffer) {
|
||||
var parsed map[string]interface{}
|
||||
|
||||
filename := configDir + "/settings.json"
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
TermMessage("Error reading settings.json file: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
TermMessage("Error reading settings.json:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range parsed {
|
||||
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
g, err := glob.Compile(k)
|
||||
if err != nil {
|
||||
TermMessage("Error with glob setting ", k, ": ", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if g.MatchString(buf.Path) {
|
||||
for k1, v1 := range v.(map[string]interface{}) {
|
||||
buf.Settings[k1] = v1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteSettings writes the settings to the specified filename as JSON
|
||||
func WriteSettings(filename string) error {
|
||||
var err error
|
||||
if _, e := os.Stat(configDir); e == nil {
|
||||
var parsed map[string]interface{}
|
||||
|
||||
filename := configDir + "/settings.json"
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
TermMessage("Error reading settings.json:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range parsed {
|
||||
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
if _, ok := globalSettings[k]; ok {
|
||||
parsed[k] = globalSettings[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txt, _ := json.MarshalIndent(parsed, "", " ")
|
||||
err = ioutil.WriteFile(filename, txt, 0644)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// AddOption creates a new option. This is meant to be called by plugins to add options.
|
||||
func AddOption(name string, value interface{}) {
|
||||
globalSettings[name] = value
|
||||
err := WriteSettings(configDir + "/settings.json")
|
||||
if err != nil {
|
||||
TermMessage("Error writing settings.json file: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// GetGlobalOption returns the global value of the given option
|
||||
func GetGlobalOption(name string) interface{} {
|
||||
return globalSettings[name]
|
||||
}
|
||||
|
||||
// GetLocalOption returns the local value of the given option
|
||||
func GetLocalOption(name string, buf *Buffer) interface{} {
|
||||
return buf.Settings[name]
|
||||
}
|
||||
|
||||
// GetOption returns the value of the given option
|
||||
// If there is a local version of the option, it returns that
|
||||
// otherwise it will return the global version
|
||||
func GetOption(name string) interface{} {
|
||||
if GetLocalOption(name, CurView().Buf) != nil {
|
||||
return GetLocalOption(name, CurView().Buf)
|
||||
}
|
||||
return GetGlobalOption(name)
|
||||
}
|
||||
|
||||
// DefaultGlobalSettings returns the default global settings for micro
|
||||
// Note that colorscheme is a global only option
|
||||
func DefaultGlobalSettings() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"autoindent": true,
|
||||
"colorscheme": "zenburn",
|
||||
"cursorline": true,
|
||||
"ignorecase": false,
|
||||
"indentchar": " ",
|
||||
"ruler": true,
|
||||
"savecursor": false,
|
||||
"saveundo": false,
|
||||
"scrollspeed": float64(2),
|
||||
"scrollmargin": float64(3),
|
||||
"statusline": true,
|
||||
"syntax": true,
|
||||
"tabsize": float64(4),
|
||||
"tabstospaces": false,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultLocalSettings returns the default local settings
|
||||
// Note that filetype is a local only option
|
||||
func DefaultLocalSettings() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"autoindent": true,
|
||||
"cursorline": true,
|
||||
"filetype": "Unknown",
|
||||
"ignorecase": false,
|
||||
"indentchar": " ",
|
||||
"ruler": true,
|
||||
"savecursor": false,
|
||||
"saveundo": false,
|
||||
"scrollspeed": float64(2),
|
||||
"scrollmargin": float64(3),
|
||||
"statusline": true,
|
||||
"syntax": true,
|
||||
"tabsize": float64(4),
|
||||
"tabstospaces": false,
|
||||
}
|
||||
}
|
||||
|
||||
// SetOption attempts to set the given option to the value
|
||||
// By default it will set the option as global, but if the option
|
||||
// is local only it will set the local version
|
||||
// Use setlocal to force an option to be set locally
|
||||
func SetOption(option, value string) error {
|
||||
if _, ok := globalSettings[option]; !ok {
|
||||
if _, ok := CurView().Buf.Settings[option]; !ok {
|
||||
return errors.New("Invalid option")
|
||||
}
|
||||
SetLocalOption(option, value, CurView())
|
||||
return nil
|
||||
}
|
||||
|
||||
kind := reflect.TypeOf(globalSettings[option]).Kind()
|
||||
if kind == reflect.Bool {
|
||||
b, err := ParseBool(value)
|
||||
if err != nil {
|
||||
return errors.New("Invalid value")
|
||||
}
|
||||
globalSettings[option] = b
|
||||
} else if kind == reflect.String {
|
||||
globalSettings[option] = value
|
||||
} else if kind == reflect.Float64 {
|
||||
i, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return errors.New("Invalid value")
|
||||
}
|
||||
globalSettings[option] = float64(i)
|
||||
}
|
||||
|
||||
if option == "colorscheme" {
|
||||
LoadSyntaxFiles()
|
||||
for _, tab := range tabs {
|
||||
for _, view := range tab.views {
|
||||
view.Buf.UpdateRules()
|
||||
if view.Buf.Settings["syntax"].(bool) {
|
||||
view.matches = Match(view)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := CurView().Buf.Settings[option]; ok {
|
||||
for _, tab := range tabs {
|
||||
for _, view := range tab.views {
|
||||
SetLocalOption(option, value, view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLocalOption sets the local version of this option
|
||||
func SetLocalOption(option, value string, view *View) error {
|
||||
buf := view.Buf
|
||||
if _, ok := buf.Settings[option]; !ok {
|
||||
return errors.New("Invalid option")
|
||||
}
|
||||
|
||||
kind := reflect.TypeOf(buf.Settings[option]).Kind()
|
||||
if kind == reflect.Bool {
|
||||
b, err := ParseBool(value)
|
||||
if err != nil {
|
||||
return errors.New("Invalid value")
|
||||
}
|
||||
buf.Settings[option] = b
|
||||
} else if kind == reflect.String {
|
||||
buf.Settings[option] = value
|
||||
} else if kind == reflect.Float64 {
|
||||
i, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return errors.New("Invalid value")
|
||||
}
|
||||
buf.Settings[option] = float64(i)
|
||||
}
|
||||
|
||||
if option == "statusline" {
|
||||
view.ToggleStatusLine()
|
||||
if buf.Settings["syntax"].(bool) {
|
||||
view.matches = Match(view)
|
||||
}
|
||||
}
|
||||
|
||||
if option == "filetype" {
|
||||
LoadSyntaxFiles()
|
||||
buf.UpdateRules()
|
||||
if buf.Settings["syntax"].(bool) {
|
||||
view.matches = Match(view)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetOptionAndSettings sets the given option and saves the option setting to the settings config file
|
||||
func SetOptionAndSettings(option, value string) {
|
||||
filename := configDir + "/settings.json"
|
||||
|
||||
err := SetOption(option, value)
|
||||
|
||||
if err != nil {
|
||||
messenger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = WriteSettings(filename)
|
||||
if err != nil {
|
||||
messenger.Error("Error writing to settings.json: " + err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
package main
|
||||
|
||||
type SplitType bool
|
||||
|
||||
const (
|
||||
VerticalSplit = false
|
||||
HorizontalSplit = true
|
||||
)
|
||||
|
||||
type Node interface {
|
||||
VSplit(buf *Buffer)
|
||||
HSplit(buf *Buffer)
|
||||
String() string
|
||||
}
|
||||
|
||||
type LeafNode struct {
|
||||
view *View
|
||||
|
||||
parent *SplitTree
|
||||
}
|
||||
|
||||
func NewLeafNode(v *View, parent *SplitTree) *LeafNode {
|
||||
n := new(LeafNode)
|
||||
n.view = v
|
||||
n.view.splitNode = n
|
||||
n.parent = parent
|
||||
return n
|
||||
}
|
||||
|
||||
type SplitTree struct {
|
||||
kind SplitType
|
||||
|
||||
parent *SplitTree
|
||||
children []Node
|
||||
|
||||
x int
|
||||
y int
|
||||
|
||||
width int
|
||||
height int
|
||||
|
||||
tabNum int
|
||||
}
|
||||
|
||||
func (l *LeafNode) VSplit(buf *Buffer) {
|
||||
tab := tabs[l.parent.tabNum]
|
||||
if l.parent.kind == VerticalSplit {
|
||||
newView := NewView(buf)
|
||||
newView.TabNum = l.parent.tabNum
|
||||
newView.Num = len(tab.views)
|
||||
l.parent.children = append(l.parent.children, NewLeafNode(newView, l.parent))
|
||||
|
||||
tab.curView++
|
||||
tab.views = append(tab.views, newView)
|
||||
} else {
|
||||
s := new(SplitTree)
|
||||
s.kind = VerticalSplit
|
||||
s.parent = l.parent
|
||||
newView := NewView(buf)
|
||||
newView.TabNum = l.parent.tabNum
|
||||
newView.Num = len(tab.views)
|
||||
s.children = []Node{l, NewLeafNode(newView, s)}
|
||||
l.parent.children[search(l.parent.children, l)] = s
|
||||
l.parent = s
|
||||
|
||||
tab.curView++
|
||||
tab.views = append(tab.views, newView)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LeafNode) HSplit(buf *Buffer) {
|
||||
tab := tabs[l.parent.tabNum]
|
||||
if l.parent.kind == HorizontalSplit {
|
||||
newView := NewView(buf)
|
||||
newView.TabNum = l.parent.tabNum
|
||||
newView.Num = len(tab.views)
|
||||
l.parent.children = append(l.parent.children, NewLeafNode(newView, l.parent))
|
||||
|
||||
tab.curView++
|
||||
tab.views = append(tab.views, newView)
|
||||
} else {
|
||||
s := new(SplitTree)
|
||||
s.kind = HorizontalSplit
|
||||
s.parent = l.parent
|
||||
newView := NewView(buf)
|
||||
newView.TabNum = l.parent.tabNum
|
||||
newView.Num = len(tab.views)
|
||||
s.children = []Node{l, NewLeafNode(newView, s)}
|
||||
l.parent.children[search(l.parent.children, l)] = s
|
||||
l.parent = s
|
||||
|
||||
tab.curView++
|
||||
tab.views = append(tab.views, newView)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LeafNode) Delete() {
|
||||
i := search(l.parent.children, l)
|
||||
|
||||
copy(l.parent.children[i:], l.parent.children[i+1:])
|
||||
l.parent.children[len(l.parent.children)-1] = nil
|
||||
l.parent.children = l.parent.children[:len(l.parent.children)-1]
|
||||
|
||||
tab := tabs[l.parent.tabNum]
|
||||
j := findView(tab.views, l.view)
|
||||
copy(tab.views[j:], tab.views[j+1:])
|
||||
tab.views[len(tab.views)-1] = nil // or the zero value of T
|
||||
tab.views = tab.views[:len(tab.views)-1]
|
||||
|
||||
for i, v := range tab.views {
|
||||
v.Num = i
|
||||
}
|
||||
if tab.curView > 0 {
|
||||
tab.curView--
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SplitTree) Cleanup() {
|
||||
for i, node := range s.children {
|
||||
if n, ok := node.(*SplitTree); ok {
|
||||
if len(n.children) == 1 {
|
||||
if child, ok := n.children[0].(*LeafNode); ok {
|
||||
s.children[i] = child
|
||||
child.parent = s
|
||||
continue
|
||||
}
|
||||
}
|
||||
n.Cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SplitTree) ResizeSplits() {
|
||||
for i, node := range s.children {
|
||||
if n, ok := node.(*LeafNode); ok {
|
||||
if s.kind == VerticalSplit {
|
||||
n.view.width = s.width / len(s.children)
|
||||
n.view.height = s.height
|
||||
|
||||
n.view.x = s.x + n.view.width*i
|
||||
n.view.y = s.y
|
||||
} else {
|
||||
n.view.height = s.height / len(s.children)
|
||||
n.view.width = s.width
|
||||
|
||||
n.view.y = s.y + n.view.height*i
|
||||
n.view.x = s.x
|
||||
}
|
||||
// n.view.ToggleStatusLine()
|
||||
_, screenH := screen.Size()
|
||||
if n.view.Buf.Settings["statusline"].(bool) || (n.view.y+n.view.height) != screenH-1 {
|
||||
n.view.height--
|
||||
}
|
||||
|
||||
n.view.ToggleTabbar()
|
||||
n.view.matches = Match(n.view)
|
||||
} else if n, ok := node.(*SplitTree); ok {
|
||||
if s.kind == VerticalSplit {
|
||||
n.width = s.width / len(s.children)
|
||||
n.height = s.height
|
||||
|
||||
n.x = s.x + n.width*i
|
||||
n.y = s.y
|
||||
} else {
|
||||
n.height = s.height / len(s.children)
|
||||
n.width = s.width
|
||||
|
||||
n.y = s.y + n.height*i
|
||||
n.x = s.x
|
||||
}
|
||||
n.ResizeSplits()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *LeafNode) String() string {
|
||||
return l.view.Buf.Name
|
||||
}
|
||||
|
||||
func search(haystack []Node, needle Node) int {
|
||||
for i, x := range haystack {
|
||||
if x == needle {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func findView(haystack []*View, needle *View) int {
|
||||
for i, x := range haystack {
|
||||
if x == needle {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *SplitTree) VSplit(buf *Buffer) {}
|
||||
func (s *SplitTree) HSplit(buf *Buffer) {}
|
||||
|
||||
func (s *SplitTree) String() string {
|
||||
str := "["
|
||||
for _, child := range s.children {
|
||||
str += child.String() + ", "
|
||||
}
|
||||
return str + "]"
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Statusline represents the information line at the bottom
|
||||
// of each view
|
||||
// It gives information such as filename, whether the file has been
|
||||
// modified, filetype, cursor location
|
||||
type Statusline struct {
|
||||
view *View
|
||||
}
|
||||
|
||||
// Display draws the statusline to the screen
|
||||
func (sline *Statusline) Display() {
|
||||
// We'll draw the line at the lowest line in the view
|
||||
y := sline.view.height + sline.view.y
|
||||
|
||||
file := sline.view.Buf.Name
|
||||
|
||||
// If the buffer is dirty (has been modified) write a little '+'
|
||||
if sline.view.Buf.IsModified {
|
||||
file += " +"
|
||||
}
|
||||
|
||||
// Add one to cursor.x and cursor.y because (0,0) is the top left,
|
||||
// but users will be used to (1,1) (first line,first column)
|
||||
// We use GetVisualX() here because otherwise we get the column number in runes
|
||||
// so a '\t' is only 1, when it should be tabSize
|
||||
columnNum := strconv.Itoa(sline.view.Cursor.GetVisualX() + 1)
|
||||
lineNum := strconv.Itoa(sline.view.Cursor.Y + 1)
|
||||
|
||||
file += " (" + lineNum + "," + columnNum + ")"
|
||||
|
||||
// Add the filetype
|
||||
file += " " + sline.view.Buf.FileType()
|
||||
|
||||
rightText := helpBinding + " for help "
|
||||
if sline.view.Help {
|
||||
rightText = helpBinding + " to close help "
|
||||
}
|
||||
|
||||
statusLineStyle := defStyle.Reverse(true)
|
||||
if style, ok := colorscheme["statusline"]; ok {
|
||||
statusLineStyle = style
|
||||
}
|
||||
|
||||
// Maybe there is a unicode filename?
|
||||
fileRunes := []rune(file)
|
||||
viewX := sline.view.x
|
||||
if viewX != 0 {
|
||||
screen.SetContent(viewX, y, ' ', nil, statusLineStyle)
|
||||
viewX++
|
||||
}
|
||||
for x := 0; x < sline.view.width; x++ {
|
||||
if x < len(fileRunes) {
|
||||
screen.SetContent(viewX+x, y, fileRunes[x], nil, statusLineStyle)
|
||||
} else if x >= sline.view.width-len(rightText) && x < len(rightText)+sline.view.width-len(rightText) {
|
||||
screen.SetContent(viewX+x, y, []rune(rightText)[x-sline.view.width+len(rightText)], nil, statusLineStyle)
|
||||
} else {
|
||||
screen.SetContent(viewX+x, y, ' ', nil, statusLineStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
152
cmd/micro/tab.go
152
cmd/micro/tab.go
@@ -1,152 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
type Tab struct {
|
||||
// This contains all the views in this tab
|
||||
// There is generally only one view per tab, but you can have
|
||||
// multiple views with splits
|
||||
views []*View
|
||||
// This is the current view for this tab
|
||||
curView int
|
||||
// Generally this is the name of the current view's buffer
|
||||
name string
|
||||
|
||||
tree *SplitTree
|
||||
}
|
||||
|
||||
// NewTabFromView creates a new tab and puts the given view in the tab
|
||||
func NewTabFromView(v *View) *Tab {
|
||||
t := new(Tab)
|
||||
t.views = append(t.views, v)
|
||||
t.views[0].Num = 0
|
||||
|
||||
t.tree = new(SplitTree)
|
||||
t.tree.kind = VerticalSplit
|
||||
t.tree.children = []Node{NewLeafNode(t.views[0], t.tree)}
|
||||
|
||||
w, h := screen.Size()
|
||||
t.tree.width = w
|
||||
t.tree.height = h - 1
|
||||
return t
|
||||
}
|
||||
|
||||
// SetNum sets all this tab's views to have the correct tab number
|
||||
func (t *Tab) SetNum(num int) {
|
||||
for _, v := range t.views {
|
||||
v.TabNum = num
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tab) Cleanup() {
|
||||
t.tree.Cleanup()
|
||||
}
|
||||
|
||||
func (t *Tab) Resize() {
|
||||
w, h := screen.Size()
|
||||
t.tree.width = w
|
||||
t.tree.height = h - 1
|
||||
t.tree.ResizeSplits()
|
||||
}
|
||||
|
||||
// CurView returns the current view
|
||||
func CurView() *View {
|
||||
curTab := tabs[curTab]
|
||||
return curTab.views[curTab.curView]
|
||||
}
|
||||
|
||||
// TabbarString returns the string that should be displayed in the tabbar
|
||||
// It also returns a map containing which indicies correspond to which tab number
|
||||
// This is useful when we know that the mouse click has occurred at an x location
|
||||
// but need to know which tab that corresponds to to accurately change the tab
|
||||
func TabbarString() (string, map[int]int) {
|
||||
str := ""
|
||||
indicies := make(map[int]int)
|
||||
for i, t := range tabs {
|
||||
if i == curTab {
|
||||
str += "["
|
||||
} else {
|
||||
str += " "
|
||||
}
|
||||
str += t.views[t.curView].Buf.Name
|
||||
if i == curTab {
|
||||
str += "]"
|
||||
} else {
|
||||
str += " "
|
||||
}
|
||||
indicies[len(str)-1] = i + 1
|
||||
str += " "
|
||||
}
|
||||
return str, indicies
|
||||
}
|
||||
|
||||
// TabbarHandleMouseEvent checks the given mouse event if it is clicking on the tabbar
|
||||
// If it is it changes the current tab accordingly
|
||||
// This function returns true if the tab is changed
|
||||
func TabbarHandleMouseEvent(event tcell.Event) bool {
|
||||
// There is no tabbar displayed if there are less than 2 tabs
|
||||
if len(tabs) <= 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventMouse:
|
||||
button := e.Buttons()
|
||||
// Must be a left click
|
||||
if button == tcell.Button1 {
|
||||
x, y := e.Position()
|
||||
if y != 0 {
|
||||
return false
|
||||
}
|
||||
str, indicies := TabbarString()
|
||||
if x >= len(str) {
|
||||
return false
|
||||
}
|
||||
var tabnum int
|
||||
var keys []int
|
||||
for k := range indicies {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Ints(keys)
|
||||
for _, k := range keys {
|
||||
if x <= k {
|
||||
tabnum = indicies[k] - 1
|
||||
break
|
||||
}
|
||||
}
|
||||
curTab = tabnum
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// DisplayTabs displays the tabbar at the top of the editor if there are multiple tabs
|
||||
func DisplayTabs() {
|
||||
if len(tabs) <= 1 {
|
||||
return
|
||||
}
|
||||
|
||||
str, _ := TabbarString()
|
||||
|
||||
tabBarStyle := defStyle.Reverse(true)
|
||||
if style, ok := colorscheme["tabbar"]; ok {
|
||||
tabBarStyle = style
|
||||
}
|
||||
|
||||
// Maybe there is a unicode filename?
|
||||
fileRunes := []rune(str)
|
||||
w, _ := screen.Size()
|
||||
for x := 0; x < w; x++ {
|
||||
if x < len(fileRunes) {
|
||||
screen.SetContent(x, 0, fileRunes[x], nil, tabBarStyle)
|
||||
} else {
|
||||
screen.SetContent(x, 0, ' ', nil, tabBarStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
)
|
||||
|
||||
// Util.go is a collection of utility functions that are used throughout
|
||||
// the program
|
||||
|
||||
// Count returns the length of a string in runes
|
||||
// This is exactly equivalent to utf8.RuneCountInString(), just less characters
|
||||
func Count(s string) int {
|
||||
return utf8.RuneCountInString(s)
|
||||
}
|
||||
|
||||
// NumOccurrences counts the number of occurences of a byte in a string
|
||||
func NumOccurrences(s string, c byte) int {
|
||||
var n int
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == c {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Spaces returns a string with n spaces
|
||||
func Spaces(n int) string {
|
||||
var str string
|
||||
for i := 0; i < n; i++ {
|
||||
str += " "
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// Min takes the min of two ints
|
||||
func Min(a, b int) int {
|
||||
if a > b {
|
||||
return b
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Max takes the max of two ints
|
||||
func Max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// IsWordChar returns whether or not the string is a 'word character'
|
||||
// If it is a unicode character, then it does not match
|
||||
// Word characters are defined as [A-Za-z0-9_]
|
||||
func IsWordChar(str string) bool {
|
||||
if len(str) > 1 {
|
||||
// Unicode
|
||||
return false
|
||||
}
|
||||
c := str[0]
|
||||
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_')
|
||||
}
|
||||
|
||||
// IsWhitespace returns true if the given rune is a space, tab, or newline
|
||||
func IsWhitespace(c rune) bool {
|
||||
return c == ' ' || c == '\t' || c == '\n'
|
||||
}
|
||||
|
||||
// Contains returns whether or not a string array contains a given string
|
||||
func Contains(list []string, a string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Insert makes a simple insert into a string at the given position
|
||||
func Insert(str string, pos int, value string) string {
|
||||
return string([]rune(str)[:pos]) + value + string([]rune(str)[pos:])
|
||||
}
|
||||
|
||||
// GetLeadingWhitespace returns the leading whitespace of the given string
|
||||
func GetLeadingWhitespace(str string) string {
|
||||
ws := ""
|
||||
for _, c := range str {
|
||||
if c == ' ' || c == '\t' {
|
||||
ws += string(c)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return ws
|
||||
}
|
||||
|
||||
// IsSpaces checks if a given string is only spaces
|
||||
func IsSpaces(str string) bool {
|
||||
for _, c := range str {
|
||||
if c != ' ' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsSpacesOrTabs checks if a given string contains only spaces and tabs
|
||||
func IsSpacesOrTabs(str string) bool {
|
||||
for _, c := range str {
|
||||
if c != ' ' && c != '\t' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ParseBool is almost exactly like strconv.ParseBool, except it also accepts 'on' and 'off'
|
||||
// as 'true' and 'false' respectively
|
||||
func ParseBool(str string) (bool, error) {
|
||||
if str == "on" {
|
||||
return true, nil
|
||||
}
|
||||
if str == "off" {
|
||||
return false, nil
|
||||
}
|
||||
return strconv.ParseBool(str)
|
||||
}
|
||||
|
||||
// EscapePath replaces every path separator in a given path with a %
|
||||
func EscapePath(path string) string {
|
||||
path = filepath.ToSlash(path)
|
||||
return strings.Replace(path, "/", "%", -1)
|
||||
}
|
||||
|
||||
// GetModTime returns the last modification time for a given file
|
||||
// It also returns a boolean if there was a problem accessing the file
|
||||
func GetModTime(path string) (time.Time, bool) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return time.Now(), false
|
||||
}
|
||||
return info.ModTime(), true
|
||||
}
|
||||
|
||||
// StringWidth returns the width of a string where tabs count as `tabsize` width
|
||||
func StringWidth(str string, tabsize int) int {
|
||||
sw := runewidth.StringWidth(str)
|
||||
sw += NumOccurrences(str, '\t') * (tabsize - 1)
|
||||
return sw
|
||||
}
|
||||
|
||||
// WidthOfLargeRunes searches all the runes in a string and counts up all the widths of runes
|
||||
// that have a width larger than 1 (this also counts tabs as `tabsize` width)
|
||||
func WidthOfLargeRunes(str string, tabsize int) int {
|
||||
count := 0
|
||||
for _, ch := range str {
|
||||
var w int
|
||||
if ch == '\t' {
|
||||
w = tabsize
|
||||
} else {
|
||||
w = runewidth.RuneWidth(ch)
|
||||
}
|
||||
if w > 1 {
|
||||
count += (w - 1)
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// RunePos returns the rune index of a given byte index
|
||||
// This could cause problems if the byte index is between code points
|
||||
func runePos(p int, str string) int {
|
||||
return utf8.RuneCountInString(str[:p])
|
||||
}
|
||||
|
||||
func lcs(a, b string) string {
|
||||
arunes := []rune(a)
|
||||
brunes := []rune(b)
|
||||
|
||||
lcs := ""
|
||||
for i, r := range arunes {
|
||||
if i >= len(brunes) {
|
||||
break
|
||||
}
|
||||
if r == brunes[i] {
|
||||
lcs += string(r)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return lcs
|
||||
}
|
||||
|
||||
func CommonSubstring(arr ...string) string {
|
||||
commonStr := arr[0]
|
||||
|
||||
for _, str := range arr[1:] {
|
||||
commonStr = lcs(commonStr, str)
|
||||
}
|
||||
|
||||
return commonStr
|
||||
}
|
||||
|
||||
// Abs is a simple absolute value function for ints
|
||||
func Abs(n int) int {
|
||||
if n < 0 {
|
||||
return -n
|
||||
}
|
||||
return n
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNumOccurences(t *testing.T) {
|
||||
var tests = []struct {
|
||||
inputStr string
|
||||
inputChar byte
|
||||
want int
|
||||
}{
|
||||
{"aaaa", 'a', 4},
|
||||
{"\trfd\ta", '\t', 2},
|
||||
{"∆ƒ\tø ® \t\t", '\t', 3},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if got := NumOccurrences(test.inputStr, test.inputChar); got != test.want {
|
||||
t.Errorf("NumOccurences(%s, %c) = %d", test.inputStr, test.inputChar, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpaces(t *testing.T) {
|
||||
var tests = []struct {
|
||||
input int
|
||||
want string
|
||||
}{
|
||||
{4, " "},
|
||||
{0, ""},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if got := Spaces(test.input); got != test.want {
|
||||
t.Errorf("Spaces(%d) = \"%s\"", test.input, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsWordChar(t *testing.T) {
|
||||
if IsWordChar("t") == false {
|
||||
t.Errorf("IsWordChar(t) = false")
|
||||
}
|
||||
if IsWordChar("T") == false {
|
||||
t.Errorf("IsWordChar(T) = false")
|
||||
}
|
||||
if IsWordChar("5") == false {
|
||||
t.Errorf("IsWordChar(5) = false")
|
||||
}
|
||||
if IsWordChar("_") == false {
|
||||
t.Errorf("IsWordChar(_) = false")
|
||||
}
|
||||
if IsWordChar("~") == true {
|
||||
t.Errorf("IsWordChar(~) = true")
|
||||
}
|
||||
if IsWordChar(" ") == true {
|
||||
t.Errorf("IsWordChar( ) = true")
|
||||
}
|
||||
if IsWordChar("ß") == true {
|
||||
t.Errorf("IsWordChar(ß) = true")
|
||||
}
|
||||
if IsWordChar(")") == true {
|
||||
t.Errorf("IsWordChar()) = true")
|
||||
}
|
||||
if IsWordChar("\n") == true {
|
||||
t.Errorf("IsWordChar(\n)) = true")
|
||||
}
|
||||
}
|
||||
@@ -1,773 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-runewidth"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
// The View struct stores information about a view into a buffer.
|
||||
// It stores information about the cursor, and the viewport
|
||||
// that the user sees the buffer from.
|
||||
type View struct {
|
||||
// A pointer to the buffer's cursor for ease of access
|
||||
Cursor *Cursor
|
||||
|
||||
// The topmost line, used for vertical scrolling
|
||||
Topline int
|
||||
// The leftmost column, used for horizontal scrolling
|
||||
leftCol int
|
||||
|
||||
// Percentage of the terminal window that this view takes up (from 0 to 100)
|
||||
widthPercent int
|
||||
heightPercent int
|
||||
|
||||
// Specifies whether or not this view holds a help buffer
|
||||
Help bool
|
||||
|
||||
// Actual with and height
|
||||
width int
|
||||
height int
|
||||
|
||||
// Where this view is located
|
||||
x, y int
|
||||
|
||||
// How much to offset because of line numbers
|
||||
lineNumOffset int
|
||||
|
||||
// Holds the list of gutter messages
|
||||
messages map[string][]GutterMessage
|
||||
|
||||
// This is the index of this view in the views array
|
||||
Num int
|
||||
// What tab is this view stored in
|
||||
TabNum int
|
||||
|
||||
// The buffer
|
||||
Buf *Buffer
|
||||
// The statusline
|
||||
sline Statusline
|
||||
|
||||
// Since tcell doesn't differentiate between a mouse release event
|
||||
// and a mouse move event with no keys pressed, we need to keep
|
||||
// track of whether or not the mouse was pressed (or not released) last event to determine
|
||||
// mouse release events
|
||||
mouseReleased bool
|
||||
|
||||
// This stores when the last click was
|
||||
// This is useful for detecting double and triple clicks
|
||||
lastClickTime time.Time
|
||||
|
||||
// lastCutTime stores when the last ctrl+k was issued.
|
||||
// It is used for clearing the clipboard to replace it with fresh cut lines.
|
||||
lastCutTime time.Time
|
||||
|
||||
// freshClip returns true if the clipboard has never been pasted.
|
||||
freshClip bool
|
||||
|
||||
// Was the last mouse event actually a double click?
|
||||
// Useful for detecting triple clicks -- if a double click is detected
|
||||
// but the last mouse event was actually a double click, it's a triple click
|
||||
doubleClick bool
|
||||
// Same here, just to keep track for mouse move events
|
||||
tripleClick bool
|
||||
|
||||
// Syntax highlighting matches
|
||||
matches SyntaxMatches
|
||||
|
||||
splitNode *LeafNode
|
||||
}
|
||||
|
||||
// NewView returns a new fullscreen view
|
||||
func NewView(buf *Buffer) *View {
|
||||
screenW, screenH := screen.Size()
|
||||
return NewViewWidthHeight(buf, screenW, screenH-1)
|
||||
}
|
||||
|
||||
// NewViewWidthHeight returns a new view with the specified width and height
|
||||
// Note that w and h are raw column and row values
|
||||
func NewViewWidthHeight(buf *Buffer, w, h int) *View {
|
||||
v := new(View)
|
||||
|
||||
v.x, v.y = 0, 0
|
||||
|
||||
v.width = w
|
||||
v.height = h
|
||||
|
||||
v.ToggleTabbar()
|
||||
|
||||
v.OpenBuffer(buf)
|
||||
|
||||
v.messages = make(map[string][]GutterMessage)
|
||||
|
||||
v.sline = Statusline{
|
||||
view: v,
|
||||
}
|
||||
|
||||
if v.Buf.Settings["statusline"].(bool) {
|
||||
v.height--
|
||||
}
|
||||
|
||||
for _, pl := range loadedPlugins {
|
||||
_, err := Call(pl+".onViewOpen", v)
|
||||
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
|
||||
TermMessage(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *View) ToggleStatusLine() {
|
||||
if v.Buf.Settings["statusline"].(bool) {
|
||||
v.height--
|
||||
} else {
|
||||
v.height++
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) ToggleTabbar() {
|
||||
if len(tabs) > 1 {
|
||||
if v.y == 0 {
|
||||
// Include one line for the tab bar at the top
|
||||
v.height--
|
||||
v.y = 1
|
||||
}
|
||||
} else {
|
||||
if v.y == 1 {
|
||||
v.y = 0
|
||||
v.height++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ScrollUp scrolls the view up n lines (if possible)
|
||||
func (v *View) ScrollUp(n int) {
|
||||
// Try to scroll by n but if it would overflow, scroll by 1
|
||||
if v.Topline-n >= 0 {
|
||||
v.Topline -= n
|
||||
} else if v.Topline > 0 {
|
||||
v.Topline--
|
||||
}
|
||||
}
|
||||
|
||||
// ScrollDown scrolls the view down n lines (if possible)
|
||||
func (v *View) ScrollDown(n int) {
|
||||
// Try to scroll by n but if it would overflow, scroll by 1
|
||||
if v.Topline+n <= v.Buf.NumLines-v.height {
|
||||
v.Topline += n
|
||||
} else if v.Topline < v.Buf.NumLines-v.height {
|
||||
v.Topline++
|
||||
}
|
||||
}
|
||||
|
||||
// CanClose returns whether or not the view can be closed
|
||||
// If there are unsaved changes, the user will be asked if the view can be closed
|
||||
// causing them to lose the unsaved changes
|
||||
// The message is what to print after saying "You have unsaved changes. "
|
||||
func (v *View) CanClose(msg string, responses ...rune) bool {
|
||||
if v.Buf.IsModified {
|
||||
char, canceled := messenger.LetterPrompt("You have unsaved changes. "+msg, responses...)
|
||||
if !canceled {
|
||||
if char == 'y' {
|
||||
return true
|
||||
} else if char == 's' {
|
||||
v.Save(true)
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OpenBuffer opens a new buffer in this view.
|
||||
// This resets the topline, event handler and cursor.
|
||||
func (v *View) OpenBuffer(buf *Buffer) {
|
||||
screen.Clear()
|
||||
v.CloseBuffer()
|
||||
v.Buf = buf
|
||||
v.Cursor = &buf.Cursor
|
||||
v.Topline = 0
|
||||
v.leftCol = 0
|
||||
v.Cursor.ResetSelection()
|
||||
v.Relocate()
|
||||
v.Center(false)
|
||||
v.messages = make(map[string][]GutterMessage)
|
||||
|
||||
v.matches = Match(v)
|
||||
|
||||
// Set mouseReleased to true because we assume the mouse is not being pressed when
|
||||
// the editor is opened
|
||||
v.mouseReleased = true
|
||||
v.lastClickTime = time.Time{}
|
||||
}
|
||||
|
||||
// CloseBuffer performs any closing functions on the buffer
|
||||
func (v *View) CloseBuffer() {
|
||||
if v.Buf != nil {
|
||||
v.Buf.Serialize()
|
||||
}
|
||||
}
|
||||
|
||||
// ReOpen reloads the current buffer
|
||||
func (v *View) ReOpen() {
|
||||
if v.CanClose("Continue? (y,n,s) ", 'y', 'n', 's') {
|
||||
screen.Clear()
|
||||
v.Buf.ReOpen()
|
||||
v.Relocate()
|
||||
v.matches = Match(v)
|
||||
}
|
||||
}
|
||||
|
||||
// HSplit opens a horizontal split with the given buffer
|
||||
func (v *View) HSplit(buf *Buffer) bool {
|
||||
v.splitNode.HSplit(buf)
|
||||
tabs[v.TabNum].Resize()
|
||||
return false
|
||||
}
|
||||
|
||||
// VSplit opens a vertical split with the given buffer
|
||||
func (v *View) VSplit(buf *Buffer) bool {
|
||||
v.splitNode.VSplit(buf)
|
||||
tabs[v.TabNum].Resize()
|
||||
return false
|
||||
}
|
||||
|
||||
// Relocate moves the view window so that the cursor is in view
|
||||
// This is useful if the user has scrolled far away, and then starts typing
|
||||
func (v *View) Relocate() bool {
|
||||
ret := false
|
||||
cy := v.Cursor.Y
|
||||
scrollmargin := int(v.Buf.Settings["scrollmargin"].(float64))
|
||||
if cy < v.Topline+scrollmargin && cy > scrollmargin-1 {
|
||||
v.Topline = cy - scrollmargin
|
||||
ret = true
|
||||
} else if cy < v.Topline {
|
||||
v.Topline = cy
|
||||
ret = true
|
||||
}
|
||||
if cy > v.Topline+v.height-1-scrollmargin && cy < v.Buf.NumLines-scrollmargin {
|
||||
v.Topline = cy - v.height + 1 + scrollmargin
|
||||
ret = true
|
||||
} else if cy >= v.Buf.NumLines-scrollmargin && cy > v.height {
|
||||
v.Topline = v.Buf.NumLines - v.height
|
||||
ret = true
|
||||
}
|
||||
|
||||
cx := v.Cursor.GetVisualX()
|
||||
if cx < v.leftCol {
|
||||
v.leftCol = cx
|
||||
ret = true
|
||||
}
|
||||
if cx+v.lineNumOffset+1 > v.leftCol+v.width {
|
||||
v.leftCol = cx - v.width + v.lineNumOffset + 1
|
||||
ret = true
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// MoveToMouseClick moves the cursor to location x, y assuming x, y were given
|
||||
// by a mouse click
|
||||
func (v *View) MoveToMouseClick(x, y int) {
|
||||
if y-v.Topline > v.height-1 {
|
||||
v.ScrollDown(1)
|
||||
y = v.height + v.Topline - 1
|
||||
}
|
||||
if y >= v.Buf.NumLines {
|
||||
y = v.Buf.NumLines - 1
|
||||
}
|
||||
if y < 0 {
|
||||
y = 0
|
||||
}
|
||||
if x < 0 {
|
||||
x = 0
|
||||
}
|
||||
|
||||
x = v.Cursor.GetCharPosInLine(y, x)
|
||||
if x > Count(v.Buf.Line(y)) {
|
||||
x = Count(v.Buf.Line(y))
|
||||
}
|
||||
v.Cursor.X = x
|
||||
v.Cursor.Y = y
|
||||
v.Cursor.LastVisualX = v.Cursor.GetVisualX()
|
||||
}
|
||||
|
||||
// HandleEvent handles an event passed by the main loop
|
||||
func (v *View) HandleEvent(event tcell.Event) {
|
||||
// This bool determines whether the view is relocated at the end of the function
|
||||
// By default it's true because most events should cause a relocate
|
||||
relocate := true
|
||||
|
||||
v.Buf.CheckModTime()
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventResize:
|
||||
// Window resized
|
||||
tabs[v.TabNum].Resize()
|
||||
case *tcell.EventKey:
|
||||
if e.Key() == tcell.KeyRune && (e.Modifiers() == 0 || e.Modifiers() == tcell.ModShift) {
|
||||
// Insert a character
|
||||
if v.Cursor.HasSelection() {
|
||||
v.Cursor.DeleteSelection()
|
||||
v.Cursor.ResetSelection()
|
||||
}
|
||||
v.Buf.Insert(v.Cursor.Loc, string(e.Rune()))
|
||||
v.Cursor.Right()
|
||||
|
||||
for _, pl := range loadedPlugins {
|
||||
_, err := Call(pl+".onRune", string(e.Rune()), v)
|
||||
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
|
||||
TermMessage(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for key, actions := range bindings {
|
||||
if e.Key() == key.keyCode {
|
||||
if e.Key() == tcell.KeyRune {
|
||||
if e.Rune() != key.r {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if e.Modifiers() == key.modifiers {
|
||||
relocate = false
|
||||
for _, action := range actions {
|
||||
relocate = action(v, true) || relocate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case *tcell.EventPaste:
|
||||
relocate = v.Paste(true)
|
||||
case *tcell.EventMouse:
|
||||
x, y := e.Position()
|
||||
x -= v.lineNumOffset - v.leftCol + v.x
|
||||
y += v.Topline - v.y
|
||||
// Don't relocate for mouse events
|
||||
relocate = false
|
||||
|
||||
button := e.Buttons()
|
||||
|
||||
switch button {
|
||||
case tcell.Button1:
|
||||
// Left click
|
||||
if v.mouseReleased {
|
||||
v.MoveToMouseClick(x, y)
|
||||
if time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold {
|
||||
if v.doubleClick {
|
||||
// Triple click
|
||||
v.lastClickTime = time.Now()
|
||||
|
||||
v.tripleClick = true
|
||||
v.doubleClick = false
|
||||
|
||||
v.Cursor.SelectLine()
|
||||
} else {
|
||||
// Double click
|
||||
v.lastClickTime = time.Now()
|
||||
|
||||
v.doubleClick = true
|
||||
v.tripleClick = false
|
||||
|
||||
v.Cursor.SelectWord()
|
||||
}
|
||||
} else {
|
||||
v.doubleClick = false
|
||||
v.tripleClick = false
|
||||
v.lastClickTime = time.Now()
|
||||
|
||||
v.Cursor.OrigSelection[0] = v.Cursor.Loc
|
||||
v.Cursor.CurSelection[0] = v.Cursor.Loc
|
||||
v.Cursor.CurSelection[1] = v.Cursor.Loc
|
||||
}
|
||||
v.mouseReleased = false
|
||||
} else if !v.mouseReleased {
|
||||
v.MoveToMouseClick(x, y)
|
||||
if v.tripleClick {
|
||||
v.Cursor.AddLineToSelection()
|
||||
} else if v.doubleClick {
|
||||
v.Cursor.AddWordToSelection()
|
||||
} else {
|
||||
v.Cursor.CurSelection[1] = v.Cursor.Loc
|
||||
}
|
||||
}
|
||||
case tcell.ButtonNone:
|
||||
// Mouse event with no click
|
||||
if !v.mouseReleased {
|
||||
// Mouse was just released
|
||||
|
||||
// Relocating here isn't really necessary because the cursor will
|
||||
// be in the right place from the last mouse event
|
||||
// However, if we are running in a terminal that doesn't support mouse motion
|
||||
// events, this still allows the user to make selections, except only after they
|
||||
// release the mouse
|
||||
|
||||
if !v.doubleClick && !v.tripleClick {
|
||||
v.MoveToMouseClick(x, y)
|
||||
v.Cursor.CurSelection[1] = v.Cursor.Loc
|
||||
}
|
||||
v.mouseReleased = true
|
||||
}
|
||||
case tcell.WheelUp:
|
||||
// Scroll up
|
||||
scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64))
|
||||
v.ScrollUp(scrollspeed)
|
||||
case tcell.WheelDown:
|
||||
// Scroll down
|
||||
scrollspeed := int(v.Buf.Settings["scrollspeed"].(float64))
|
||||
v.ScrollDown(scrollspeed)
|
||||
}
|
||||
}
|
||||
|
||||
if relocate {
|
||||
v.Relocate()
|
||||
}
|
||||
if v.Buf.Settings["syntax"].(bool) {
|
||||
v.matches = Match(v)
|
||||
}
|
||||
}
|
||||
|
||||
// GutterMessage creates a message in this view's gutter
|
||||
func (v *View) GutterMessage(section string, lineN int, msg string, kind int) {
|
||||
lineN--
|
||||
gutterMsg := GutterMessage{
|
||||
lineNum: lineN,
|
||||
msg: msg,
|
||||
kind: kind,
|
||||
}
|
||||
for _, v := range v.messages {
|
||||
for _, gmsg := range v {
|
||||
if gmsg.lineNum == lineN {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
messages := v.messages[section]
|
||||
v.messages[section] = append(messages, gutterMsg)
|
||||
}
|
||||
|
||||
// ClearGutterMessages clears all gutter messages from a given section
|
||||
func (v *View) ClearGutterMessages(section string) {
|
||||
v.messages[section] = []GutterMessage{}
|
||||
}
|
||||
|
||||
// ClearAllGutterMessages clears all the gutter messages
|
||||
func (v *View) ClearAllGutterMessages() {
|
||||
for k := range v.messages {
|
||||
v.messages[k] = []GutterMessage{}
|
||||
}
|
||||
}
|
||||
|
||||
// Opens the given help page in a new horizontal split
|
||||
func (v *View) openHelp(helpPage string) {
|
||||
if v.Help {
|
||||
helpBuffer := NewBuffer([]byte(helpPages[helpPage]), helpPage+".md")
|
||||
helpBuffer.Name = "Help"
|
||||
v.OpenBuffer(helpBuffer)
|
||||
} else {
|
||||
helpBuffer := NewBuffer([]byte(helpPages[helpPage]), helpPage+".md")
|
||||
helpBuffer.Name = "Help"
|
||||
v.HSplit(helpBuffer)
|
||||
CurView().Help = true
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) drawCell(x, y int, ch rune, combc []rune, style tcell.Style) {
|
||||
if x >= v.x && x < v.x+v.width && y >= v.y && y < v.y+v.height {
|
||||
screen.SetContent(x, y, ch, combc, style)
|
||||
}
|
||||
}
|
||||
|
||||
// DisplayView renders the view to the screen
|
||||
func (v *View) DisplayView() {
|
||||
// The charNum we are currently displaying
|
||||
// starts at the start of the viewport
|
||||
charNum := Loc{0, v.Topline}
|
||||
|
||||
// Convert the length of buffer to a string, and get the length of the string
|
||||
// We are going to have to offset by that amount
|
||||
maxLineLength := len(strconv.Itoa(v.Buf.NumLines))
|
||||
|
||||
if v.Buf.Settings["ruler"] == true {
|
||||
// + 1 for the little space after the line number
|
||||
v.lineNumOffset = maxLineLength + 1
|
||||
} else {
|
||||
v.lineNumOffset = 0
|
||||
}
|
||||
|
||||
// We need to add to the line offset if there are gutter messages
|
||||
var hasGutterMessages bool
|
||||
for _, v := range v.messages {
|
||||
if len(v) > 0 {
|
||||
hasGutterMessages = true
|
||||
}
|
||||
}
|
||||
if hasGutterMessages {
|
||||
v.lineNumOffset += 2
|
||||
}
|
||||
|
||||
if v.x != 0 {
|
||||
// One space for the extra split divider
|
||||
v.lineNumOffset++
|
||||
}
|
||||
|
||||
// These represent the current screen coordinates
|
||||
screenX, screenY := 0, 0
|
||||
|
||||
highlightStyle := defStyle
|
||||
|
||||
// ViewLine is the current line from the top of the viewport
|
||||
for viewLine := 0; viewLine < v.height; viewLine++ {
|
||||
screenY = v.y + viewLine
|
||||
screenX = v.x
|
||||
|
||||
// This is the current line number of the buffer that we are drawing
|
||||
curLineN := viewLine + v.Topline
|
||||
|
||||
if v.x != 0 {
|
||||
// Draw the split divider
|
||||
v.drawCell(screenX, screenY, '|', nil, defStyle.Reverse(true))
|
||||
screenX++
|
||||
}
|
||||
|
||||
// If the buffer is smaller than the view height we have to clear all this space
|
||||
if curLineN >= v.Buf.NumLines {
|
||||
for i := screenX; i < v.x+v.width; i++ {
|
||||
v.drawCell(i, screenY, ' ', nil, defStyle)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
line := v.Buf.Line(curLineN)
|
||||
|
||||
// If there are gutter messages we need to display the '>>' symbol here
|
||||
if hasGutterMessages {
|
||||
// msgOnLine stores whether or not there is a gutter message on this line in particular
|
||||
msgOnLine := false
|
||||
for k := range v.messages {
|
||||
for _, msg := range v.messages[k] {
|
||||
if msg.lineNum == curLineN {
|
||||
msgOnLine = true
|
||||
gutterStyle := defStyle
|
||||
switch msg.kind {
|
||||
case GutterInfo:
|
||||
if style, ok := colorscheme["gutter-info"]; ok {
|
||||
gutterStyle = style
|
||||
}
|
||||
case GutterWarning:
|
||||
if style, ok := colorscheme["gutter-warning"]; ok {
|
||||
gutterStyle = style
|
||||
}
|
||||
case GutterError:
|
||||
if style, ok := colorscheme["gutter-error"]; ok {
|
||||
gutterStyle = style
|
||||
}
|
||||
}
|
||||
v.drawCell(screenX, screenY, '>', nil, gutterStyle)
|
||||
screenX++
|
||||
v.drawCell(screenX, screenY, '>', nil, gutterStyle)
|
||||
screenX++
|
||||
if v.Cursor.Y == curLineN && !messenger.hasPrompt {
|
||||
messenger.Message(msg.msg)
|
||||
messenger.gutterMessage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If there is no message on this line we just display an empty offset
|
||||
if !msgOnLine {
|
||||
v.drawCell(screenX, screenY, ' ', nil, defStyle)
|
||||
screenX++
|
||||
v.drawCell(screenX, screenY, ' ', nil, defStyle)
|
||||
screenX++
|
||||
if v.Cursor.Y == curLineN && messenger.gutterMessage {
|
||||
messenger.Reset()
|
||||
messenger.gutterMessage = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if v.Buf.Settings["ruler"] == true {
|
||||
// Write the line number
|
||||
lineNumStyle := defStyle
|
||||
if style, ok := colorscheme["line-number"]; ok {
|
||||
lineNumStyle = style
|
||||
}
|
||||
if style, ok := colorscheme["current-line-number"]; ok {
|
||||
if curLineN == v.Cursor.Y && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() {
|
||||
lineNumStyle = style
|
||||
}
|
||||
}
|
||||
|
||||
lineNum := strconv.Itoa(curLineN + 1)
|
||||
|
||||
// Write the spaces before the line number if necessary
|
||||
for i := 0; i < maxLineLength-len(lineNum); i++ {
|
||||
v.drawCell(screenX, screenY, ' ', nil, lineNumStyle)
|
||||
screenX++
|
||||
}
|
||||
// Write the actual line number
|
||||
for _, ch := range lineNum {
|
||||
v.drawCell(screenX, screenY, ch, nil, lineNumStyle)
|
||||
screenX++
|
||||
}
|
||||
|
||||
// Write the extra space
|
||||
v.drawCell(screenX, screenY, ' ', nil, lineNumStyle)
|
||||
screenX++
|
||||
}
|
||||
|
||||
// Now we actually draw the line
|
||||
colN := 0
|
||||
for _, ch := range line {
|
||||
lineStyle := defStyle
|
||||
|
||||
if v.Buf.Settings["syntax"].(bool) {
|
||||
// Syntax highlighting is enabled
|
||||
highlightStyle = v.matches[viewLine][colN]
|
||||
}
|
||||
|
||||
if v.Cursor.HasSelection() &&
|
||||
(charNum.GreaterEqual(v.Cursor.CurSelection[0]) && charNum.LessThan(v.Cursor.CurSelection[1]) ||
|
||||
charNum.LessThan(v.Cursor.CurSelection[0]) && charNum.GreaterEqual(v.Cursor.CurSelection[1])) {
|
||||
// The current character is selected
|
||||
lineStyle = defStyle.Reverse(true)
|
||||
|
||||
if style, ok := colorscheme["selection"]; ok {
|
||||
lineStyle = style
|
||||
}
|
||||
} else {
|
||||
lineStyle = highlightStyle
|
||||
}
|
||||
|
||||
// We need to display the background of the linestyle with the correct color if cursorline is enabled
|
||||
// and this is the current view and there is no selection on this line and the cursor is on this line
|
||||
if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN {
|
||||
if style, ok := colorscheme["cursor-line"]; ok {
|
||||
fg, _, _ := style.Decompose()
|
||||
lineStyle = lineStyle.Background(fg)
|
||||
}
|
||||
}
|
||||
|
||||
if ch == '\t' {
|
||||
// If the character we are displaying is a tab, we need to do a bunch of special things
|
||||
|
||||
// First the user may have configured an `indent-char` to be displayed to show that this
|
||||
// is a tab character
|
||||
lineIndentStyle := defStyle
|
||||
if style, ok := colorscheme["indent-char"]; ok {
|
||||
lineIndentStyle = style
|
||||
}
|
||||
if v.Cursor.HasSelection() &&
|
||||
(charNum.GreaterEqual(v.Cursor.CurSelection[0]) && charNum.LessThan(v.Cursor.CurSelection[1]) ||
|
||||
charNum.LessThan(v.Cursor.CurSelection[0]) && charNum.GreaterEqual(v.Cursor.CurSelection[1])) {
|
||||
|
||||
lineIndentStyle = defStyle.Reverse(true)
|
||||
|
||||
if style, ok := colorscheme["selection"]; ok {
|
||||
lineIndentStyle = style
|
||||
}
|
||||
}
|
||||
if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN {
|
||||
if style, ok := colorscheme["cursor-line"]; ok {
|
||||
fg, _, _ := style.Decompose()
|
||||
lineIndentStyle = lineIndentStyle.Background(fg)
|
||||
}
|
||||
}
|
||||
// Here we get the indent char
|
||||
indentChar := []rune(v.Buf.Settings["indentchar"].(string))
|
||||
if screenX-v.x-v.leftCol >= v.lineNumOffset {
|
||||
v.drawCell(screenX-v.leftCol, screenY, indentChar[0], nil, lineIndentStyle)
|
||||
}
|
||||
// Now the tab has to be displayed as a bunch of spaces
|
||||
tabSize := int(v.Buf.Settings["tabsize"].(float64))
|
||||
for i := 0; i < tabSize-1; i++ {
|
||||
screenX++
|
||||
if screenX-v.x-v.leftCol >= v.lineNumOffset {
|
||||
v.drawCell(screenX-v.leftCol, screenY, ' ', nil, lineStyle)
|
||||
}
|
||||
}
|
||||
} else if runewidth.RuneWidth(ch) > 1 {
|
||||
if screenX-v.x-v.leftCol >= v.lineNumOffset {
|
||||
v.drawCell(screenX, screenY, ch, nil, lineStyle)
|
||||
}
|
||||
for i := 0; i < runewidth.RuneWidth(ch)-1; i++ {
|
||||
screenX++
|
||||
if screenX-v.x-v.leftCol >= v.lineNumOffset {
|
||||
v.drawCell(screenX-v.leftCol, screenY, '<', nil, lineStyle)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if screenX-v.x-v.leftCol >= v.lineNumOffset {
|
||||
v.drawCell(screenX-v.leftCol, screenY, ch, nil, lineStyle)
|
||||
}
|
||||
}
|
||||
charNum = charNum.Move(1, v.Buf)
|
||||
screenX++
|
||||
colN++
|
||||
}
|
||||
// Here we are at a newline
|
||||
|
||||
// The newline may be selected, in which case we should draw the selection style
|
||||
// with a space to represent it
|
||||
if v.Cursor.HasSelection() &&
|
||||
(charNum.GreaterEqual(v.Cursor.CurSelection[0]) && charNum.LessThan(v.Cursor.CurSelection[1]) ||
|
||||
charNum.LessThan(v.Cursor.CurSelection[0]) && charNum.GreaterEqual(v.Cursor.CurSelection[1])) {
|
||||
|
||||
selectStyle := defStyle.Reverse(true)
|
||||
|
||||
if style, ok := colorscheme["selection"]; ok {
|
||||
selectStyle = style
|
||||
}
|
||||
v.drawCell(screenX, screenY, ' ', nil, selectStyle)
|
||||
screenX++
|
||||
}
|
||||
|
||||
charNum = charNum.Move(1, v.Buf)
|
||||
|
||||
for i := 0; i < v.width; i++ {
|
||||
lineStyle := defStyle
|
||||
if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN {
|
||||
if style, ok := colorscheme["cursor-line"]; ok {
|
||||
fg, _, _ := style.Decompose()
|
||||
lineStyle = lineStyle.Background(fg)
|
||||
}
|
||||
}
|
||||
if screenX-v.x-v.leftCol+i >= v.lineNumOffset {
|
||||
v.drawCell(screenX-v.leftCol+i, screenY, ' ', nil, lineStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DisplayCursor draws the current buffer's cursor to the screen
|
||||
func (v *View) DisplayCursor() {
|
||||
// Don't draw the cursor if it is out of the viewport or if it has a selection
|
||||
if (v.Cursor.Y-v.Topline < 0 || v.Cursor.Y-v.Topline > v.height-1) || v.Cursor.HasSelection() {
|
||||
screen.HideCursor()
|
||||
} else {
|
||||
screen.ShowCursor(v.x+v.Cursor.GetVisualX()+v.lineNumOffset-v.leftCol, v.Cursor.Y-v.Topline+v.y)
|
||||
}
|
||||
}
|
||||
|
||||
// Display renders the view, the cursor, and statusline
|
||||
func (v *View) Display() {
|
||||
v.DisplayView()
|
||||
if v.Num == tabs[curTab].curView {
|
||||
v.DisplayCursor()
|
||||
}
|
||||
_, screenH := screen.Size()
|
||||
if v.Buf.Settings["statusline"].(bool) {
|
||||
v.sline.Display()
|
||||
} else if (v.y + v.height) != screenH-1 {
|
||||
for x := 0; x < v.width; x++ {
|
||||
screen.SetContent(v.x+x, v.y+v.height, '-', nil, defStyle.Reverse(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
23
data/com.github.zyedidia.micro.metainfo.xml
Normal file
23
data/com.github.zyedidia.micro.metainfo.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<component>
|
||||
<id>com.github.zyedidia.micro</id>
|
||||
<name>Micro Text Editor</name>
|
||||
<summary>A modern and intuitive terminal-based text editor</summary>
|
||||
<metadata_license>MIT</metadata_license>
|
||||
<categories>
|
||||
<category>Development</category>
|
||||
<category>TextEditor</category>
|
||||
</categories>
|
||||
<provides>
|
||||
<binary>micro</binary>
|
||||
</provides>
|
||||
<developer_name>Zachary Yedidia</developer_name>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<caption>Micro Text Editor editing its source code.</caption>
|
||||
<image type="source">https://raw.githubusercontent.com/zyedidia/micro/master/assets/micro-solarized.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<url type="homepage">https://micro-editor.github.io</url>
|
||||
<url type="bugtracker">https://github.com/zyedidia/micro/issues</url>
|
||||
</component>
|
||||
28
go.mod
Normal file
28
go.mod
Normal file
@@ -0,0 +1,28 @@
|
||||
module github.com/zyedidia/micro
|
||||
|
||||
require (
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/go-errors/errors v1.0.1
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/mattn/go-isatty v0.0.11
|
||||
github.com/mattn/go-runewidth v0.0.7
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff
|
||||
github.com/sergi/go-diff v1.1.0
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb
|
||||
github.com/zyedidia/clipboard v0.0.0-20190823154308-241f98e9b197
|
||||
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3
|
||||
github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5
|
||||
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d
|
||||
github.com/zyedidia/pty v2.0.0+incompatible // indirect
|
||||
github.com/zyedidia/tcell v1.4.4
|
||||
github.com/zyedidia/terminal v0.0.0-20180726154117-533c623e2415
|
||||
golang.org/x/text v0.3.2
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.7
|
||||
layeh.com/gopher-luar v1.0.7
|
||||
)
|
||||
|
||||
go 1.11
|
||||
77
go.sum
Normal file
77
go.sum
Normal file
@@ -0,0 +1,77 @@
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff h1:+6NUiITWwE5q1KO6SAfUX918c+Tab0+tGAM/mtdlUyA=
|
||||
github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
|
||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||
github.com/zyedidia/clipboard v0.0.0-20190823154308-241f98e9b197 h1:gYTNnAW6azuB3BbA6QYWO/H4F2ABSOjjw3Z03tlXd2c=
|
||||
github.com/zyedidia/clipboard v0.0.0-20190823154308-241f98e9b197/go.mod h1:WDk3p8GiZV9+xFWlSo8qreeoLhW6Ik692rqXk+cNeRY=
|
||||
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3 h1:oMHjjTLfGXVuyOQBYj5/td9WC0mw4g1xDBPovIqmHew=
|
||||
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3/go.mod h1:YKbIYP//Eln8eDgAJGI3IDvR3s4Tv9Z9TGIOumiyQ5c=
|
||||
github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5 h1:Zs6mpwXvlqpF9zHl5XaN0p5V4J9XvP+WBuiuXyIgqvc=
|
||||
github.com/zyedidia/highlight v0.0.0-20170330143449-201131ce5cf5/go.mod h1:c1r+Ob9tUTPB0FKWO1+x+Hsc/zNa45WdGq7Y38Ybip0=
|
||||
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d h1:zmDMkh22zXOB7gz8jFaI4GpI7llsPgzm38/jG0UgxjE=
|
||||
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d/go.mod h1:NDJSTTYWivnza6zkRapeX2/LwhKPEMQ7bJxqgDVT78I=
|
||||
github.com/zyedidia/poller v1.0.1 h1:Tt9S3AxAjXwWGNiC2TUdRJkQDZSzCBNVQ4xXiQ7440s=
|
||||
github.com/zyedidia/poller v1.0.1/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE=
|
||||
github.com/zyedidia/pty v2.0.0+incompatible h1:Ou5vXL6tvjst+RV8sUFISbuKDnUJPhnpygApMFGweqw=
|
||||
github.com/zyedidia/pty v2.0.0+incompatible/go.mod h1:4y9l9yJZNxRa7GB/fB+mmDmGkG3CqmzLf4vUxGGotEA=
|
||||
github.com/zyedidia/tcell v1.4.4 h1:o34LXujNuSueuyTy+5eoQW+rQr8g0UbY8k1NczZyskQ=
|
||||
github.com/zyedidia/tcell v1.4.4/go.mod h1:HhlbMSCcGX15rFDB+Q1Lk3pKEOocsCUAQC3zhZ9sadA=
|
||||
github.com/zyedidia/terminal v0.0.0-20180726154117-533c623e2415 h1:752dTQ5OatJ9M5ULK2+9lor+nzyZz+LYDo3WGngg3Rc=
|
||||
github.com/zyedidia/terminal v0.0.0-20180726154117-533c623e2415/go.mod h1:8leT8G0Cm8NoJHdrrKHyR9MirWoF4YW7pZh06B6H+1E=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
|
||||
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
layeh.com/gopher-luar v1.0.7 h1:53iv6CCkRs5wyofZ+qVXcyAYQOIG52s6pt4xkqZdq7k=
|
||||
layeh.com/gopher-luar v1.0.7/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk=
|
||||
1730
internal/action/actions.go
Normal file
1730
internal/action/actions.go
Normal file
File diff suppressed because it is too large
Load Diff
8
internal/action/actions_other.go
Normal file
8
internal/action/actions_other.go
Normal file
@@ -0,0 +1,8 @@
|
||||
// +build plan9 nacl windows
|
||||
|
||||
package action
|
||||
|
||||
func (*BufPane) Suspend() bool {
|
||||
InfoBar.Error("Suspend is only supported on BSD/Linux")
|
||||
return false
|
||||
}
|
||||
27
internal/action/actions_posix.go
Normal file
27
internal/action/actions_posix.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// +build linux darwin dragonfly solaris openbsd netbsd freebsd
|
||||
|
||||
package action
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
)
|
||||
|
||||
// Suspend sends micro to the background. This is the same as pressing CtrlZ in most unix programs.
|
||||
// This only works on linux and has no default binding.
|
||||
// This code was adapted from the suspend code in nsf/godit
|
||||
func (*BufPane) Suspend() bool {
|
||||
screenb := screen.TempFini()
|
||||
|
||||
// suspend the process
|
||||
pid := syscall.Getpid()
|
||||
err := syscall.Kill(pid, syscall.SIGSTOP)
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
screen.TempStart(screenb)
|
||||
|
||||
return false
|
||||
}
|
||||
400
internal/action/bindings.go
Normal file
400
internal/action/bindings.go
Normal file
@@ -0,0 +1,400 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/zyedidia/json5"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
func createBindingsIfNotExist(fname string) {
|
||||
if _, e := os.Stat(fname); os.IsNotExist(e) {
|
||||
ioutil.WriteFile(fname, []byte("{}"), 0644)
|
||||
}
|
||||
}
|
||||
|
||||
// InitBindings intializes the bindings map by reading from bindings.json
|
||||
func InitBindings() {
|
||||
config.Bindings = DefaultBindings()
|
||||
|
||||
var parsed map[string]string
|
||||
defaults := DefaultBindings()
|
||||
|
||||
filename := filepath.Join(config.ConfigDir, "bindings.json")
|
||||
createBindingsIfNotExist(filename)
|
||||
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error reading bindings.json file: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
err = json5.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
screen.TermMessage("Error reading bindings.json:", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range defaults {
|
||||
BindKey(k, v)
|
||||
}
|
||||
for k, v := range parsed {
|
||||
BindKey(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func BindKey(k, v string) {
|
||||
event, ok := findEvent(k)
|
||||
if !ok {
|
||||
screen.TermMessage(k, "is not a bindable event")
|
||||
}
|
||||
|
||||
switch e := event.(type) {
|
||||
case KeyEvent:
|
||||
BufMapKey(e, v)
|
||||
case MouseEvent:
|
||||
BufMapMouse(e, v)
|
||||
case RawEvent:
|
||||
BufMapKey(e, v)
|
||||
}
|
||||
|
||||
config.Bindings[k] = v
|
||||
}
|
||||
|
||||
// findEvent will find binding Key 'b' using string 'k'
|
||||
func findEvent(k string) (b Event, ok bool) {
|
||||
modifiers := tcell.ModNone
|
||||
|
||||
// First, we'll strip off all the modifiers in the name and add them to the
|
||||
// ModMask
|
||||
modSearch:
|
||||
for {
|
||||
switch {
|
||||
case strings.HasPrefix(k, "-"):
|
||||
// We optionally support dashes between modifiers
|
||||
k = k[1:]
|
||||
case strings.HasPrefix(k, "Ctrl") && k != "CtrlH":
|
||||
// CtrlH technically does not have a 'Ctrl' modifier because it is really backspace
|
||||
k = k[4:]
|
||||
modifiers |= tcell.ModCtrl
|
||||
case strings.HasPrefix(k, "Alt"):
|
||||
k = k[3:]
|
||||
modifiers |= tcell.ModAlt
|
||||
case strings.HasPrefix(k, "Shift"):
|
||||
k = k[5:]
|
||||
modifiers |= tcell.ModShift
|
||||
case strings.HasPrefix(k, "\x1b"):
|
||||
screen.Screen.RegisterRawSeq(k)
|
||||
return RawEvent{
|
||||
esc: k,
|
||||
}, true
|
||||
default:
|
||||
break modSearch
|
||||
}
|
||||
}
|
||||
|
||||
if len(k) == 0 {
|
||||
return KeyEvent{}, false
|
||||
}
|
||||
|
||||
// Control is handled in a special way, since the terminal sends explicitly
|
||||
// marked escape sequences for control keys
|
||||
// We should check for Control keys first
|
||||
if modifiers&tcell.ModCtrl != 0 {
|
||||
// see if the key is in bindingKeys with the Ctrl prefix.
|
||||
k = string(unicode.ToUpper(rune(k[0]))) + k[1:]
|
||||
if code, ok := keyEvents["Ctrl"+k]; ok {
|
||||
var r tcell.Key
|
||||
// Special case for escape, for some reason tcell doesn't send it with the esc character
|
||||
if code < 256 && code != 27 {
|
||||
r = code
|
||||
}
|
||||
// It is, we're done.
|
||||
return KeyEvent{
|
||||
code: code,
|
||||
mod: modifiers,
|
||||
r: rune(r),
|
||||
}, true
|
||||
}
|
||||
}
|
||||
|
||||
// See if we can find the key in bindingKeys
|
||||
if code, ok := keyEvents[k]; ok {
|
||||
var r tcell.Key
|
||||
// Special case for escape, for some reason tcell doesn't send it with the esc character
|
||||
if code < 256 && code != 27 {
|
||||
r = code
|
||||
}
|
||||
return KeyEvent{
|
||||
code: code,
|
||||
mod: modifiers,
|
||||
r: rune(r),
|
||||
}, true
|
||||
}
|
||||
|
||||
// See if we can find the key in bindingMouse
|
||||
if code, ok := mouseEvents[k]; ok {
|
||||
return MouseEvent{
|
||||
btn: code,
|
||||
mod: modifiers,
|
||||
}, true
|
||||
}
|
||||
|
||||
// If we were given one character, then we've got a rune.
|
||||
if len(k) == 1 {
|
||||
return KeyEvent{
|
||||
code: tcell.KeyRune,
|
||||
mod: modifiers,
|
||||
r: rune(k[0]),
|
||||
}, true
|
||||
}
|
||||
|
||||
// We don't know what happened.
|
||||
return KeyEvent{}, false
|
||||
}
|
||||
|
||||
// TryBindKey tries to bind a key by writing to config.ConfigDir/bindings.json
|
||||
// Returns true if the keybinding already existed and a possible error
|
||||
func TryBindKey(k, v string, overwrite bool) (bool, error) {
|
||||
var e error
|
||||
var parsed map[string]string
|
||||
|
||||
filename := filepath.Join(config.ConfigDir, "bindings.json")
|
||||
createBindingsIfNotExist(filename)
|
||||
if _, e = os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return false, errors.New("Error reading bindings.json file: " + err.Error())
|
||||
}
|
||||
|
||||
err = json5.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
return false, errors.New("Error reading bindings.json: " + err.Error())
|
||||
}
|
||||
|
||||
key, ok := findEvent(k)
|
||||
if !ok {
|
||||
return false, errors.New("Invalid event " + k)
|
||||
}
|
||||
|
||||
found := false
|
||||
for ev := range parsed {
|
||||
if e, ok := findEvent(ev); ok {
|
||||
if e == key {
|
||||
if overwrite {
|
||||
parsed[ev] = v
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found && !overwrite {
|
||||
return true, nil
|
||||
} else if !found {
|
||||
parsed[k] = v
|
||||
}
|
||||
|
||||
BindKey(k, v)
|
||||
|
||||
txt, _ := json.MarshalIndent(parsed, "", " ")
|
||||
return true, ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
||||
}
|
||||
return false, e
|
||||
}
|
||||
|
||||
// UnbindKey removes the binding for a key from the bindings.json file
|
||||
func UnbindKey(k string) error {
|
||||
var e error
|
||||
var parsed map[string]string
|
||||
|
||||
filename := filepath.Join(config.ConfigDir, "bindings.json")
|
||||
createBindingsIfNotExist(filename)
|
||||
if _, e = os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return errors.New("Error reading bindings.json file: " + err.Error())
|
||||
}
|
||||
|
||||
err = json5.Unmarshal(input, &parsed)
|
||||
if err != nil {
|
||||
return errors.New("Error reading bindings.json: " + err.Error())
|
||||
}
|
||||
|
||||
key, ok := findEvent(k)
|
||||
if !ok {
|
||||
return errors.New("Invalid event " + k)
|
||||
}
|
||||
|
||||
for ev := range parsed {
|
||||
if e, ok := findEvent(ev); ok {
|
||||
if e == key {
|
||||
delete(parsed, ev)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaults := DefaultBindings()
|
||||
if a, ok := defaults[k]; ok {
|
||||
BindKey(k, a)
|
||||
} else if _, ok := config.Bindings[k]; ok {
|
||||
delete(config.Bindings, k)
|
||||
}
|
||||
|
||||
txt, _ := json.MarshalIndent(parsed, "", " ")
|
||||
return ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
var mouseEvents = map[string]tcell.ButtonMask{
|
||||
"MouseLeft": tcell.Button1,
|
||||
"MouseMiddle": tcell.Button2,
|
||||
"MouseRight": tcell.Button3,
|
||||
"MouseWheelUp": tcell.WheelUp,
|
||||
"MouseWheelDown": tcell.WheelDown,
|
||||
"MouseWheelLeft": tcell.WheelLeft,
|
||||
"MouseWheelRight": tcell.WheelRight,
|
||||
}
|
||||
|
||||
var keyEvents = map[string]tcell.Key{
|
||||
"Up": tcell.KeyUp,
|
||||
"Down": tcell.KeyDown,
|
||||
"Right": tcell.KeyRight,
|
||||
"Left": tcell.KeyLeft,
|
||||
"UpLeft": tcell.KeyUpLeft,
|
||||
"UpRight": tcell.KeyUpRight,
|
||||
"DownLeft": tcell.KeyDownLeft,
|
||||
"DownRight": tcell.KeyDownRight,
|
||||
"Center": tcell.KeyCenter,
|
||||
"PageUp": tcell.KeyPgUp,
|
||||
"PageDown": tcell.KeyPgDn,
|
||||
"Home": tcell.KeyHome,
|
||||
"End": tcell.KeyEnd,
|
||||
"Insert": tcell.KeyInsert,
|
||||
"Delete": tcell.KeyDelete,
|
||||
"Help": tcell.KeyHelp,
|
||||
"Exit": tcell.KeyExit,
|
||||
"Clear": tcell.KeyClear,
|
||||
"Cancel": tcell.KeyCancel,
|
||||
"Print": tcell.KeyPrint,
|
||||
"Pause": tcell.KeyPause,
|
||||
"Backtab": tcell.KeyBacktab,
|
||||
"F1": tcell.KeyF1,
|
||||
"F2": tcell.KeyF2,
|
||||
"F3": tcell.KeyF3,
|
||||
"F4": tcell.KeyF4,
|
||||
"F5": tcell.KeyF5,
|
||||
"F6": tcell.KeyF6,
|
||||
"F7": tcell.KeyF7,
|
||||
"F8": tcell.KeyF8,
|
||||
"F9": tcell.KeyF9,
|
||||
"F10": tcell.KeyF10,
|
||||
"F11": tcell.KeyF11,
|
||||
"F12": tcell.KeyF12,
|
||||
"F13": tcell.KeyF13,
|
||||
"F14": tcell.KeyF14,
|
||||
"F15": tcell.KeyF15,
|
||||
"F16": tcell.KeyF16,
|
||||
"F17": tcell.KeyF17,
|
||||
"F18": tcell.KeyF18,
|
||||
"F19": tcell.KeyF19,
|
||||
"F20": tcell.KeyF20,
|
||||
"F21": tcell.KeyF21,
|
||||
"F22": tcell.KeyF22,
|
||||
"F23": tcell.KeyF23,
|
||||
"F24": tcell.KeyF24,
|
||||
"F25": tcell.KeyF25,
|
||||
"F26": tcell.KeyF26,
|
||||
"F27": tcell.KeyF27,
|
||||
"F28": tcell.KeyF28,
|
||||
"F29": tcell.KeyF29,
|
||||
"F30": tcell.KeyF30,
|
||||
"F31": tcell.KeyF31,
|
||||
"F32": tcell.KeyF32,
|
||||
"F33": tcell.KeyF33,
|
||||
"F34": tcell.KeyF34,
|
||||
"F35": tcell.KeyF35,
|
||||
"F36": tcell.KeyF36,
|
||||
"F37": tcell.KeyF37,
|
||||
"F38": tcell.KeyF38,
|
||||
"F39": tcell.KeyF39,
|
||||
"F40": tcell.KeyF40,
|
||||
"F41": tcell.KeyF41,
|
||||
"F42": tcell.KeyF42,
|
||||
"F43": tcell.KeyF43,
|
||||
"F44": tcell.KeyF44,
|
||||
"F45": tcell.KeyF45,
|
||||
"F46": tcell.KeyF46,
|
||||
"F47": tcell.KeyF47,
|
||||
"F48": tcell.KeyF48,
|
||||
"F49": tcell.KeyF49,
|
||||
"F50": tcell.KeyF50,
|
||||
"F51": tcell.KeyF51,
|
||||
"F52": tcell.KeyF52,
|
||||
"F53": tcell.KeyF53,
|
||||
"F54": tcell.KeyF54,
|
||||
"F55": tcell.KeyF55,
|
||||
"F56": tcell.KeyF56,
|
||||
"F57": tcell.KeyF57,
|
||||
"F58": tcell.KeyF58,
|
||||
"F59": tcell.KeyF59,
|
||||
"F60": tcell.KeyF60,
|
||||
"F61": tcell.KeyF61,
|
||||
"F62": tcell.KeyF62,
|
||||
"F63": tcell.KeyF63,
|
||||
"F64": tcell.KeyF64,
|
||||
"CtrlSpace": tcell.KeyCtrlSpace,
|
||||
"CtrlA": tcell.KeyCtrlA,
|
||||
"CtrlB": tcell.KeyCtrlB,
|
||||
"CtrlC": tcell.KeyCtrlC,
|
||||
"CtrlD": tcell.KeyCtrlD,
|
||||
"CtrlE": tcell.KeyCtrlE,
|
||||
"CtrlF": tcell.KeyCtrlF,
|
||||
"CtrlG": tcell.KeyCtrlG,
|
||||
"CtrlH": tcell.KeyCtrlH,
|
||||
"CtrlI": tcell.KeyCtrlI,
|
||||
"CtrlJ": tcell.KeyCtrlJ,
|
||||
"CtrlK": tcell.KeyCtrlK,
|
||||
"CtrlL": tcell.KeyCtrlL,
|
||||
"CtrlM": tcell.KeyCtrlM,
|
||||
"CtrlN": tcell.KeyCtrlN,
|
||||
"CtrlO": tcell.KeyCtrlO,
|
||||
"CtrlP": tcell.KeyCtrlP,
|
||||
"CtrlQ": tcell.KeyCtrlQ,
|
||||
"CtrlR": tcell.KeyCtrlR,
|
||||
"CtrlS": tcell.KeyCtrlS,
|
||||
"CtrlT": tcell.KeyCtrlT,
|
||||
"CtrlU": tcell.KeyCtrlU,
|
||||
"CtrlV": tcell.KeyCtrlV,
|
||||
"CtrlW": tcell.KeyCtrlW,
|
||||
"CtrlX": tcell.KeyCtrlX,
|
||||
"CtrlY": tcell.KeyCtrlY,
|
||||
"CtrlZ": tcell.KeyCtrlZ,
|
||||
"CtrlLeftSq": tcell.KeyCtrlLeftSq,
|
||||
"CtrlBackslash": tcell.KeyCtrlBackslash,
|
||||
"CtrlRightSq": tcell.KeyCtrlRightSq,
|
||||
"CtrlCarat": tcell.KeyCtrlCarat,
|
||||
"CtrlUnderscore": tcell.KeyCtrlUnderscore,
|
||||
"Tab": tcell.KeyTab,
|
||||
"Esc": tcell.KeyEsc,
|
||||
"Escape": tcell.KeyEscape,
|
||||
"Enter": tcell.KeyEnter,
|
||||
"Backspace": tcell.KeyBackspace2,
|
||||
"OldBackspace": tcell.KeyBackspace,
|
||||
|
||||
// I renamed these keys to PageUp and PageDown but I don't want to break someone's keybindings
|
||||
"PgUp": tcell.KeyPgUp,
|
||||
"PgDown": tcell.KeyPgDn,
|
||||
}
|
||||
691
internal/action/bufpane.go
Normal file
691
internal/action/bufpane.go
Normal file
@@ -0,0 +1,691 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
luar "layeh.com/gopher-luar"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/display"
|
||||
ulua "github.com/zyedidia/micro/internal/lua"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
type BufKeyAction func(*BufPane) bool
|
||||
type BufMouseAction func(*BufPane, *tcell.EventMouse) bool
|
||||
|
||||
var BufKeyBindings map[Event]BufKeyAction
|
||||
var BufKeyStrings map[Event]string
|
||||
var BufMouseBindings map[MouseEvent]BufMouseAction
|
||||
|
||||
func init() {
|
||||
BufKeyBindings = make(map[Event]BufKeyAction)
|
||||
BufKeyStrings = make(map[Event]string)
|
||||
BufMouseBindings = make(map[MouseEvent]BufMouseAction)
|
||||
}
|
||||
|
||||
func LuaAction(fn string) func(*BufPane) bool {
|
||||
luaFn := strings.Split(fn, ".")
|
||||
if len(luaFn) <= 1 {
|
||||
return nil
|
||||
}
|
||||
plName, plFn := luaFn[0], luaFn[1]
|
||||
pl := config.FindPlugin(plName)
|
||||
if pl == nil {
|
||||
return nil
|
||||
}
|
||||
return func(h *BufPane) bool {
|
||||
val, err := pl.Call(plFn, luar.New(ulua.L, h))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
if v, ok := val.(lua.LBool); !ok {
|
||||
return false
|
||||
} else {
|
||||
return bool(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BufMapKey maps a key event to an action
|
||||
func BufMapKey(k Event, action string) {
|
||||
BufKeyStrings[k] = action
|
||||
var actionfns []func(*BufPane) bool
|
||||
var names []string
|
||||
var types []byte
|
||||
for i := 0; ; i++ {
|
||||
if action == "" {
|
||||
break
|
||||
}
|
||||
|
||||
// TODO: fix problem when complex bindings have these
|
||||
// characters (escape them?)
|
||||
idx := strings.IndexAny(action, "&|,")
|
||||
a := action
|
||||
if idx >= 0 {
|
||||
a = action[:idx]
|
||||
types = append(types, action[idx])
|
||||
action = action[idx+1:]
|
||||
} else {
|
||||
types = append(types, ' ')
|
||||
action = ""
|
||||
}
|
||||
|
||||
var afn func(*BufPane) bool
|
||||
if strings.HasPrefix(a, "command:") {
|
||||
a = strings.SplitN(a, ":", 2)[1]
|
||||
afn = CommandAction(a)
|
||||
names = append(names, "")
|
||||
} else if strings.HasPrefix(a, "command-edit:") {
|
||||
a = strings.SplitN(a, ":", 2)[1]
|
||||
afn = CommandEditAction(a)
|
||||
names = append(names, "")
|
||||
} else if strings.HasPrefix(a, "lua:") {
|
||||
a = strings.SplitN(a, ":", 2)[1]
|
||||
afn = LuaAction(a)
|
||||
if afn == nil {
|
||||
screen.TermMessage("Lua Error:", a, "does not exist")
|
||||
continue
|
||||
}
|
||||
split := strings.SplitN(a, ".", 2)
|
||||
if len(split) > 1 {
|
||||
a = strings.Title(split[0]) + strings.Title(split[1])
|
||||
} else {
|
||||
a = strings.Title(a)
|
||||
}
|
||||
|
||||
names = append(names, a)
|
||||
} else if f, ok := BufKeyActions[a]; ok {
|
||||
afn = f
|
||||
names = append(names, a)
|
||||
} else {
|
||||
screen.TermMessage("Error:", a, "does not exist")
|
||||
continue
|
||||
}
|
||||
actionfns = append(actionfns, afn)
|
||||
}
|
||||
BufKeyBindings[k] = func(h *BufPane) bool {
|
||||
cursors := h.Buf.GetCursors()
|
||||
success := true
|
||||
for i, a := range actionfns {
|
||||
for j, c := range cursors {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
h.Buf.SetCurCursor(c.Num)
|
||||
h.Cursor = c
|
||||
if i == 0 || (success && types[i-1] == '&') || (!success && types[i-1] == '|') || (types[i-1] == ',') {
|
||||
success = h.execAction(a, names[i], j)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// BufMapMouse maps a mouse event to an action
|
||||
func BufMapMouse(k MouseEvent, action string) {
|
||||
if f, ok := BufMouseActions[action]; ok {
|
||||
BufMouseBindings[k] = f
|
||||
} else {
|
||||
delete(BufMouseBindings, k)
|
||||
BufMapKey(k, action)
|
||||
}
|
||||
}
|
||||
|
||||
// The BufPane connects the buffer and the window
|
||||
// It provides a cursor (or multiple) and defines a set of actions
|
||||
// that can be taken on the buffer
|
||||
// The ActionHandler can access the window for necessary info about
|
||||
// visual positions for mouse clicks and scrolling
|
||||
type BufPane struct {
|
||||
display.BWindow
|
||||
|
||||
Buf *buffer.Buffer
|
||||
|
||||
Cursor *buffer.Cursor // the active cursor
|
||||
|
||||
// Since tcell doesn't differentiate between a mouse release event
|
||||
// and a mouse move event with no keys pressed, we need to keep
|
||||
// track of whether or not the mouse was pressed (or not released) last event to determine
|
||||
// mouse release events
|
||||
mouseReleased bool
|
||||
|
||||
// We need to keep track of insert key press toggle
|
||||
isOverwriteMode bool
|
||||
// This stores when the last click was
|
||||
// This is useful for detecting double and triple clicks
|
||||
lastClickTime time.Time
|
||||
lastLoc buffer.Loc
|
||||
|
||||
// lastCutTime stores when the last ctrl+k was issued.
|
||||
// It is used for clearing the clipboard to replace it with fresh cut lines.
|
||||
lastCutTime time.Time
|
||||
|
||||
// freshClip returns true if the clipboard has never been pasted.
|
||||
freshClip bool
|
||||
|
||||
// Was the last mouse event actually a double click?
|
||||
// Useful for detecting triple clicks -- if a double click is detected
|
||||
// but the last mouse event was actually a double click, it's a triple click
|
||||
doubleClick bool
|
||||
// Same here, just to keep track for mouse move events
|
||||
tripleClick bool
|
||||
|
||||
// Last search stores the last successful search for FindNext and FindPrev
|
||||
lastSearch string
|
||||
// Should the current multiple cursor selection search based on word or
|
||||
// based on selection (false for selection, true for word)
|
||||
multiWord bool
|
||||
|
||||
splitID uint64
|
||||
tab *Tab
|
||||
|
||||
// remember original location of a search in case the search is canceled
|
||||
searchOrig buffer.Loc
|
||||
}
|
||||
|
||||
func NewBufPane(buf *buffer.Buffer, win display.BWindow, tab *Tab) *BufPane {
|
||||
h := new(BufPane)
|
||||
h.Buf = buf
|
||||
h.BWindow = win
|
||||
h.tab = tab
|
||||
|
||||
h.Cursor = h.Buf.GetActiveCursor()
|
||||
h.mouseReleased = true
|
||||
|
||||
config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func NewBufPaneFromBuf(buf *buffer.Buffer, tab *Tab) *BufPane {
|
||||
w := display.NewBufWindow(0, 0, 0, 0, buf)
|
||||
return NewBufPane(buf, w, tab)
|
||||
}
|
||||
|
||||
func (h *BufPane) SetTab(t *Tab) {
|
||||
h.tab = t
|
||||
}
|
||||
|
||||
func (h *BufPane) Tab() *Tab {
|
||||
return h.tab
|
||||
}
|
||||
|
||||
func (h *BufPane) ResizePane(size int) {
|
||||
n := h.tab.GetNode(h.splitID)
|
||||
n.ResizeSplit(size)
|
||||
h.tab.Resize()
|
||||
}
|
||||
|
||||
// PluginCB calls all plugin callbacks with a certain name and
|
||||
// displays an error if there is one and returns the aggregrate
|
||||
// boolean response
|
||||
func (h *BufPane) PluginCB(cb string) bool {
|
||||
b, err := config.RunPluginFnBool(cb, luar.New(ulua.L, h))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// PluginCBRune is the same as PluginCB but also passes a rune to
|
||||
// the plugins
|
||||
func (h *BufPane) PluginCBRune(cb string, r rune) bool {
|
||||
b, err := config.RunPluginFnBool(cb, luar.New(ulua.L, h), luar.New(ulua.L, string(r)))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
|
||||
h.Buf.Close()
|
||||
h.Buf = b
|
||||
h.BWindow.SetBuffer(b)
|
||||
h.Cursor = b.GetActiveCursor()
|
||||
h.Resize(h.GetView().Width, h.GetView().Height)
|
||||
h.Relocate()
|
||||
// Set mouseReleased to true because we assume the mouse is not being pressed when
|
||||
// the editor is opened
|
||||
h.mouseReleased = true
|
||||
// Set isOverwriteMode to false, because we assume we are in the default mode when editor
|
||||
// is opened
|
||||
h.isOverwriteMode = false
|
||||
h.lastClickTime = time.Time{}
|
||||
}
|
||||
|
||||
func (h *BufPane) ID() uint64 {
|
||||
return h.splitID
|
||||
}
|
||||
|
||||
func (h *BufPane) SetID(i uint64) {
|
||||
h.splitID = i
|
||||
}
|
||||
|
||||
func (h *BufPane) Name() string {
|
||||
n := h.Buf.GetName()
|
||||
if h.Buf.Modified() {
|
||||
n += " +"
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// HandleEvent executes the tcell event properly
|
||||
func (h *BufPane) HandleEvent(event tcell.Event) {
|
||||
if h.Buf.ExternallyModified() && !h.Buf.ReloadDisabled {
|
||||
InfoBar.YNPrompt("The file on disk has changed. Reload file? (y,n,esc)", func(yes, canceled bool) {
|
||||
if canceled {
|
||||
h.Buf.DisableReload()
|
||||
}
|
||||
if !yes || canceled {
|
||||
h.Buf.UpdateModTime()
|
||||
} else {
|
||||
h.Buf.ReOpen()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventRaw:
|
||||
re := RawEvent{
|
||||
esc: e.EscSeq(),
|
||||
}
|
||||
h.DoKeyEvent(re)
|
||||
case *tcell.EventPaste:
|
||||
h.paste(e.Text())
|
||||
h.Relocate()
|
||||
case *tcell.EventKey:
|
||||
ke := KeyEvent{
|
||||
code: e.Key(),
|
||||
mod: e.Modifiers(),
|
||||
r: e.Rune(),
|
||||
}
|
||||
|
||||
done := h.DoKeyEvent(ke)
|
||||
if !done && e.Key() == tcell.KeyRune {
|
||||
h.DoRuneInsert(e.Rune())
|
||||
}
|
||||
case *tcell.EventMouse:
|
||||
cancel := false
|
||||
switch e.Buttons() {
|
||||
case tcell.Button1:
|
||||
_, my := e.Position()
|
||||
if h.Buf.Settings["statusline"].(bool) && my >= h.GetView().Y+h.GetView().Height-1 {
|
||||
cancel = true
|
||||
}
|
||||
case tcell.ButtonNone:
|
||||
// Mouse event with no click
|
||||
if !h.mouseReleased {
|
||||
// Mouse was just released
|
||||
|
||||
// mx, my := e.Position()
|
||||
// mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my})
|
||||
|
||||
// we could finish the selection based on the release location as described
|
||||
// below but when the mouse click is within the scroll margin this will
|
||||
// cause a scroll and selection even for a simple mouse click which is
|
||||
// not good
|
||||
// for terminals that don't support mouse motion events, selection via
|
||||
// the mouse won't work but this is ok
|
||||
|
||||
// Relocating here isn't really necessary because the cursor will
|
||||
// be in the right place from the last mouse event
|
||||
// However, if we are running in a terminal that doesn't support mouse motion
|
||||
// events, this still allows the user to make selections, except only after they
|
||||
// release the mouse
|
||||
|
||||
// if !h.doubleClick && !h.tripleClick {
|
||||
// h.Cursor.SetSelectionEnd(h.Cursor.Loc)
|
||||
// }
|
||||
if h.Cursor.HasSelection() {
|
||||
h.Cursor.CopySelection("primary")
|
||||
}
|
||||
h.mouseReleased = true
|
||||
}
|
||||
}
|
||||
|
||||
if !cancel {
|
||||
me := MouseEvent{
|
||||
btn: e.Buttons(),
|
||||
mod: e.Modifiers(),
|
||||
}
|
||||
h.DoMouseEvent(me, e)
|
||||
}
|
||||
}
|
||||
h.Buf.MergeCursors()
|
||||
|
||||
if h.IsActive() {
|
||||
// Display any gutter messages for this line
|
||||
c := h.Buf.GetActiveCursor()
|
||||
none := true
|
||||
for _, m := range h.Buf.Messages {
|
||||
if c.Y == m.Start.Y || c.Y == m.End.Y {
|
||||
InfoBar.GutterMessage(m.Msg)
|
||||
none = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if none && InfoBar.HasGutter {
|
||||
InfoBar.ClearGutter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DoKeyEvent executes a key event by finding the action it is bound
|
||||
// to and executing it (possibly multiple times for multiple cursors)
|
||||
func (h *BufPane) DoKeyEvent(e Event) bool {
|
||||
if action, ok := BufKeyBindings[e]; ok {
|
||||
return action(h)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *BufPane) execAction(action func(*BufPane) bool, name string, cursor int) bool {
|
||||
if name != "Autocomplete" && name != "CycleAutocompleteBack" {
|
||||
h.Buf.HasSuggestions = false
|
||||
}
|
||||
|
||||
_, isMulti := MultiActions[name]
|
||||
if (!isMulti && cursor == 0) || isMulti {
|
||||
if h.PluginCB("pre" + name) {
|
||||
success := action(h)
|
||||
success = success && h.PluginCB("on"+name)
|
||||
|
||||
if isMulti {
|
||||
if recording_macro {
|
||||
if name != "ToggleMacro" && name != "PlayMacro" {
|
||||
curmacro = append(curmacro, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *BufPane) completeAction(action string) {
|
||||
h.PluginCB("on" + action)
|
||||
}
|
||||
|
||||
func (h *BufPane) HasKeyEvent(e Event) bool {
|
||||
_, ok := BufKeyBindings[e]
|
||||
return ok
|
||||
}
|
||||
|
||||
// DoMouseEvent executes a mouse event by finding the action it is bound
|
||||
// to and executing it
|
||||
func (h *BufPane) DoMouseEvent(e MouseEvent, te *tcell.EventMouse) bool {
|
||||
if action, ok := BufMouseBindings[e]; ok {
|
||||
if action(h, te) {
|
||||
h.Relocate()
|
||||
}
|
||||
return true
|
||||
} else if h.HasKeyEvent(e) {
|
||||
return h.DoKeyEvent(e)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// DoRuneInsert inserts a given rune into the current buffer
|
||||
// (possibly multiple times for multiple cursors)
|
||||
func (h *BufPane) DoRuneInsert(r rune) {
|
||||
cursors := h.Buf.GetCursors()
|
||||
for _, c := range cursors {
|
||||
// Insert a character
|
||||
h.Buf.SetCurCursor(c.Num)
|
||||
h.Cursor = c
|
||||
if !h.PluginCBRune("preRune", r) {
|
||||
continue
|
||||
}
|
||||
if c.HasSelection() {
|
||||
c.DeleteSelection()
|
||||
c.ResetSelection()
|
||||
}
|
||||
|
||||
if h.isOverwriteMode {
|
||||
next := c.Loc
|
||||
next.X++
|
||||
h.Buf.Replace(c.Loc, next, string(r))
|
||||
} else {
|
||||
h.Buf.Insert(c.Loc, string(r))
|
||||
}
|
||||
if recording_macro {
|
||||
curmacro = append(curmacro, r)
|
||||
}
|
||||
h.Relocate()
|
||||
h.PluginCBRune("onRune", r)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BufPane) VSplitIndex(buf *buffer.Buffer, right bool) *BufPane {
|
||||
e := NewBufPaneFromBuf(buf, h.tab)
|
||||
e.splitID = MainTab().GetNode(h.splitID).VSplit(right)
|
||||
MainTab().Panes = append(MainTab().Panes, e)
|
||||
MainTab().Resize()
|
||||
MainTab().SetActive(len(MainTab().Panes) - 1)
|
||||
return e
|
||||
}
|
||||
func (h *BufPane) HSplitIndex(buf *buffer.Buffer, bottom bool) *BufPane {
|
||||
e := NewBufPaneFromBuf(buf, h.tab)
|
||||
e.splitID = MainTab().GetNode(h.splitID).HSplit(bottom)
|
||||
MainTab().Panes = append(MainTab().Panes, e)
|
||||
MainTab().Resize()
|
||||
MainTab().SetActive(len(MainTab().Panes) - 1)
|
||||
return e
|
||||
}
|
||||
|
||||
func (h *BufPane) VSplitBuf(buf *buffer.Buffer) *BufPane {
|
||||
return h.VSplitIndex(buf, h.Buf.Settings["splitright"].(bool))
|
||||
}
|
||||
func (h *BufPane) HSplitBuf(buf *buffer.Buffer) *BufPane {
|
||||
return h.HSplitIndex(buf, h.Buf.Settings["splitbottom"].(bool))
|
||||
}
|
||||
func (h *BufPane) Close() {
|
||||
h.Buf.Close()
|
||||
}
|
||||
|
||||
func (h *BufPane) SetActive(b bool) {
|
||||
h.BWindow.SetActive(b)
|
||||
if b {
|
||||
// Display any gutter messages for this line
|
||||
c := h.Buf.GetActiveCursor()
|
||||
none := true
|
||||
for _, m := range h.Buf.Messages {
|
||||
if c.Y == m.Start.Y || c.Y == m.End.Y {
|
||||
InfoBar.GutterMessage(m.Msg)
|
||||
none = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if none && InfoBar.HasGutter {
|
||||
InfoBar.ClearGutter()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// BufKeyActions contains the list of all possible key actions the bufhandler could execute
|
||||
var BufKeyActions = map[string]BufKeyAction{
|
||||
"CursorUp": (*BufPane).CursorUp,
|
||||
"CursorDown": (*BufPane).CursorDown,
|
||||
"CursorPageUp": (*BufPane).CursorPageUp,
|
||||
"CursorPageDown": (*BufPane).CursorPageDown,
|
||||
"CursorLeft": (*BufPane).CursorLeft,
|
||||
"CursorRight": (*BufPane).CursorRight,
|
||||
"CursorStart": (*BufPane).CursorStart,
|
||||
"CursorEnd": (*BufPane).CursorEnd,
|
||||
"SelectToStart": (*BufPane).SelectToStart,
|
||||
"SelectToEnd": (*BufPane).SelectToEnd,
|
||||
"SelectUp": (*BufPane).SelectUp,
|
||||
"SelectDown": (*BufPane).SelectDown,
|
||||
"SelectLeft": (*BufPane).SelectLeft,
|
||||
"SelectRight": (*BufPane).SelectRight,
|
||||
"WordRight": (*BufPane).WordRight,
|
||||
"WordLeft": (*BufPane).WordLeft,
|
||||
"SelectWordRight": (*BufPane).SelectWordRight,
|
||||
"SelectWordLeft": (*BufPane).SelectWordLeft,
|
||||
"DeleteWordRight": (*BufPane).DeleteWordRight,
|
||||
"DeleteWordLeft": (*BufPane).DeleteWordLeft,
|
||||
"SelectLine": (*BufPane).SelectLine,
|
||||
"SelectToStartOfLine": (*BufPane).SelectToStartOfLine,
|
||||
"SelectToStartOfText": (*BufPane).SelectToStartOfText,
|
||||
"SelectToStartOfTextToggle":(*BufPane).SelectToStartOfTextToggle,
|
||||
"SelectToEndOfLine": (*BufPane).SelectToEndOfLine,
|
||||
"ParagraphPrevious": (*BufPane).ParagraphPrevious,
|
||||
"ParagraphNext": (*BufPane).ParagraphNext,
|
||||
"InsertNewline": (*BufPane).InsertNewline,
|
||||
"Backspace": (*BufPane).Backspace,
|
||||
"Delete": (*BufPane).Delete,
|
||||
"InsertTab": (*BufPane).InsertTab,
|
||||
"Save": (*BufPane).Save,
|
||||
"SaveAll": (*BufPane).SaveAll,
|
||||
"SaveAs": (*BufPane).SaveAs,
|
||||
"Find": (*BufPane).Find,
|
||||
"FindNext": (*BufPane).FindNext,
|
||||
"FindPrevious": (*BufPane).FindPrevious,
|
||||
"Center": (*BufPane).Center,
|
||||
"Undo": (*BufPane).Undo,
|
||||
"Redo": (*BufPane).Redo,
|
||||
"Copy": (*BufPane).Copy,
|
||||
"Cut": (*BufPane).Cut,
|
||||
"CutLine": (*BufPane).CutLine,
|
||||
"DuplicateLine": (*BufPane).DuplicateLine,
|
||||
"DeleteLine": (*BufPane).DeleteLine,
|
||||
"MoveLinesUp": (*BufPane).MoveLinesUp,
|
||||
"MoveLinesDown": (*BufPane).MoveLinesDown,
|
||||
"IndentSelection": (*BufPane).IndentSelection,
|
||||
"OutdentSelection": (*BufPane).OutdentSelection,
|
||||
"Autocomplete": (*BufPane).Autocomplete,
|
||||
"CycleAutocompleteBack": (*BufPane).CycleAutocompleteBack,
|
||||
"OutdentLine": (*BufPane).OutdentLine,
|
||||
"IndentLine": (*BufPane).IndentLine,
|
||||
"Paste": (*BufPane).Paste,
|
||||
"PastePrimary": (*BufPane).PastePrimary,
|
||||
"SelectAll": (*BufPane).SelectAll,
|
||||
"OpenFile": (*BufPane).OpenFile,
|
||||
"Start": (*BufPane).Start,
|
||||
"End": (*BufPane).End,
|
||||
"PageUp": (*BufPane).PageUp,
|
||||
"PageDown": (*BufPane).PageDown,
|
||||
"SelectPageUp": (*BufPane).SelectPageUp,
|
||||
"SelectPageDown": (*BufPane).SelectPageDown,
|
||||
"HalfPageUp": (*BufPane).HalfPageUp,
|
||||
"HalfPageDown": (*BufPane).HalfPageDown,
|
||||
"StartOfText": (*BufPane).StartOfText,
|
||||
"StartOfTextToggle": (*BufPane).StartOfTextToggle,
|
||||
"StartOfLine": (*BufPane).StartOfLine,
|
||||
"EndOfLine": (*BufPane).EndOfLine,
|
||||
"ToggleHelp": (*BufPane).ToggleHelp,
|
||||
"ToggleKeyMenu": (*BufPane).ToggleKeyMenu,
|
||||
"ToggleDiffGutter": (*BufPane).ToggleDiffGutter,
|
||||
"ToggleRuler": (*BufPane).ToggleRuler,
|
||||
"ClearStatus": (*BufPane).ClearStatus,
|
||||
"ShellMode": (*BufPane).ShellMode,
|
||||
"CommandMode": (*BufPane).CommandMode,
|
||||
"ToggleOverwriteMode": (*BufPane).ToggleOverwriteMode,
|
||||
"Escape": (*BufPane).Escape,
|
||||
"Quit": (*BufPane).Quit,
|
||||
"QuitAll": (*BufPane).QuitAll,
|
||||
"AddTab": (*BufPane).AddTab,
|
||||
"PreviousTab": (*BufPane).PreviousTab,
|
||||
"NextTab": (*BufPane).NextTab,
|
||||
"NextSplit": (*BufPane).NextSplit,
|
||||
"PreviousSplit": (*BufPane).PreviousSplit,
|
||||
"Unsplit": (*BufPane).Unsplit,
|
||||
"VSplit": (*BufPane).VSplitAction,
|
||||
"HSplit": (*BufPane).HSplitAction,
|
||||
"ToggleMacro": (*BufPane).ToggleMacro,
|
||||
"PlayMacro": (*BufPane).PlayMacro,
|
||||
"Suspend": (*BufPane).Suspend,
|
||||
"ScrollUp": (*BufPane).ScrollUpAction,
|
||||
"ScrollDown": (*BufPane).ScrollDownAction,
|
||||
"SpawnMultiCursor": (*BufPane).SpawnMultiCursor,
|
||||
"SpawnMultiCursorUp": (*BufPane).SpawnMultiCursorUp,
|
||||
"SpawnMultiCursorDown": (*BufPane).SpawnMultiCursorDown,
|
||||
"SpawnMultiCursorSelect": (*BufPane).SpawnMultiCursorSelect,
|
||||
"RemoveMultiCursor": (*BufPane).RemoveMultiCursor,
|
||||
"RemoveAllMultiCursors": (*BufPane).RemoveAllMultiCursors,
|
||||
"SkipMultiCursor": (*BufPane).SkipMultiCursor,
|
||||
"JumpToMatchingBrace": (*BufPane).JumpToMatchingBrace,
|
||||
"JumpLine": (*BufPane).JumpLine,
|
||||
"None": (*BufPane).None,
|
||||
|
||||
// This was changed to InsertNewline but I don't want to break backwards compatibility
|
||||
"InsertEnter": (*BufPane).InsertNewline,
|
||||
}
|
||||
|
||||
// BufMouseActions contains the list of all possible mouse actions the bufhandler could execute
|
||||
var BufMouseActions = map[string]BufMouseAction{
|
||||
"MousePress": (*BufPane).MousePress,
|
||||
"MouseMultiCursor": (*BufPane).MouseMultiCursor,
|
||||
}
|
||||
|
||||
// MultiActions is a list of actions that should be executed multiple
|
||||
// times if there are multiple cursors (one per cursor)
|
||||
// Generally actions that modify global editor state like quitting or
|
||||
// saving should not be included in this list
|
||||
var MultiActions = map[string]bool{
|
||||
"CursorUp": true,
|
||||
"CursorDown": true,
|
||||
"CursorPageUp": true,
|
||||
"CursorPageDown": true,
|
||||
"CursorLeft": true,
|
||||
"CursorRight": true,
|
||||
"CursorStart": true,
|
||||
"CursorEnd": true,
|
||||
"SelectToStart": true,
|
||||
"SelectToEnd": true,
|
||||
"SelectUp": true,
|
||||
"SelectDown": true,
|
||||
"SelectLeft": true,
|
||||
"SelectRight": true,
|
||||
"WordRight": true,
|
||||
"WordLeft": true,
|
||||
"SelectWordRight": true,
|
||||
"SelectWordLeft": true,
|
||||
"DeleteWordRight": true,
|
||||
"DeleteWordLeft": true,
|
||||
"SelectLine": true,
|
||||
"SelectToStartOfLine": true,
|
||||
"SelectToStartOfText": true,
|
||||
"SelectToStartOfTextToggle": true,
|
||||
"SelectToEndOfLine": true,
|
||||
"ParagraphPrevious": true,
|
||||
"ParagraphNext": true,
|
||||
"InsertNewline": true,
|
||||
"Backspace": true,
|
||||
"Delete": true,
|
||||
"InsertTab": true,
|
||||
"FindNext": true,
|
||||
"FindPrevious": true,
|
||||
"Cut": true,
|
||||
"CutLine": true,
|
||||
"DuplicateLine": true,
|
||||
"DeleteLine": true,
|
||||
"MoveLinesUp": true,
|
||||
"MoveLinesDown": true,
|
||||
"IndentSelection": true,
|
||||
"OutdentSelection": true,
|
||||
"OutdentLine": true,
|
||||
"IndentLine": true,
|
||||
"Paste": true,
|
||||
"PastePrimary": true,
|
||||
"SelectPageUp": true,
|
||||
"SelectPageDown": true,
|
||||
"StartOfLine": true,
|
||||
"StartOfText": true,
|
||||
"StartOfTextToggle": true,
|
||||
"EndOfLine": true,
|
||||
"JumpToMatchingBrace": true,
|
||||
}
|
||||
907
internal/action/command.go
Normal file
907
internal/action/command.go
Normal file
@@ -0,0 +1,907 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/shell"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// A Command contains information about how to execute a command
|
||||
// It has the action for that command as well as a completer function
|
||||
type Command struct {
|
||||
action func(*BufPane, []string)
|
||||
completer buffer.Completer
|
||||
}
|
||||
|
||||
var commands map[string]Command
|
||||
|
||||
func InitCommands() {
|
||||
commands = map[string]Command{
|
||||
"set": {(*BufPane).SetCmd, OptionValueComplete},
|
||||
"reset": {(*BufPane).ResetCmd, OptionValueComplete},
|
||||
"setlocal": {(*BufPane).SetLocalCmd, OptionValueComplete},
|
||||
"show": {(*BufPane).ShowCmd, OptionComplete},
|
||||
"showkey": {(*BufPane).ShowKeyCmd, nil},
|
||||
"run": {(*BufPane).RunCmd, nil},
|
||||
"bind": {(*BufPane).BindCmd, nil},
|
||||
"unbind": {(*BufPane).UnbindCmd, nil},
|
||||
"quit": {(*BufPane).QuitCmd, nil},
|
||||
"goto": {(*BufPane).GotoCmd, nil},
|
||||
"save": {(*BufPane).SaveCmd, nil},
|
||||
"replace": {(*BufPane).ReplaceCmd, nil},
|
||||
"replaceall": {(*BufPane).ReplaceAllCmd, nil},
|
||||
"vsplit": {(*BufPane).VSplitCmd, buffer.FileComplete},
|
||||
"hsplit": {(*BufPane).HSplitCmd, buffer.FileComplete},
|
||||
"tab": {(*BufPane).NewTabCmd, buffer.FileComplete},
|
||||
"help": {(*BufPane).HelpCmd, HelpComplete},
|
||||
"eval": {(*BufPane).EvalCmd, nil},
|
||||
"log": {(*BufPane).ToggleLogCmd, nil},
|
||||
"plugin": {(*BufPane).PluginCmd, PluginComplete},
|
||||
"reload": {(*BufPane).ReloadCmd, nil},
|
||||
"reopen": {(*BufPane).ReopenCmd, nil},
|
||||
"cd": {(*BufPane).CdCmd, buffer.FileComplete},
|
||||
"pwd": {(*BufPane).PwdCmd, nil},
|
||||
"open": {(*BufPane).OpenCmd, buffer.FileComplete},
|
||||
"tabswitch": {(*BufPane).TabSwitchCmd, nil},
|
||||
"term": {(*BufPane).TermCmd, nil},
|
||||
"memusage": {(*BufPane).MemUsageCmd, nil},
|
||||
"retab": {(*BufPane).RetabCmd, nil},
|
||||
"raw": {(*BufPane).RawCmd, nil},
|
||||
"textfilter": {(*BufPane).TextFilterCmd, nil},
|
||||
}
|
||||
}
|
||||
|
||||
// MakeCommand is a function to easily create new commands
|
||||
// This can be called by plugins in Lua so that plugins can define their own commands
|
||||
func MakeCommand(name string, action func(bp *BufPane, args []string), completer buffer.Completer) {
|
||||
if action != nil {
|
||||
commands[name] = Command{action, completer}
|
||||
}
|
||||
}
|
||||
|
||||
// CommandEditAction returns a bindable function that opens a prompt with
|
||||
// the given string and executes the command when the user presses
|
||||
// enter
|
||||
func CommandEditAction(prompt string) BufKeyAction {
|
||||
return func(h *BufPane) bool {
|
||||
InfoBar.Prompt("> ", prompt, "Command", nil, func(resp string, canceled bool) {
|
||||
if !canceled {
|
||||
MainTab().CurPane().HandleCommand(resp)
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// CommandAction returns a bindable function which executes the
|
||||
// given command
|
||||
func CommandAction(cmd string) BufKeyAction {
|
||||
return func(h *BufPane) bool {
|
||||
MainTab().CurPane().HandleCommand(cmd)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var PluginCmds = []string{"install", "remove", "update", "available", "list", "search"}
|
||||
|
||||
// PluginCmd installs, removes, updates, lists, or searches for given plugins
|
||||
func (h *BufPane) PluginCmd(args []string) {
|
||||
if len(args) < 1 {
|
||||
InfoBar.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
if h.Buf.Type != buffer.BTLog {
|
||||
h.OpenLogBuf()
|
||||
}
|
||||
|
||||
config.PluginCommand(buffer.LogBuf, args[0], args[1:])
|
||||
}
|
||||
|
||||
// RetabCmd changes all spaces to tabs or all tabs to spaces
|
||||
// depending on the user's settings
|
||||
func (h *BufPane) RetabCmd(args []string) {
|
||||
h.Buf.Retab()
|
||||
}
|
||||
|
||||
// RawCmd opens a new raw view which displays the escape sequences micro
|
||||
// is receiving in real-time
|
||||
func (h *BufPane) RawCmd(args []string) {
|
||||
width, height := screen.Screen.Size()
|
||||
iOffset := config.GetInfoBarOffset()
|
||||
tp := NewTabFromPane(0, 0, width, height-iOffset, NewRawPane(nil))
|
||||
Tabs.AddTab(tp)
|
||||
Tabs.SetActive(len(Tabs.List) - 1)
|
||||
}
|
||||
|
||||
// TextFilterCmd filters the selection through the command.
|
||||
// Selection goes to the command input.
|
||||
// On successful run command output replaces the current selection.
|
||||
func (h *BufPane) TextFilterCmd(args []string) {
|
||||
if len(args) == 0 {
|
||||
InfoBar.Error("usage: textfilter arguments")
|
||||
return
|
||||
}
|
||||
sel := h.Cursor.GetSelection()
|
||||
if len(sel) == 0 {
|
||||
h.Cursor.SelectWord()
|
||||
sel = h.Cursor.GetSelection()
|
||||
}
|
||||
var bout, berr bytes.Buffer
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdin = strings.NewReader(string(sel))
|
||||
cmd.Stderr = &berr
|
||||
cmd.Stdout = &bout
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
InfoBar.Error(err.Error() + " " + berr.String())
|
||||
return
|
||||
}
|
||||
h.Cursor.DeleteSelection()
|
||||
h.Buf.Insert(h.Cursor.Loc, bout.String())
|
||||
}
|
||||
|
||||
// TabSwitchCmd switches to a given tab either by name or by number
|
||||
func (h *BufPane) TabSwitchCmd(args []string) {
|
||||
if len(args) > 0 {
|
||||
num, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
// Check for tab with this name
|
||||
|
||||
found := false
|
||||
for i, t := range Tabs.List {
|
||||
if t.Panes[t.active].Name() == args[0] {
|
||||
Tabs.SetActive(i)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
InfoBar.Error("Could not find tab: ", err)
|
||||
}
|
||||
} else {
|
||||
num--
|
||||
if num >= 0 && num < len(Tabs.List) {
|
||||
Tabs.SetActive(num)
|
||||
} else {
|
||||
InfoBar.Error("Invalid tab index")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CdCmd changes the current working directory
|
||||
func (h *BufPane) CdCmd(args []string) {
|
||||
if len(args) > 0 {
|
||||
path, err := util.ReplaceHome(args[0])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
err = os.Chdir(path)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
wd, _ := os.Getwd()
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
if len(b.Path) > 0 {
|
||||
b.Path, _ = util.MakeRelative(b.AbsPath, wd)
|
||||
if p, _ := filepath.Abs(b.Path); !strings.Contains(p, wd) {
|
||||
b.Path = b.AbsPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MemUsageCmd prints micro's memory usage
|
||||
// Alloc shows how many bytes are currently in use
|
||||
// Sys shows how many bytes have been requested from the operating system
|
||||
// NumGC shows how many times the GC has been run
|
||||
// Note that Go commonly reserves more memory from the OS than is currently in-use/required
|
||||
// Additionally, even if Go returns memory to the OS, the OS does not always claim it because
|
||||
// there may be plenty of memory to spare
|
||||
func (h *BufPane) MemUsageCmd(args []string) {
|
||||
InfoBar.Message(util.GetMemStats())
|
||||
}
|
||||
|
||||
// PwdCmd prints the current working directory
|
||||
func (h *BufPane) PwdCmd(args []string) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
InfoBar.Message(err.Error())
|
||||
} else {
|
||||
InfoBar.Message(wd)
|
||||
}
|
||||
}
|
||||
|
||||
// OpenCmd opens a new buffer with a given filename
|
||||
func (h *BufPane) OpenCmd(args []string) {
|
||||
if len(args) > 0 {
|
||||
filename := args[0]
|
||||
// the filename might or might not be quoted, so unquote first then join the strings.
|
||||
args, err := shellquote.Split(filename)
|
||||
if err != nil {
|
||||
InfoBar.Error("Error parsing args ", err)
|
||||
return
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return
|
||||
}
|
||||
filename = strings.Join(args, " ")
|
||||
|
||||
open := func() {
|
||||
b, err := buffer.NewBufferFromFile(filename, buffer.BTDefault)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
h.OpenBuffer(b)
|
||||
}
|
||||
if h.Buf.Modified() {
|
||||
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
|
||||
if !canceled && !yes {
|
||||
open()
|
||||
} else if !canceled && yes {
|
||||
h.Save()
|
||||
open()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
open()
|
||||
}
|
||||
} else {
|
||||
InfoBar.Error("No filename")
|
||||
}
|
||||
}
|
||||
|
||||
// ToggleLogCmd toggles the log view
|
||||
func (h *BufPane) ToggleLogCmd(args []string) {
|
||||
if h.Buf.Type != buffer.BTLog {
|
||||
h.OpenLogBuf()
|
||||
} else {
|
||||
h.Quit()
|
||||
}
|
||||
}
|
||||
|
||||
// ReloadCmd reloads all files (syntax files, colorschemes...)
|
||||
func (h *BufPane) ReloadCmd(args []string) {
|
||||
ReloadConfig()
|
||||
}
|
||||
|
||||
func ReloadConfig() {
|
||||
config.InitRuntimeFiles()
|
||||
err := config.ReadSettings()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
config.InitGlobalSettings()
|
||||
InitBindings()
|
||||
InitCommands()
|
||||
|
||||
err = config.InitColorscheme()
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.UpdateRules()
|
||||
}
|
||||
}
|
||||
|
||||
// ReopenCmd reopens the buffer (reload from disk)
|
||||
func (h *BufPane) ReopenCmd(args []string) {
|
||||
if h.Buf.Modified() {
|
||||
InfoBar.YNPrompt("Save file before reopen?", func(yes, canceled bool) {
|
||||
if !canceled && yes {
|
||||
h.Save()
|
||||
h.Buf.ReOpen()
|
||||
} else if !canceled {
|
||||
h.Buf.ReOpen()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
h.Buf.ReOpen()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BufPane) openHelp(page string) error {
|
||||
if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil {
|
||||
return errors.New(fmt.Sprint("Unable to load help text", page, "\n", err))
|
||||
} else {
|
||||
helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
|
||||
helpBuffer.SetName("Help " + page)
|
||||
|
||||
if h.Buf.Type == buffer.BTHelp {
|
||||
h.OpenBuffer(helpBuffer)
|
||||
} else {
|
||||
h.HSplitBuf(helpBuffer)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HelpCmd tries to open the given help page in a horizontal split
|
||||
func (h *BufPane) HelpCmd(args []string) {
|
||||
if len(args) < 1 {
|
||||
// Open the default help if the user just typed "> help"
|
||||
h.openHelp("help")
|
||||
} else {
|
||||
if config.FindRuntimeFile(config.RTHelp, args[0]) != nil {
|
||||
err := h.openHelp(args[0])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
}
|
||||
} else {
|
||||
InfoBar.Error("Sorry, no help for ", args[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VSplitCmd opens a vertical split with file given in the first argument
|
||||
// If no file is given, it opens an empty buffer in a new split
|
||||
func (h *BufPane) VSplitCmd(args []string) {
|
||||
if len(args) == 0 {
|
||||
// Open an empty vertical split
|
||||
h.VSplitAction()
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
h.VSplitBuf(buf)
|
||||
}
|
||||
|
||||
// HSplitCmd opens a horizontal split with file given in the first argument
|
||||
// If no file is given, it opens an empty buffer in a new split
|
||||
func (h *BufPane) HSplitCmd(args []string) {
|
||||
if len(args) == 0 {
|
||||
// Open an empty horizontal split
|
||||
h.HSplitAction()
|
||||
return
|
||||
}
|
||||
|
||||
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
h.HSplitBuf(buf)
|
||||
}
|
||||
|
||||
// EvalCmd evaluates a lua expression
|
||||
func (h *BufPane) EvalCmd(args []string) {
|
||||
InfoBar.Error("Eval unsupported")
|
||||
}
|
||||
|
||||
// NewTabCmd opens the given file in a new tab
|
||||
func (h *BufPane) NewTabCmd(args []string) {
|
||||
width, height := screen.Screen.Size()
|
||||
iOffset := config.GetInfoBarOffset()
|
||||
if len(args) > 0 {
|
||||
for _, a := range args {
|
||||
b, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
tp := NewTabFromBuffer(0, 0, width, height-1-iOffset, b)
|
||||
Tabs.AddTab(tp)
|
||||
Tabs.SetActive(len(Tabs.List) - 1)
|
||||
}
|
||||
} else {
|
||||
b := buffer.NewBufferFromString("", "", buffer.BTDefault)
|
||||
tp := NewTabFromBuffer(0, 0, width, height-iOffset, b)
|
||||
Tabs.AddTab(tp)
|
||||
Tabs.SetActive(len(Tabs.List) - 1)
|
||||
}
|
||||
}
|
||||
|
||||
func SetGlobalOptionNative(option string, nativeValue interface{}) error {
|
||||
local := false
|
||||
for _, s := range config.LocalSettings {
|
||||
if s == option {
|
||||
local = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !local {
|
||||
config.GlobalSettings[option] = nativeValue
|
||||
|
||||
if option == "colorscheme" {
|
||||
// LoadSyntaxFiles()
|
||||
config.InitColorscheme()
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.UpdateRules()
|
||||
}
|
||||
} else if option == "infobar" || option == "keymenu" {
|
||||
Tabs.Resize()
|
||||
} else if option == "mouse" {
|
||||
if !nativeValue.(bool) {
|
||||
screen.Screen.DisableMouse()
|
||||
} else {
|
||||
screen.Screen.EnableMouse()
|
||||
}
|
||||
} else if option == "autosave" {
|
||||
if nativeValue.(float64) > 0 {
|
||||
config.SetAutoTime(int(nativeValue.(float64)))
|
||||
config.StartAutoSave()
|
||||
} else {
|
||||
config.SetAutoTime(0)
|
||||
}
|
||||
} else if option == "paste" {
|
||||
screen.Screen.SetPaste(nativeValue.(bool))
|
||||
} else {
|
||||
for _, pl := range config.Plugins {
|
||||
if option == pl.Name {
|
||||
if nativeValue.(bool) && !pl.Loaded {
|
||||
pl.Load()
|
||||
_, err := pl.Call("init")
|
||||
if err != nil && err != config.ErrNoSuchFunction {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
} else if !nativeValue.(bool) && pl.Loaded {
|
||||
_, err := pl.Call("deinit")
|
||||
if err != nil && err != config.ErrNoSuchFunction {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, b := range buffer.OpenBuffers {
|
||||
b.SetOptionNative(option, nativeValue)
|
||||
}
|
||||
|
||||
return config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
|
||||
}
|
||||
|
||||
func SetGlobalOption(option, value string) error {
|
||||
if _, ok := config.GlobalSettings[option]; !ok {
|
||||
return config.ErrInvalidOption
|
||||
}
|
||||
|
||||
nativeValue, err := config.GetNativeValue(option, config.GlobalSettings[option], value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return SetGlobalOptionNative(option, nativeValue)
|
||||
}
|
||||
|
||||
// ResetCmd resets a setting to its default value
|
||||
func (h *BufPane) ResetCmd(args []string) {
|
||||
if len(args) < 1 {
|
||||
InfoBar.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
option := args[0]
|
||||
|
||||
defaultGlobals := config.DefaultGlobalSettings()
|
||||
defaultLocals := config.DefaultCommonSettings()
|
||||
|
||||
if _, ok := defaultGlobals[option]; ok {
|
||||
SetGlobalOptionNative(option, defaultGlobals[option])
|
||||
return
|
||||
}
|
||||
if _, ok := defaultLocals[option]; ok {
|
||||
h.Buf.SetOptionNative(option, defaultLocals[option])
|
||||
return
|
||||
}
|
||||
InfoBar.Error(config.ErrInvalidOption)
|
||||
}
|
||||
|
||||
// SetCmd sets an option
|
||||
func (h *BufPane) SetCmd(args []string) {
|
||||
if len(args) < 2 {
|
||||
InfoBar.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
option := args[0]
|
||||
value := args[1]
|
||||
|
||||
err := SetGlobalOption(option, value)
|
||||
if err == config.ErrInvalidOption {
|
||||
err := h.Buf.SetOption(option, value)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
}
|
||||
} else if err != nil {
|
||||
InfoBar.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// SetLocalCmd sets an option local to the buffer
|
||||
func (h *BufPane) SetLocalCmd(args []string) {
|
||||
if len(args) < 2 {
|
||||
InfoBar.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
option := args[0]
|
||||
value := args[1]
|
||||
|
||||
err := h.Buf.SetOption(option, value)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ShowCmd shows the value of the given option
|
||||
func (h *BufPane) ShowCmd(args []string) {
|
||||
if len(args) < 1 {
|
||||
InfoBar.Error("Please provide an option to show")
|
||||
return
|
||||
}
|
||||
|
||||
var option interface{}
|
||||
if opt, ok := h.Buf.Settings[args[0]]; ok {
|
||||
option = opt
|
||||
} else if opt, ok := config.GlobalSettings[args[0]]; ok {
|
||||
option = opt
|
||||
}
|
||||
|
||||
if option == nil {
|
||||
InfoBar.Error(args[0], " is not a valid option")
|
||||
return
|
||||
}
|
||||
|
||||
InfoBar.Message(option)
|
||||
}
|
||||
|
||||
// ShowKeyCmd displays the action that a key is bound to
|
||||
func (h *BufPane) ShowKeyCmd(args []string) {
|
||||
if len(args) < 1 {
|
||||
InfoBar.Error("Please provide a key to show")
|
||||
return
|
||||
}
|
||||
|
||||
if action, ok := config.Bindings[args[0]]; ok {
|
||||
InfoBar.Message(action)
|
||||
} else {
|
||||
InfoBar.Message(args[0], " has no binding")
|
||||
}
|
||||
}
|
||||
|
||||
// BindCmd creates a new keybinding
|
||||
func (h *BufPane) BindCmd(args []string) {
|
||||
if len(args) < 2 {
|
||||
InfoBar.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
_, err := TryBindKey(args[0], args[1], true)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// UnbindCmd binds a key to its default action
|
||||
func (h *BufPane) UnbindCmd(args []string) {
|
||||
if len(args) < 1 {
|
||||
InfoBar.Error("Not enough arguments")
|
||||
return
|
||||
}
|
||||
|
||||
err := UnbindKey(args[0])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// RunCmd runs a shell command in the background
|
||||
func (h *BufPane) RunCmd(args []string) {
|
||||
runf, err := shell.RunBackgroundShell(shellquote.Join(args...))
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
} else {
|
||||
go func() {
|
||||
InfoBar.Message(runf())
|
||||
screen.Redraw()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// QuitCmd closes the main view
|
||||
func (h *BufPane) QuitCmd(args []string) {
|
||||
h.Quit()
|
||||
}
|
||||
|
||||
// GotoCmd is a command that will send the cursor to a certain
|
||||
// position in the buffer
|
||||
// For example: `goto line`, or `goto line:col`
|
||||
func (h *BufPane) GotoCmd(args []string) {
|
||||
if len(args) <= 0 {
|
||||
InfoBar.Error("Not enough arguments")
|
||||
} else {
|
||||
h.RemoveAllMultiCursors()
|
||||
if strings.Contains(args[0], ":") {
|
||||
parts := strings.SplitN(args[0], ":", 2)
|
||||
line, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
col, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
|
||||
col = util.Clamp(col-1, 0, utf8.RuneCount(h.Buf.LineBytes(line)))
|
||||
h.Cursor.GotoLoc(buffer.Loc{col, line})
|
||||
} else {
|
||||
line, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
|
||||
h.Cursor.GotoLoc(buffer.Loc{0, line})
|
||||
}
|
||||
h.Relocate()
|
||||
}
|
||||
}
|
||||
|
||||
// SaveCmd saves the buffer optionally with an argument file name
|
||||
func (h *BufPane) SaveCmd(args []string) {
|
||||
if len(args) == 0 {
|
||||
h.Save()
|
||||
} else {
|
||||
h.Buf.SaveAs(args[0])
|
||||
}
|
||||
}
|
||||
|
||||
// ReplaceCmd runs search and replace
|
||||
func (h *BufPane) ReplaceCmd(args []string) {
|
||||
if len(args) < 2 || len(args) > 4 {
|
||||
// We need to find both a search and replace expression
|
||||
InfoBar.Error("Invalid replace statement: " + strings.Join(args, " "))
|
||||
return
|
||||
}
|
||||
|
||||
all := false
|
||||
noRegex := false
|
||||
|
||||
foundSearch := false
|
||||
foundReplace := false
|
||||
var search string
|
||||
var replaceStr string
|
||||
for _, arg := range args {
|
||||
switch arg {
|
||||
case "-a":
|
||||
all = true
|
||||
case "-l":
|
||||
noRegex = true
|
||||
default:
|
||||
if !foundSearch {
|
||||
foundSearch = true
|
||||
search = arg
|
||||
} else if !foundReplace {
|
||||
foundReplace = true
|
||||
replaceStr = arg
|
||||
} else {
|
||||
InfoBar.Error("Invalid flag: " + arg)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if noRegex {
|
||||
search = regexp.QuoteMeta(search)
|
||||
}
|
||||
|
||||
replace := []byte(replaceStr)
|
||||
|
||||
var regex *regexp.Regexp
|
||||
var err error
|
||||
if h.Buf.Settings["ignorecase"].(bool) {
|
||||
regex, err = regexp.Compile("(?im)" + search)
|
||||
} else {
|
||||
regex, err = regexp.Compile("(?m)" + search)
|
||||
}
|
||||
if err != nil {
|
||||
// There was an error with the user's regex
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
nreplaced := 0
|
||||
start := h.Buf.Start()
|
||||
end := h.Buf.End()
|
||||
selection := h.Cursor.HasSelection()
|
||||
if selection {
|
||||
start = h.Cursor.CurSelection[0]
|
||||
end = h.Cursor.CurSelection[1]
|
||||
}
|
||||
if all {
|
||||
nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace)
|
||||
} else {
|
||||
inRange := func(l buffer.Loc) bool {
|
||||
return l.GreaterEqual(start) && l.LessEqual(end)
|
||||
}
|
||||
|
||||
searchLoc := start
|
||||
var doReplacement func()
|
||||
doReplacement = func() {
|
||||
locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, !noRegex)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
if !found || !inRange(locs[0]) || !inRange(locs[1]) {
|
||||
h.Cursor.ResetSelection()
|
||||
h.Buf.RelocateCursors()
|
||||
return
|
||||
}
|
||||
|
||||
h.Cursor.SetSelectionStart(locs[0])
|
||||
h.Cursor.SetSelectionEnd(locs[1])
|
||||
|
||||
h.Relocate()
|
||||
|
||||
InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
|
||||
if !canceled && yes {
|
||||
_, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace)
|
||||
|
||||
searchLoc = locs[0]
|
||||
searchLoc.X += nrunes + locs[0].Diff(locs[1], h.Buf)
|
||||
end.Move(nrunes, h.Buf)
|
||||
h.Cursor.Loc = searchLoc
|
||||
nreplaced++
|
||||
} else if !canceled && !yes {
|
||||
searchLoc = locs[0]
|
||||
searchLoc.X += utf8.RuneCount(replace)
|
||||
} else if canceled {
|
||||
h.Cursor.ResetSelection()
|
||||
h.Buf.RelocateCursors()
|
||||
return
|
||||
}
|
||||
doReplacement()
|
||||
})
|
||||
}
|
||||
doReplacement()
|
||||
}
|
||||
|
||||
h.Buf.RelocateCursors()
|
||||
h.Relocate()
|
||||
|
||||
var s string
|
||||
if nreplaced > 1 {
|
||||
s = fmt.Sprintf("Replaced %d occurrences of %s", nreplaced, search)
|
||||
} else if nreplaced == 1 {
|
||||
s = fmt.Sprintf("Replaced 1 occurrence of %s", search)
|
||||
} else {
|
||||
s = fmt.Sprintf("Nothing matched %s", search)
|
||||
}
|
||||
|
||||
if selection {
|
||||
s += " in selection"
|
||||
}
|
||||
|
||||
InfoBar.Message(s)
|
||||
}
|
||||
|
||||
// ReplaceAllCmd replaces search term all at once
|
||||
func (h *BufPane) ReplaceAllCmd(args []string) {
|
||||
// aliased to Replace command
|
||||
h.ReplaceCmd(append(args, "-a"))
|
||||
}
|
||||
|
||||
// TermCmd opens a terminal in the current view
|
||||
func (h *BufPane) TermCmd(args []string) {
|
||||
ps := h.tab.Panes
|
||||
|
||||
if !TermEmuSupported {
|
||||
InfoBar.Error("Terminal emulator not supported on this system")
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
sh := os.Getenv("SHELL")
|
||||
if sh == "" {
|
||||
InfoBar.Error("Shell environment not found")
|
||||
return
|
||||
}
|
||||
args = []string{sh}
|
||||
}
|
||||
|
||||
term := func(i int, newtab bool) {
|
||||
t := new(shell.Terminal)
|
||||
err := t.Start(args, false, true, nil, nil)
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
id := h.ID()
|
||||
if newtab {
|
||||
h.AddTab()
|
||||
i = 0
|
||||
id = MainTab().Panes[0].ID()
|
||||
} else {
|
||||
MainTab().Panes[i].Close()
|
||||
}
|
||||
|
||||
v := h.GetView()
|
||||
tp, err := NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
|
||||
if err != nil {
|
||||
InfoBar.Error(err)
|
||||
return
|
||||
}
|
||||
MainTab().Panes[i] = tp
|
||||
MainTab().SetActive(i)
|
||||
}
|
||||
|
||||
// If there is only one open file we make a new tab instead of overwriting it
|
||||
newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1
|
||||
|
||||
if newtab {
|
||||
term(0, true)
|
||||
return
|
||||
}
|
||||
|
||||
for i, p := range ps {
|
||||
if p.ID() == h.ID() {
|
||||
if h.Buf.Modified() {
|
||||
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
|
||||
if !canceled && !yes {
|
||||
term(i, false)
|
||||
} else if !canceled && yes {
|
||||
h.Save()
|
||||
term(i, false)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
term(i, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleCommand handles input from the user
|
||||
func (h *BufPane) HandleCommand(input string) {
|
||||
args, err := shellquote.Split(input)
|
||||
if err != nil {
|
||||
InfoBar.Error("Error parsing args ", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
inputCmd := args[0]
|
||||
|
||||
if _, ok := commands[inputCmd]; !ok {
|
||||
InfoBar.Error("Unknown command ", inputCmd)
|
||||
} else {
|
||||
WriteLog("> " + input + "\n")
|
||||
commands[inputCmd].action(h, args[1:])
|
||||
WriteLog("\n")
|
||||
}
|
||||
}
|
||||
107
internal/action/defaults_darwin.go
Normal file
107
internal/action/defaults_darwin.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package action
|
||||
|
||||
// DefaultBindings returns a map containing micro's default keybindings
|
||||
func DefaultBindings() map[string]string {
|
||||
return map[string]string{
|
||||
"Up": "CursorUp",
|
||||
"Down": "CursorDown",
|
||||
"Right": "CursorRight",
|
||||
"Left": "CursorLeft",
|
||||
"ShiftUp": "SelectUp",
|
||||
"ShiftDown": "SelectDown",
|
||||
"ShiftLeft": "SelectLeft",
|
||||
"ShiftRight": "SelectRight",
|
||||
"AltLeft": "WordLeft",
|
||||
"AltRight": "WordRight",
|
||||
"AltUp": "MoveLinesUp",
|
||||
"AltDown": "MoveLinesDown",
|
||||
"AltShiftRight": "SelectWordRight",
|
||||
"AltShiftLeft": "SelectWordLeft",
|
||||
"CtrlLeft": "StartOfTextToggle",
|
||||
"CtrlRight": "EndOfLine",
|
||||
"CtrlShiftLeft": "SelectToStartOfTextToggle",
|
||||
"ShiftHome": "SelectToStartOfTextToggle",
|
||||
"CtrlShiftRight": "SelectToEndOfLine",
|
||||
"ShiftEnd": "SelectToEndOfLine",
|
||||
"CtrlUp": "CursorStart",
|
||||
"CtrlDown": "CursorEnd",
|
||||
"CtrlShiftUp": "SelectToStart",
|
||||
"CtrlShiftDown": "SelectToEnd",
|
||||
"Alt-{": "ParagraphPrevious",
|
||||
"Alt-}": "ParagraphNext",
|
||||
"Enter": "InsertNewline",
|
||||
"CtrlH": "Backspace",
|
||||
"Backspace": "Backspace",
|
||||
"Alt-CtrlH": "DeleteWordLeft",
|
||||
"Alt-Backspace": "DeleteWordLeft",
|
||||
"Tab": "Autocomplete|IndentSelection|InsertTab",
|
||||
"Backtab": "CycleAutocompleteBack|OutdentSelection|OutdentLine",
|
||||
"CtrlO": "OpenFile",
|
||||
"CtrlS": "Save",
|
||||
"CtrlF": "Find",
|
||||
"CtrlN": "FindNext",
|
||||
"CtrlP": "FindPrevious",
|
||||
"CtrlZ": "Undo",
|
||||
"CtrlY": "Redo",
|
||||
"CtrlC": "Copy",
|
||||
"CtrlX": "Cut",
|
||||
"CtrlK": "CutLine",
|
||||
"CtrlD": "DuplicateLine",
|
||||
"CtrlV": "Paste",
|
||||
"CtrlA": "SelectAll",
|
||||
"CtrlT": "AddTab",
|
||||
"Alt,": "PreviousTab",
|
||||
"Alt.": "NextTab",
|
||||
"Home": "StartOfTextToggle",
|
||||
"End": "EndOfLine",
|
||||
"CtrlHome": "CursorStart",
|
||||
"CtrlEnd": "CursorEnd",
|
||||
"PageUp": "CursorPageUp",
|
||||
"PageDown": "CursorPageDown",
|
||||
"CtrlPageUp": "PreviousTab",
|
||||
"CtrlPageDown": "NextTab",
|
||||
"CtrlG": "ToggleHelp",
|
||||
"Alt-g": "ToggleKeyMenu",
|
||||
"CtrlR": "ToggleRuler",
|
||||
"CtrlL": "command-edit:goto ",
|
||||
"Delete": "Delete",
|
||||
"CtrlB": "ShellMode",
|
||||
"CtrlQ": "Quit",
|
||||
"CtrlE": "CommandMode",
|
||||
"CtrlW": "NextSplit",
|
||||
"CtrlU": "ToggleMacro",
|
||||
"CtrlJ": "PlayMacro",
|
||||
"Insert": "ToggleOverwriteMode",
|
||||
|
||||
// Emacs-style keybindings
|
||||
"Alt-f": "WordRight",
|
||||
"Alt-b": "WordLeft",
|
||||
"Alt-a": "StartOfText",
|
||||
"Alt-e": "EndOfLine",
|
||||
// "Alt-p": "CursorUp",
|
||||
// "Alt-n": "CursorDown",
|
||||
|
||||
// Integration with file managers
|
||||
"F2": "Save",
|
||||
"F3": "Find",
|
||||
"F4": "Quit",
|
||||
"F7": "Find",
|
||||
"F10": "Quit",
|
||||
"Esc": "Escape",
|
||||
|
||||
// Mouse bindings
|
||||
"MouseWheelUp": "ScrollUp",
|
||||
"MouseWheelDown": "ScrollDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
"Ctrl-MouseLeft": "MouseMultiCursor",
|
||||
|
||||
"Alt-n": "SpawnMultiCursor",
|
||||
"AltShiftUp": "SpawnMultiCursorUp",
|
||||
"AltShiftDown": "SpawnMultiCursorDown",
|
||||
"Alt-m": "SpawnMultiCursorSelect",
|
||||
"Alt-p": "RemoveMultiCursor",
|
||||
"Alt-c": "RemoveAllMultiCursors",
|
||||
"Alt-x": "SkipMultiCursor",
|
||||
}
|
||||
}
|
||||
109
internal/action/defaults_other.go
Normal file
109
internal/action/defaults_other.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// +build !darwin
|
||||
|
||||
package action
|
||||
|
||||
// DefaultBindings returns a map containing micro's default keybindings
|
||||
func DefaultBindings() map[string]string {
|
||||
return map[string]string{
|
||||
"Up": "CursorUp",
|
||||
"Down": "CursorDown",
|
||||
"Right": "CursorRight",
|
||||
"Left": "CursorLeft",
|
||||
"ShiftUp": "SelectUp",
|
||||
"ShiftDown": "SelectDown",
|
||||
"ShiftLeft": "SelectLeft",
|
||||
"ShiftRight": "SelectRight",
|
||||
"CtrlLeft": "WordLeft",
|
||||
"CtrlRight": "WordRight",
|
||||
"AltUp": "MoveLinesUp",
|
||||
"AltDown": "MoveLinesDown",
|
||||
"CtrlShiftRight": "SelectWordRight",
|
||||
"CtrlShiftLeft": "SelectWordLeft",
|
||||
"AltLeft": "StartOfTextToggle",
|
||||
"AltRight": "EndOfLine",
|
||||
"AltShiftLeft": "SelectToStartOfTextToggle",
|
||||
"ShiftHome": "SelectToStartOfTextToggle",
|
||||
"AltShiftRight": "SelectToEndOfLine",
|
||||
"ShiftEnd": "SelectToEndOfLine",
|
||||
"CtrlUp": "CursorStart",
|
||||
"CtrlDown": "CursorEnd",
|
||||
"CtrlShiftUp": "SelectToStart",
|
||||
"CtrlShiftDown": "SelectToEnd",
|
||||
"Alt-{": "ParagraphPrevious",
|
||||
"Alt-}": "ParagraphNext",
|
||||
"Enter": "InsertNewline",
|
||||
"CtrlH": "Backspace",
|
||||
"Backspace": "Backspace",
|
||||
"Alt-CtrlH": "DeleteWordLeft",
|
||||
"Alt-Backspace": "DeleteWordLeft",
|
||||
"Tab": "Autocomplete|IndentSelection|InsertTab",
|
||||
"Backtab": "CycleAutocompleteBack|OutdentSelection|OutdentLine",
|
||||
"CtrlO": "OpenFile",
|
||||
"CtrlS": "Save",
|
||||
"CtrlF": "Find",
|
||||
"CtrlN": "FindNext",
|
||||
"CtrlP": "FindPrevious",
|
||||
"CtrlZ": "Undo",
|
||||
"CtrlY": "Redo",
|
||||
"CtrlC": "Copy",
|
||||
"CtrlX": "Cut",
|
||||
"CtrlK": "CutLine",
|
||||
"CtrlD": "DuplicateLine",
|
||||
"CtrlV": "Paste",
|
||||
"CtrlA": "SelectAll",
|
||||
"CtrlT": "AddTab",
|
||||
"Alt,": "PreviousTab",
|
||||
"Alt.": "NextTab",
|
||||
"Home": "StartOfTextToggle",
|
||||
"End": "EndOfLine",
|
||||
"CtrlHome": "CursorStart",
|
||||
"CtrlEnd": "CursorEnd",
|
||||
"PageUp": "CursorPageUp",
|
||||
"PageDown": "CursorPageDown",
|
||||
"CtrlPageUp": "PreviousTab",
|
||||
"CtrlPageDown": "NextTab",
|
||||
"CtrlG": "ToggleHelp",
|
||||
"Alt-g": "ToggleKeyMenu",
|
||||
"CtrlR": "ToggleRuler",
|
||||
"CtrlL": "command-edit:goto ",
|
||||
"Delete": "Delete",
|
||||
"CtrlB": "ShellMode",
|
||||
"CtrlQ": "Quit",
|
||||
"CtrlE": "CommandMode",
|
||||
"CtrlW": "NextSplit",
|
||||
"CtrlU": "ToggleMacro",
|
||||
"CtrlJ": "PlayMacro",
|
||||
"Insert": "ToggleOverwriteMode",
|
||||
|
||||
// Emacs-style keybindings
|
||||
"Alt-f": "WordRight",
|
||||
"Alt-b": "WordLeft",
|
||||
"Alt-a": "StartOfText",
|
||||
"Alt-e": "EndOfLine",
|
||||
// "Alt-p": "CursorUp",
|
||||
// "Alt-n": "CursorDown",
|
||||
|
||||
// Integration with file managers
|
||||
"F2": "Save",
|
||||
"F3": "Find",
|
||||
"F4": "Quit",
|
||||
"F7": "Find",
|
||||
"F10": "Quit",
|
||||
"Esc": "Escape",
|
||||
|
||||
// Mouse bindings
|
||||
"MouseWheelUp": "ScrollUp",
|
||||
"MouseWheelDown": "ScrollDown",
|
||||
"MouseLeft": "MousePress",
|
||||
"MouseMiddle": "PastePrimary",
|
||||
"Ctrl-MouseLeft": "MouseMultiCursor",
|
||||
|
||||
"Alt-n": "SpawnMultiCursor",
|
||||
"Alt-m": "SpawnMultiCursorSelect",
|
||||
"AltShiftUp": "SpawnMultiCursorUp",
|
||||
"AltShiftDown": "SpawnMultiCursorDown",
|
||||
"Alt-p": "RemoveMultiCursor",
|
||||
"Alt-c": "RemoveAllMultiCursors",
|
||||
"Alt-x": "SkipMultiCursor",
|
||||
}
|
||||
}
|
||||
42
internal/action/events.go
Normal file
42
internal/action/events.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
type Event interface{}
|
||||
|
||||
// RawEvent is simply an escape code
|
||||
// We allow users to directly bind escape codes
|
||||
// to get around some of a limitations of terminals
|
||||
type RawEvent struct {
|
||||
esc string
|
||||
}
|
||||
|
||||
// KeyEvent is a key event containing a key code,
|
||||
// some possible modifiers (alt, ctrl, etc...) and
|
||||
// a rune if it was simply a character press
|
||||
// Note: to be compatible with tcell events,
|
||||
// for ctrl keys r=code
|
||||
type KeyEvent struct {
|
||||
code tcell.Key
|
||||
mod tcell.ModMask
|
||||
r rune
|
||||
}
|
||||
|
||||
// MouseEvent is a mouse event with a mouse button and
|
||||
// any possible key modifiers
|
||||
type MouseEvent struct {
|
||||
btn tcell.ButtonMask
|
||||
mod tcell.ModMask
|
||||
}
|
||||
|
||||
type KeyAction func(Handler) bool
|
||||
type MouseAction func(Handler, tcell.EventMouse) bool
|
||||
|
||||
// A Handler will take a tcell event and execute it
|
||||
// appropriately
|
||||
type Handler interface {
|
||||
HandleEvent(tcell.Event)
|
||||
HandleCommand(string)
|
||||
}
|
||||
48
internal/action/globals.go
Normal file
48
internal/action/globals.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package action
|
||||
|
||||
import "github.com/zyedidia/micro/internal/buffer"
|
||||
|
||||
var InfoBar *InfoPane
|
||||
var LogBufPane *BufPane
|
||||
|
||||
// InitGlobals initializes the log buffer and the info bar
|
||||
func InitGlobals() {
|
||||
InfoBar = NewInfoBar()
|
||||
buffer.LogBuf = buffer.NewBufferFromString("", "Log", buffer.BTLog)
|
||||
}
|
||||
|
||||
// GetInfoBar returns the infobar pane
|
||||
func GetInfoBar() *InfoPane {
|
||||
return InfoBar
|
||||
}
|
||||
|
||||
// WriteLog writes a string to the log buffer
|
||||
func WriteLog(s string) {
|
||||
buffer.WriteLog(s)
|
||||
if LogBufPane != nil {
|
||||
LogBufPane.CursorEnd()
|
||||
v := LogBufPane.GetView()
|
||||
endY := buffer.LogBuf.End().Y
|
||||
|
||||
if endY > v.StartLine+v.Height {
|
||||
v.StartLine = buffer.LogBuf.End().Y - v.Height + 2
|
||||
LogBufPane.SetView(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OpenLogBuf opens the log buffer from the current bufpane
|
||||
// If the current bufpane is a log buffer nothing happens,
|
||||
// otherwise the log buffer is opened in a horizontal split
|
||||
func (h *BufPane) OpenLogBuf() {
|
||||
LogBufPane = h.HSplitBuf(buffer.LogBuf)
|
||||
LogBufPane.CursorEnd()
|
||||
|
||||
v := LogBufPane.GetView()
|
||||
endY := buffer.LogBuf.End().Y
|
||||
|
||||
if endY > v.StartLine+v.Height {
|
||||
v.StartLine = buffer.LogBuf.End().Y - v.Height + 2
|
||||
LogBufPane.SetView(v)
|
||||
}
|
||||
}
|
||||
280
internal/action/infocomplete.go
Normal file
280
internal/action/infocomplete.go
Normal file
@@ -0,0 +1,280 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// This file is meant (for now) for autocompletion in command mode, not
|
||||
// while coding. This helps micro autocomplete commands and then filenames
|
||||
// for example with `vsplit filename`.
|
||||
|
||||
// CommandComplete autocompletes commands
|
||||
func CommandComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := buffer.GetArg(b)
|
||||
|
||||
var suggestions []string
|
||||
for cmd := range commands {
|
||||
if strings.HasPrefix(cmd, input) {
|
||||
suggestions = append(suggestions, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// HelpComplete autocompletes help topics
|
||||
func HelpComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := buffer.GetArg(b)
|
||||
|
||||
var suggestions []string
|
||||
|
||||
for _, file := range config.ListRuntimeFiles(config.RTHelp) {
|
||||
topic := file.Name()
|
||||
if strings.HasPrefix(topic, input) {
|
||||
suggestions = append(suggestions, topic)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// colorschemeComplete tab-completes names of colorschemes.
|
||||
// This is just a heper value for OptionValueComplete
|
||||
func colorschemeComplete(input string) (string, []string) {
|
||||
var suggestions []string
|
||||
files := config.ListRuntimeFiles(config.RTColorscheme)
|
||||
|
||||
for _, f := range files {
|
||||
if strings.HasPrefix(f.Name(), input) {
|
||||
suggestions = append(suggestions, f.Name())
|
||||
}
|
||||
}
|
||||
|
||||
var chosen string
|
||||
if len(suggestions) == 1 {
|
||||
chosen = suggestions[0]
|
||||
}
|
||||
|
||||
return chosen, suggestions
|
||||
}
|
||||
|
||||
func contains(s []string, e string) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OptionComplete autocompletes options
|
||||
func OptionComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := buffer.GetArg(b)
|
||||
|
||||
var suggestions []string
|
||||
for option := range config.GlobalSettings {
|
||||
if strings.HasPrefix(option, input) {
|
||||
suggestions = append(suggestions, option)
|
||||
}
|
||||
}
|
||||
// for option := range localSettings {
|
||||
// if strings.HasPrefix(option, input) && !contains(suggestions, option) {
|
||||
// suggestions = append(suggestions, option)
|
||||
// }
|
||||
// }
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// OptionValueComplete completes values for various options
|
||||
func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
input, argstart := buffer.GetArg(b)
|
||||
|
||||
completeValue := false
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
if len(args) >= 2 {
|
||||
// localSettings := config.DefaultLocalSettings()
|
||||
for option := range config.GlobalSettings {
|
||||
if option == string(args[len(args)-2]) {
|
||||
completeValue = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// for option := range localSettings {
|
||||
// if option == string(args[len(args)-2]) {
|
||||
// completeValue = true
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
}
|
||||
if !completeValue {
|
||||
return OptionComplete(b)
|
||||
}
|
||||
|
||||
inputOpt := string(args[len(args)-2])
|
||||
|
||||
inputOpt = strings.TrimSpace(inputOpt)
|
||||
var suggestions []string
|
||||
// localSettings := config.DefaultLocalSettings()
|
||||
var optionVal interface{}
|
||||
for k, option := range config.GlobalSettings {
|
||||
if k == inputOpt {
|
||||
optionVal = option
|
||||
}
|
||||
}
|
||||
// for k, option := range localSettings {
|
||||
// if k == inputOpt {
|
||||
// optionVal = option
|
||||
// }
|
||||
// }
|
||||
|
||||
switch optionVal.(type) {
|
||||
case bool:
|
||||
if strings.HasPrefix("on", input) {
|
||||
suggestions = append(suggestions, "on")
|
||||
} else if strings.HasPrefix("true", input) {
|
||||
suggestions = append(suggestions, "true")
|
||||
}
|
||||
if strings.HasPrefix("off", input) {
|
||||
suggestions = append(suggestions, "off")
|
||||
} else if strings.HasPrefix("false", input) {
|
||||
suggestions = append(suggestions, "false")
|
||||
}
|
||||
case string:
|
||||
switch inputOpt {
|
||||
case "colorscheme":
|
||||
_, suggestions = colorschemeComplete(input)
|
||||
case "fileformat":
|
||||
if strings.HasPrefix("unix", input) {
|
||||
suggestions = append(suggestions, "unix")
|
||||
}
|
||||
if strings.HasPrefix("dos", input) {
|
||||
suggestions = append(suggestions, "dos")
|
||||
}
|
||||
case "sucmd":
|
||||
if strings.HasPrefix("sudo", input) {
|
||||
suggestions = append(suggestions, "sudo")
|
||||
}
|
||||
if strings.HasPrefix("doas", input) {
|
||||
suggestions = append(suggestions, "doas")
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(suggestions)
|
||||
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// PluginCmdComplete autocompletes the plugin command
|
||||
func PluginCmdComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := buffer.GetArg(b)
|
||||
|
||||
var suggestions []string
|
||||
for _, cmd := range PluginCmds {
|
||||
if strings.HasPrefix(cmd, input) {
|
||||
suggestions = append(suggestions, cmd)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// PluginComplete completes values for the plugin command
|
||||
func PluginComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
input, argstart := buffer.GetArg(b)
|
||||
|
||||
completeValue := false
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
if len(args) >= 2 {
|
||||
for _, cmd := range PluginCmds {
|
||||
if cmd == string(args[len(args)-2]) {
|
||||
completeValue = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !completeValue {
|
||||
return PluginCmdComplete(b)
|
||||
}
|
||||
|
||||
var suggestions []string
|
||||
for _, pl := range config.Plugins {
|
||||
if strings.HasPrefix(pl.Name, input) {
|
||||
suggestions = append(suggestions, pl.Name)
|
||||
}
|
||||
}
|
||||
sort.Strings(suggestions)
|
||||
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// PluginNameComplete completes with the names of loaded plugins
|
||||
// func PluginNameComplete(b *buffer.Buffer) ([]string, []string) {
|
||||
// c := b.GetActiveCursor()
|
||||
// input, argstart := buffer.GetArg(b)
|
||||
//
|
||||
// var suggestions []string
|
||||
// for _, pp := range config.GetAllPluginPackages(nil) {
|
||||
// if strings.HasPrefix(pp.Name, input) {
|
||||
// suggestions = append(suggestions, pp.Name)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// sort.Strings(suggestions)
|
||||
// completions := make([]string, len(suggestions))
|
||||
// for i := range suggestions {
|
||||
// completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
// }
|
||||
// return completions, suggestions
|
||||
// }
|
||||
|
||||
// // MakeCompletion registers a function from a plugin for autocomplete commands
|
||||
// func MakeCompletion(function string) Completion {
|
||||
// pluginCompletions = append(pluginCompletions, LuaFunctionComplete(function))
|
||||
// return Completion(-len(pluginCompletions))
|
||||
// }
|
||||
224
internal/action/infopane.go
Normal file
224
internal/action/infopane.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/display"
|
||||
"github.com/zyedidia/micro/internal/info"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
type InfoKeyAction func(*InfoPane)
|
||||
|
||||
type InfoPane struct {
|
||||
*BufPane
|
||||
*info.InfoBuf
|
||||
}
|
||||
|
||||
func NewInfoPane(ib *info.InfoBuf, w display.BWindow, tab *Tab) *InfoPane {
|
||||
ip := new(InfoPane)
|
||||
ip.InfoBuf = ib
|
||||
ip.BufPane = NewBufPane(ib.Buffer, w, tab)
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
func NewInfoBar() *InfoPane {
|
||||
ib := info.NewBuffer()
|
||||
w := display.NewInfoWindow(ib)
|
||||
return NewInfoPane(ib, w, nil)
|
||||
}
|
||||
|
||||
func (h *InfoPane) Close() {
|
||||
h.InfoBuf.Close()
|
||||
h.BufPane.Close()
|
||||
}
|
||||
|
||||
func (h *InfoPane) HandleEvent(event tcell.Event) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
ke := KeyEvent{
|
||||
code: e.Key(),
|
||||
mod: e.Modifiers(),
|
||||
r: e.Rune(),
|
||||
}
|
||||
|
||||
done := h.DoKeyEvent(ke)
|
||||
hasYN := h.HasYN
|
||||
if e.Key() == tcell.KeyRune && hasYN {
|
||||
if e.Rune() == 'y' && hasYN {
|
||||
h.YNResp = true
|
||||
h.DonePrompt(false)
|
||||
} else if e.Rune() == 'n' && hasYN {
|
||||
h.YNResp = false
|
||||
h.DonePrompt(false)
|
||||
}
|
||||
}
|
||||
if e.Key() == tcell.KeyRune && !done && !hasYN {
|
||||
h.DoRuneInsert(e.Rune())
|
||||
done = true
|
||||
}
|
||||
if done && h.HasPrompt && !hasYN {
|
||||
resp := string(h.LineBytes(0))
|
||||
hist := h.History[h.PromptType]
|
||||
hist[h.HistoryNum] = resp
|
||||
if h.EventCallback != nil {
|
||||
h.EventCallback(resp)
|
||||
}
|
||||
}
|
||||
default:
|
||||
h.BufPane.HandleEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
// DoKeyEvent executes a key event for the command bar, doing any overridden actions
|
||||
func (h *InfoPane) DoKeyEvent(e KeyEvent) bool {
|
||||
done := false
|
||||
if action, ok := BufKeyBindings[e]; ok {
|
||||
estr := BufKeyStrings[e]
|
||||
for _, s := range InfoNones {
|
||||
if s == estr {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for s, a := range InfoOverrides {
|
||||
// TODO this is a hack and really we should have support
|
||||
// for having binding overrides for different buffers
|
||||
if strings.HasPrefix(estr, s) {
|
||||
done = true
|
||||
a(h)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !done {
|
||||
done = action(h.BufPane)
|
||||
}
|
||||
}
|
||||
return done
|
||||
}
|
||||
|
||||
// InfoNones is a list of actions that should have no effect when executed
|
||||
// by an infohandler
|
||||
var InfoNones = []string{
|
||||
"Save",
|
||||
"SaveAll",
|
||||
"SaveAs",
|
||||
"Find",
|
||||
"FindNext",
|
||||
"FindPrevious",
|
||||
"Center",
|
||||
"DuplicateLine",
|
||||
"MoveLinesUp",
|
||||
"MoveLinesDown",
|
||||
"OpenFile",
|
||||
"Start",
|
||||
"End",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
"SelectPageUp",
|
||||
"SelectPageDown",
|
||||
"HalfPageUp",
|
||||
"HalfPageDown",
|
||||
"ToggleHelp",
|
||||
"ToggleKeyMenu",
|
||||
"ToggleDiffGutter",
|
||||
"ToggleRuler",
|
||||
"JumpLine",
|
||||
"ClearStatus",
|
||||
"ShellMode",
|
||||
"CommandMode",
|
||||
"AddTab",
|
||||
"PreviousTab",
|
||||
"NextTab",
|
||||
"NextSplit",
|
||||
"PreviousSplit",
|
||||
"Unsplit",
|
||||
"VSplit",
|
||||
"HSplit",
|
||||
"ToggleMacro",
|
||||
"PlayMacro",
|
||||
"Suspend",
|
||||
"ScrollUp",
|
||||
"ScrollDown",
|
||||
"SpawnMultiCursor",
|
||||
"SpawnMultiCursorSelect",
|
||||
"RemoveMultiCursor",
|
||||
"RemoveAllMultiCursors",
|
||||
"SkipMultiCursor",
|
||||
}
|
||||
|
||||
// InfoOverrides is the list of actions which have been overridden
|
||||
// by the infohandler
|
||||
var InfoOverrides = map[string]InfoKeyAction{
|
||||
"CursorUp": (*InfoPane).CursorUp,
|
||||
"CursorDown": (*InfoPane).CursorDown,
|
||||
"InsertNewline": (*InfoPane).InsertNewline,
|
||||
"Autocomplete": (*InfoPane).Autocomplete,
|
||||
"Escape": (*InfoPane).Escape,
|
||||
"Quit": (*InfoPane).Quit,
|
||||
"QuitAll": (*InfoPane).QuitAll,
|
||||
}
|
||||
|
||||
// CursorUp cycles history up
|
||||
func (h *InfoPane) CursorUp() {
|
||||
h.UpHistory(h.History[h.PromptType])
|
||||
}
|
||||
|
||||
// CursorDown cycles history down
|
||||
func (h *InfoPane) CursorDown() {
|
||||
h.DownHistory(h.History[h.PromptType])
|
||||
}
|
||||
|
||||
// Autocomplete begins autocompletion
|
||||
func (h *InfoPane) Autocomplete() {
|
||||
b := h.Buf
|
||||
if b.HasSuggestions {
|
||||
b.CycleAutocomplete(true)
|
||||
return
|
||||
}
|
||||
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(0)
|
||||
l = util.SliceStart(l, c.X)
|
||||
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
cmd := string(args[0])
|
||||
|
||||
if h.PromptType == "Command" {
|
||||
if len(args) == 1 {
|
||||
b.Autocomplete(CommandComplete)
|
||||
} else if action, ok := commands[cmd]; ok {
|
||||
if action.completer != nil {
|
||||
b.Autocomplete(action.completer)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// by default use filename autocompletion
|
||||
b.Autocomplete(buffer.FileComplete)
|
||||
}
|
||||
}
|
||||
|
||||
// InsertNewline completes the prompt
|
||||
func (h *InfoPane) InsertNewline() {
|
||||
if !h.HasYN {
|
||||
h.DonePrompt(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Quit cancels the prompt
|
||||
func (h *InfoPane) Quit() {
|
||||
h.DonePrompt(true)
|
||||
}
|
||||
|
||||
// QuitAll cancels the prompt
|
||||
func (h *InfoPane) QuitAll() {
|
||||
h.DonePrompt(true)
|
||||
}
|
||||
|
||||
// Escape cancels the prompt
|
||||
func (h *InfoPane) Escape() {
|
||||
h.DonePrompt(true)
|
||||
}
|
||||
16
internal/action/pane.go
Normal file
16
internal/action/pane.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/micro/internal/display"
|
||||
)
|
||||
|
||||
type Pane interface {
|
||||
Handler
|
||||
display.Window
|
||||
ID() uint64
|
||||
SetID(i uint64)
|
||||
Name() string
|
||||
Close()
|
||||
SetTab(t *Tab)
|
||||
Tab() *Tab
|
||||
}
|
||||
40
internal/action/rawpane.go
Normal file
40
internal/action/rawpane.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/display"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
type RawPane struct {
|
||||
*BufPane
|
||||
}
|
||||
|
||||
func NewRawPaneFromWin(b *buffer.Buffer, win display.BWindow, tab *Tab) *RawPane {
|
||||
rh := new(RawPane)
|
||||
rh.BufPane = NewBufPane(b, win, tab)
|
||||
|
||||
return rh
|
||||
}
|
||||
|
||||
func NewRawPane(tab *Tab) *RawPane {
|
||||
b := buffer.NewBufferFromString("", "", buffer.BTRaw)
|
||||
w := display.NewBufWindow(0, 0, 0, 0, b)
|
||||
return NewRawPaneFromWin(b, w, tab)
|
||||
}
|
||||
|
||||
func (h *RawPane) HandleEvent(event tcell.Event) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
if e.Key() == tcell.KeyCtrlQ {
|
||||
h.Quit()
|
||||
}
|
||||
}
|
||||
|
||||
h.Buf.Insert(h.Cursor.Loc, reflect.TypeOf(event).String()[7:])
|
||||
h.Buf.Insert(h.Cursor.Loc, fmt.Sprintf(": %q\n", event.EscSeq()))
|
||||
h.Relocate()
|
||||
}
|
||||
307
internal/action/tab.go
Normal file
307
internal/action/tab.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/display"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/views"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
// The TabList is a list of tabs and a window to display the tab bar
|
||||
// at the top of the screen
|
||||
type TabList struct {
|
||||
*display.TabWindow
|
||||
List []*Tab
|
||||
}
|
||||
|
||||
// NewTabList creates a TabList from a list of buffers by creating a Tab
|
||||
// for each buffer
|
||||
func NewTabList(bufs []*buffer.Buffer) *TabList {
|
||||
w, h := screen.Screen.Size()
|
||||
iOffset := config.GetInfoBarOffset()
|
||||
tl := new(TabList)
|
||||
tl.List = make([]*Tab, len(bufs))
|
||||
if len(bufs) > 1 {
|
||||
for i, b := range bufs {
|
||||
tl.List[i] = NewTabFromBuffer(0, 1, w, h-1-iOffset, b)
|
||||
}
|
||||
} else {
|
||||
tl.List[0] = NewTabFromBuffer(0, 0, w, h-iOffset, bufs[0])
|
||||
}
|
||||
tl.TabWindow = display.NewTabWindow(w, 0)
|
||||
tl.Names = make([]string, len(bufs))
|
||||
|
||||
return tl
|
||||
}
|
||||
|
||||
// UpdateNames makes sure that the list of names the tab window has access to is
|
||||
// correct
|
||||
func (t *TabList) UpdateNames() {
|
||||
t.Names = t.Names[:0]
|
||||
for _, p := range t.List {
|
||||
t.Names = append(t.Names, p.Panes[p.active].Name())
|
||||
}
|
||||
}
|
||||
|
||||
// AddTab adds a new tab to this TabList
|
||||
func (t *TabList) AddTab(p *Tab) {
|
||||
t.List = append(t.List, p)
|
||||
t.Resize()
|
||||
t.UpdateNames()
|
||||
}
|
||||
|
||||
// RemoveTab removes a tab with the given id from the TabList
|
||||
func (t *TabList) RemoveTab(id uint64) {
|
||||
for i, p := range t.List {
|
||||
if len(p.Panes) == 0 {
|
||||
continue
|
||||
}
|
||||
if p.Panes[0].ID() == id {
|
||||
copy(t.List[i:], t.List[i+1:])
|
||||
t.List[len(t.List)-1] = nil
|
||||
t.List = t.List[:len(t.List)-1]
|
||||
if t.Active() >= len(t.List) {
|
||||
t.SetActive(len(t.List) - 1)
|
||||
}
|
||||
t.Resize()
|
||||
t.UpdateNames()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resize resizes all elements within the tab list
|
||||
// One thing to note is that when there is only 1 tab
|
||||
// the tab bar should not be drawn so resizing must take
|
||||
// that into account
|
||||
func (t *TabList) Resize() {
|
||||
w, h := screen.Screen.Size()
|
||||
iOffset := config.GetInfoBarOffset()
|
||||
InfoBar.Resize(w, h-1)
|
||||
if len(t.List) > 1 {
|
||||
for _, p := range t.List {
|
||||
p.Y = 1
|
||||
p.Node.Resize(w, h-1-iOffset)
|
||||
p.Resize()
|
||||
}
|
||||
} else if len(t.List) == 1 {
|
||||
t.List[0].Y = 0
|
||||
t.List[0].Node.Resize(w, h-iOffset)
|
||||
t.List[0].Resize()
|
||||
}
|
||||
t.TabWindow.Resize(w, h)
|
||||
}
|
||||
|
||||
// HandleEvent checks for a resize event or a mouse event on the tab bar
|
||||
// otherwise it will forward the event to the currently active tab
|
||||
func (t *TabList) HandleEvent(event tcell.Event) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventResize:
|
||||
t.Resize()
|
||||
case *tcell.EventMouse:
|
||||
mx, my := e.Position()
|
||||
switch e.Buttons() {
|
||||
case tcell.Button1:
|
||||
if my == t.Y && mx == 0 {
|
||||
t.Scroll(-4)
|
||||
return
|
||||
} else if my == t.Y && mx == t.Width-1 {
|
||||
t.Scroll(4)
|
||||
return
|
||||
}
|
||||
if len(t.List) > 1 {
|
||||
ind := t.LocFromVisual(buffer.Loc{mx, my})
|
||||
if ind != -1 {
|
||||
t.SetActive(ind)
|
||||
return
|
||||
}
|
||||
if my == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
case tcell.WheelUp:
|
||||
if my == t.Y {
|
||||
t.Scroll(4)
|
||||
return
|
||||
}
|
||||
case tcell.WheelDown:
|
||||
if my == t.Y {
|
||||
t.Scroll(-4)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
t.List[t.Active()].HandleEvent(event)
|
||||
}
|
||||
|
||||
// Display updates the names and then displays the tab bar
|
||||
func (t *TabList) Display() {
|
||||
t.UpdateNames()
|
||||
if len(t.List) > 1 {
|
||||
t.TabWindow.Display()
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs is the global tab list
|
||||
var Tabs *TabList
|
||||
|
||||
func InitTabs(bufs []*buffer.Buffer) {
|
||||
Tabs = NewTabList(bufs)
|
||||
}
|
||||
|
||||
func MainTab() *Tab {
|
||||
return Tabs.List[Tabs.Active()]
|
||||
}
|
||||
|
||||
// A Tab represents a single tab
|
||||
// It consists of a list of edit panes (the open buffers),
|
||||
// a split tree (stored as just the root node), and a uiwindow
|
||||
// to display the UI elements like the borders between splits
|
||||
type Tab struct {
|
||||
*views.Node
|
||||
*display.UIWindow
|
||||
Panes []Pane
|
||||
active int
|
||||
|
||||
resizing *views.Node // node currently being resized
|
||||
// captures whether the mouse is released
|
||||
release bool
|
||||
}
|
||||
|
||||
// NewTabFromBuffer creates a new tab from the given buffer
|
||||
func NewTabFromBuffer(x, y, width, height int, b *buffer.Buffer) *Tab {
|
||||
t := new(Tab)
|
||||
t.Node = views.NewRoot(x, y, width, height)
|
||||
t.UIWindow = display.NewUIWindow(t.Node)
|
||||
|
||||
e := NewBufPaneFromBuf(b, t)
|
||||
e.SetID(t.ID())
|
||||
|
||||
t.Panes = append(t.Panes, e)
|
||||
return t
|
||||
}
|
||||
|
||||
func NewTabFromPane(x, y, width, height int, pane Pane) *Tab {
|
||||
t := new(Tab)
|
||||
t.Node = views.NewRoot(x, y, width, height)
|
||||
t.UIWindow = display.NewUIWindow(t.Node)
|
||||
pane.SetTab(t)
|
||||
pane.SetID(t.ID())
|
||||
|
||||
t.Panes = append(t.Panes, pane)
|
||||
return t
|
||||
}
|
||||
|
||||
// HandleEvent takes a tcell event and usually dispatches it to the current
|
||||
// active pane. However if the event is a resize or a mouse event where the user
|
||||
// is interacting with the UI (resizing splits) then the event is consumed here
|
||||
// If the event is a mouse event in a pane, that pane will become active and get
|
||||
// the event
|
||||
func (t *Tab) HandleEvent(event tcell.Event) {
|
||||
switch e := event.(type) {
|
||||
case *tcell.EventMouse:
|
||||
mx, my := e.Position()
|
||||
switch e.Buttons() {
|
||||
case tcell.Button1:
|
||||
wasReleased := t.release
|
||||
t.release = false
|
||||
if t.resizing != nil {
|
||||
var size int
|
||||
if t.resizing.Kind == views.STVert {
|
||||
size = mx - t.resizing.X
|
||||
} else {
|
||||
size = my - t.resizing.Y + 1
|
||||
}
|
||||
t.resizing.ResizeSplit(size)
|
||||
t.Resize()
|
||||
return
|
||||
}
|
||||
|
||||
if wasReleased {
|
||||
resizeID := t.GetMouseSplitID(buffer.Loc{mx, my})
|
||||
if resizeID != 0 {
|
||||
t.resizing = t.GetNode(uint64(resizeID))
|
||||
return
|
||||
}
|
||||
|
||||
for i, p := range t.Panes {
|
||||
v := p.GetView()
|
||||
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
|
||||
if inpane {
|
||||
t.SetActive(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case tcell.ButtonNone:
|
||||
t.resizing = nil
|
||||
t.release = true
|
||||
default:
|
||||
for _, p := range t.Panes {
|
||||
v := p.GetView()
|
||||
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
|
||||
if inpane {
|
||||
p.HandleEvent(event)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
t.Panes[t.active].HandleEvent(event)
|
||||
}
|
||||
|
||||
// SetActive changes the currently active pane to the specified index
|
||||
func (t *Tab) SetActive(i int) {
|
||||
t.active = i
|
||||
for j, p := range t.Panes {
|
||||
if j == i {
|
||||
p.SetActive(true)
|
||||
} else {
|
||||
p.SetActive(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetPane returns the pane with the given split index
|
||||
func (t *Tab) GetPane(splitid uint64) int {
|
||||
for i, p := range t.Panes {
|
||||
if p.ID() == splitid {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Remove pane removes the pane with the given index
|
||||
func (t *Tab) RemovePane(i int) {
|
||||
copy(t.Panes[i:], t.Panes[i+1:])
|
||||
t.Panes[len(t.Panes)-1] = nil
|
||||
t.Panes = t.Panes[:len(t.Panes)-1]
|
||||
}
|
||||
|
||||
// Resize resizes all panes according to their corresponding split nodes
|
||||
func (t *Tab) Resize() {
|
||||
for _, p := range t.Panes {
|
||||
n := t.GetNode(p.ID())
|
||||
pv := p.GetView()
|
||||
offset := 0
|
||||
if n.X != 0 {
|
||||
offset = 1
|
||||
}
|
||||
pv.X, pv.Y = n.X+offset, n.Y
|
||||
p.SetView(pv)
|
||||
p.Resize(n.W-offset, n.H)
|
||||
}
|
||||
}
|
||||
|
||||
// CurPane returns the currently active pane
|
||||
func (t *Tab) CurPane() *BufPane {
|
||||
p, ok := t.Panes[t.active].(*BufPane)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return p
|
||||
}
|
||||
42
internal/action/terminal_supported.go
Normal file
42
internal/action/terminal_supported.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// +build linux darwin dragonfly openbsd_amd64 freebsd
|
||||
|
||||
package action
|
||||
|
||||
import (
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
"github.com/zyedidia/micro/internal/shell"
|
||||
)
|
||||
|
||||
// TermEmuSupported is a constant that marks if the terminal emulator is supported
|
||||
const TermEmuSupported = true
|
||||
|
||||
// RunTermEmulator starts a terminal emulator from a bufpane with the given input (command)
|
||||
// if wait is true it will wait for the user to exit by pressing enter once the executable has terminated
|
||||
// if getOutput is true it will redirect the stdout of the process to a pipe which will be passed to the
|
||||
// callback which is a function that takes a string and a list of optional user arguments
|
||||
func RunTermEmulator(h *BufPane, input string, wait bool, getOutput bool, callback func(out string, userargs []interface{}), userargs []interface{}) error {
|
||||
args, err := shellquote.Split(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(args) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
t := new(shell.Terminal)
|
||||
t.Start(args, getOutput, wait, callback, userargs)
|
||||
|
||||
h.AddTab()
|
||||
id := MainTab().Panes[0].ID()
|
||||
|
||||
v := h.GetView()
|
||||
|
||||
tp, err := NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
MainTab().Panes[0] = tp
|
||||
MainTab().SetActive(0)
|
||||
|
||||
return nil
|
||||
}
|
||||
13
internal/action/terminal_unsupported.go
Normal file
13
internal/action/terminal_unsupported.go
Normal file
@@ -0,0 +1,13 @@
|
||||
// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd_amd64
|
||||
|
||||
package action
|
||||
|
||||
import "errors"
|
||||
|
||||
// TermEmuSupported is a constant that marks if the terminal emulator is supported
|
||||
const TermEmuSupported = false
|
||||
|
||||
// RunTermEmulator returns an error for unsupported systems (non-unix systems
|
||||
func RunTermEmulator(input string, wait bool, getOutput bool, callback func(out string, userargs []interface{}), userargs []interface{}) error {
|
||||
return errors.New("Unsupported operating system")
|
||||
}
|
||||
139
internal/action/termpane.go
Normal file
139
internal/action/termpane.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
|
||||
"github.com/zyedidia/clipboard"
|
||||
"github.com/zyedidia/micro/internal/display"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/shell"
|
||||
"github.com/zyedidia/tcell"
|
||||
"github.com/zyedidia/terminal"
|
||||
)
|
||||
|
||||
type TermPane struct {
|
||||
*shell.Terminal
|
||||
display.Window
|
||||
|
||||
mouseReleased bool
|
||||
id uint64
|
||||
tab *Tab
|
||||
}
|
||||
|
||||
func NewTermPane(x, y, w, h int, t *shell.Terminal, id uint64, tab *Tab) (*TermPane, error) {
|
||||
if !TermEmuSupported {
|
||||
return nil, errors.New("Terminal emulator is not supported on this system")
|
||||
}
|
||||
|
||||
th := new(TermPane)
|
||||
th.Terminal = t
|
||||
th.id = id
|
||||
th.mouseReleased = true
|
||||
th.Window = display.NewTermWindow(x, y, w, h, t)
|
||||
th.tab = tab
|
||||
return th, nil
|
||||
}
|
||||
|
||||
func (t *TermPane) ID() uint64 {
|
||||
return t.id
|
||||
}
|
||||
|
||||
func (t *TermPane) SetID(i uint64) {
|
||||
t.id = i
|
||||
}
|
||||
|
||||
func (t *TermPane) SetTab(tab *Tab) {
|
||||
t.tab = tab
|
||||
}
|
||||
|
||||
func (t *TermPane) Tab() *Tab {
|
||||
return t.tab
|
||||
}
|
||||
|
||||
func (t *TermPane) Close() {}
|
||||
|
||||
func (t *TermPane) Quit() {
|
||||
t.Close()
|
||||
if len(MainTab().Panes) > 1 {
|
||||
t.Unsplit()
|
||||
} else if len(Tabs.List) > 1 {
|
||||
Tabs.RemoveTab(t.id)
|
||||
} else {
|
||||
screen.Screen.Fini()
|
||||
InfoBar.Close()
|
||||
runtime.Goexit()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TermPane) Unsplit() {
|
||||
n := MainTab().GetNode(t.id)
|
||||
n.Unsplit()
|
||||
|
||||
MainTab().RemovePane(MainTab().GetPane(t.id))
|
||||
MainTab().Resize()
|
||||
MainTab().SetActive(len(MainTab().Panes) - 1)
|
||||
}
|
||||
|
||||
// HandleEvent handles a tcell event by forwarding it to the terminal emulator
|
||||
// If the event is a mouse event and the program running in the emulator
|
||||
// does not have mouse support, the emulator will support selections and
|
||||
// copy-paste
|
||||
func (t *TermPane) HandleEvent(event tcell.Event) {
|
||||
if e, ok := event.(*tcell.EventKey); ok {
|
||||
if t.Status == shell.TTDone {
|
||||
switch e.Key() {
|
||||
case tcell.KeyEscape, tcell.KeyCtrlQ, tcell.KeyEnter:
|
||||
t.Close()
|
||||
t.Quit()
|
||||
default:
|
||||
}
|
||||
}
|
||||
if e.Key() == tcell.KeyCtrlC && t.HasSelection() {
|
||||
clipboard.WriteAll(t.GetSelection(t.GetView().Width), "clipboard")
|
||||
InfoBar.Message("Copied selection to clipboard")
|
||||
} else if t.Status != shell.TTDone {
|
||||
t.WriteString(event.EscSeq())
|
||||
}
|
||||
} else if _, ok := event.(*tcell.EventPaste); ok {
|
||||
if t.Status != shell.TTDone {
|
||||
t.WriteString(event.EscSeq())
|
||||
}
|
||||
} else if e, ok := event.(*tcell.EventMouse); e != nil && (!ok || t.State.Mode(terminal.ModeMouseMask)) {
|
||||
// t.WriteString(event.EscSeq())
|
||||
} else if e != nil {
|
||||
x, y := e.Position()
|
||||
v := t.GetView()
|
||||
x -= v.X
|
||||
y -= v.Y
|
||||
|
||||
if e.Buttons() == tcell.Button1 {
|
||||
if !t.mouseReleased {
|
||||
// drag
|
||||
t.Selection[1].X = x
|
||||
t.Selection[1].Y = y
|
||||
} else {
|
||||
t.Selection[0].X = x
|
||||
t.Selection[0].Y = y
|
||||
t.Selection[1].X = x
|
||||
t.Selection[1].Y = y
|
||||
}
|
||||
|
||||
t.mouseReleased = false
|
||||
} else if e.Buttons() == tcell.ButtonNone {
|
||||
if !t.mouseReleased {
|
||||
t.Selection[1].X = x
|
||||
t.Selection[1].Y = y
|
||||
}
|
||||
t.mouseReleased = true
|
||||
}
|
||||
}
|
||||
|
||||
if t.Status == shell.TTClose {
|
||||
t.Quit()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TermPane) HandleCommand(input string) {
|
||||
InfoBar.Error("Commands are unsupported in term for now")
|
||||
}
|
||||
206
internal/buffer/autocomplete.go
Normal file
206
internal/buffer/autocomplete.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// A Completer is a function that takes a buffer and returns info
|
||||
// describing what autocompletions should be inserted at the current
|
||||
// cursor location
|
||||
// It returns a list of string suggestions which will be inserted at
|
||||
// the current cursor location if selected as well as a list of
|
||||
// suggestion names which can be displayed in an autocomplete box or
|
||||
// other UI element
|
||||
type Completer func(*Buffer) ([]string, []string)
|
||||
|
||||
func (b *Buffer) GetSuggestions() {
|
||||
|
||||
}
|
||||
|
||||
// Autocomplete starts the autocomplete process
|
||||
func (b *Buffer) Autocomplete(c Completer) bool {
|
||||
b.Completions, b.Suggestions = c(b)
|
||||
if len(b.Completions) != len(b.Suggestions) || len(b.Completions) == 0 {
|
||||
return false
|
||||
}
|
||||
b.CurSuggestion = -1
|
||||
b.CycleAutocomplete(true)
|
||||
return true
|
||||
}
|
||||
|
||||
// CycleAutocomplete moves to the next suggestion
|
||||
func (b *Buffer) CycleAutocomplete(forward bool) {
|
||||
prevSuggestion := b.CurSuggestion
|
||||
|
||||
if forward {
|
||||
b.CurSuggestion++
|
||||
} else {
|
||||
b.CurSuggestion--
|
||||
}
|
||||
if b.CurSuggestion >= len(b.Suggestions) {
|
||||
b.CurSuggestion = 0
|
||||
} else if b.CurSuggestion < 0 {
|
||||
b.CurSuggestion = len(b.Suggestions) - 1
|
||||
}
|
||||
|
||||
c := b.GetActiveCursor()
|
||||
start := c.Loc
|
||||
end := c.Loc
|
||||
if prevSuggestion < len(b.Suggestions) && prevSuggestion >= 0 {
|
||||
start = end.Move(-utf8.RuneCountInString(b.Completions[prevSuggestion]), b)
|
||||
} else {
|
||||
// end = start.Move(1, b)
|
||||
}
|
||||
|
||||
b.Replace(start, end, b.Completions[b.CurSuggestion])
|
||||
if len(b.Suggestions) > 1 {
|
||||
b.HasSuggestions = true
|
||||
}
|
||||
}
|
||||
|
||||
// GetWord gets the most recent word separated by any separator
|
||||
// (whitespace, punctuation, any non alphanumeric character)
|
||||
func GetWord(b *Buffer) ([]byte, int) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
|
||||
if c.X == 0 || util.IsWhitespace(b.RuneAt(c.Loc)) {
|
||||
return []byte{}, -1
|
||||
}
|
||||
|
||||
if util.IsNonAlphaNumeric(b.RuneAt(c.Loc)) {
|
||||
return []byte{}, c.X
|
||||
}
|
||||
|
||||
args := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
|
||||
input := args[len(args)-1]
|
||||
return input, c.X - utf8.RuneCount(input)
|
||||
}
|
||||
|
||||
// GetArg gets the most recent word (separated by ' ' only)
|
||||
func GetArg(b *Buffer) (string, int) {
|
||||
c := b.GetActiveCursor()
|
||||
l := b.LineBytes(c.Y)
|
||||
l = util.SliceStart(l, c.X)
|
||||
|
||||
args := bytes.Split(l, []byte{' '})
|
||||
input := string(args[len(args)-1])
|
||||
argstart := 0
|
||||
for i, a := range args {
|
||||
if i == len(args)-1 {
|
||||
break
|
||||
}
|
||||
argstart += utf8.RuneCount(a) + 1
|
||||
}
|
||||
|
||||
return input, argstart
|
||||
}
|
||||
|
||||
// FileComplete autocompletes filenames
|
||||
func FileComplete(b *Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := GetArg(b)
|
||||
|
||||
sep := string(os.PathSeparator)
|
||||
dirs := strings.Split(input, sep)
|
||||
|
||||
var files []os.FileInfo
|
||||
var err error
|
||||
if len(dirs) > 1 {
|
||||
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
|
||||
|
||||
directories, _ = util.ReplaceHome(directories)
|
||||
files, err = ioutil.ReadDir(directories)
|
||||
} else {
|
||||
files, err = ioutil.ReadDir(".")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var suggestions []string
|
||||
for _, f := range files {
|
||||
name := f.Name()
|
||||
if f.IsDir() {
|
||||
name += sep
|
||||
}
|
||||
if strings.HasPrefix(name, dirs[len(dirs)-1]) {
|
||||
suggestions = append(suggestions, name)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(suggestions)
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
var complete string
|
||||
if len(dirs) > 1 {
|
||||
complete = strings.Join(dirs[:len(dirs)-1], sep) + sep + suggestions[i]
|
||||
} else {
|
||||
complete = suggestions[i]
|
||||
}
|
||||
completions[i] = util.SliceEndStr(complete, c.X-argstart)
|
||||
}
|
||||
|
||||
return completions, suggestions
|
||||
}
|
||||
|
||||
// BufferComplete autocompletes based on previous words in the buffer
|
||||
func BufferComplete(b *Buffer) ([]string, []string) {
|
||||
c := b.GetActiveCursor()
|
||||
input, argstart := GetWord(b)
|
||||
|
||||
if argstart == -1 {
|
||||
return []string{}, []string{}
|
||||
}
|
||||
|
||||
inputLen := utf8.RuneCount(input)
|
||||
|
||||
suggestionsSet := make(map[string]struct{})
|
||||
|
||||
var suggestions []string
|
||||
for i := c.Y; i >= 0; i-- {
|
||||
l := b.LineBytes(i)
|
||||
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
|
||||
for _, w := range words {
|
||||
if bytes.HasPrefix(w, input) && utf8.RuneCount(w) > inputLen {
|
||||
strw := string(w)
|
||||
if _, ok := suggestionsSet[strw]; !ok {
|
||||
suggestionsSet[strw] = struct{}{}
|
||||
suggestions = append(suggestions, strw)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := c.Y + 1; i < b.LinesNum(); i++ {
|
||||
l := b.LineBytes(i)
|
||||
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
|
||||
for _, w := range words {
|
||||
if bytes.HasPrefix(w, input) && utf8.RuneCount(w) > inputLen {
|
||||
strw := string(w)
|
||||
if _, ok := suggestionsSet[strw]; !ok {
|
||||
suggestionsSet[strw] = struct{}{}
|
||||
suggestions = append(suggestions, strw)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(suggestions) > 1 {
|
||||
suggestions = append(suggestions, string(input))
|
||||
}
|
||||
|
||||
completions := make([]string, len(suggestions))
|
||||
for i := range suggestions {
|
||||
completions[i] = util.SliceEndStr(suggestions[i], c.X-argstart)
|
||||
}
|
||||
|
||||
return completions, suggestions
|
||||
}
|
||||
116
internal/buffer/backup.go
Normal file
116
internal/buffer/backup.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
"golang.org/x/text/encoding"
|
||||
)
|
||||
|
||||
const backupMsg = `A backup was detected for this file. This likely means that micro
|
||||
crashed while editing this file, or another instance of micro is currently
|
||||
editing this file.
|
||||
|
||||
The backup was created on %s, and the file is
|
||||
|
||||
%s
|
||||
|
||||
* 'recover' will apply the backup as unsaved changes to the current buffer.
|
||||
When the buffer is closed, the backup will be removed.
|
||||
* 'ignore' will ignore the backup, discarding its changes. The backup file
|
||||
will be removed.
|
||||
|
||||
Options: [r]ecover, [i]gnore: `
|
||||
|
||||
// Backup saves the current buffer to ConfigDir/backups
|
||||
func (b *Buffer) Backup(checkTime bool) error {
|
||||
if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault {
|
||||
return nil
|
||||
}
|
||||
|
||||
if checkTime {
|
||||
sub := time.Now().Sub(b.lastbackup)
|
||||
if sub < time.Duration(backupTime)*time.Millisecond {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
b.lastbackup = time.Now()
|
||||
|
||||
backupdir := filepath.Join(config.ConfigDir, "backups")
|
||||
if _, err := os.Stat(backupdir); os.IsNotExist(err) {
|
||||
os.Mkdir(backupdir, os.ModePerm)
|
||||
}
|
||||
|
||||
name := filepath.Join(backupdir, util.EscapePath(b.AbsPath))
|
||||
|
||||
err := overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) {
|
||||
if len(b.lines) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// end of line
|
||||
eol := []byte{'\n'}
|
||||
|
||||
// write lines
|
||||
if _, e = file.Write(b.lines[0].data); e != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, l := range b.lines[1:] {
|
||||
if _, e = file.Write(eol); e != nil {
|
||||
return
|
||||
}
|
||||
if _, e = file.Write(l.data); e != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}, false)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveBackup removes any backup file associated with this buffer
|
||||
func (b *Buffer) RemoveBackup() {
|
||||
if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault {
|
||||
return
|
||||
}
|
||||
f := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath))
|
||||
os.Remove(f)
|
||||
}
|
||||
|
||||
// ApplyBackup applies the corresponding backup file to this buffer (if one exists)
|
||||
// Returns true if a backup was applied
|
||||
func (b *Buffer) ApplyBackup(fsize int64) bool {
|
||||
if b.Settings["backup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault {
|
||||
backupfile := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath))
|
||||
if info, err := os.Stat(backupfile); err == nil {
|
||||
backup, err := os.Open(backupfile)
|
||||
if err == nil {
|
||||
defer backup.Close()
|
||||
t := info.ModTime()
|
||||
msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), util.EscapePath(b.AbsPath))
|
||||
choice := screen.TermPrompt(msg, []string{"r", "i", "recover", "ignore"}, true)
|
||||
|
||||
if choice%2 == 0 {
|
||||
// recover
|
||||
b.LineArray = NewLineArray(uint64(fsize), FFAuto, backup)
|
||||
b.isModified = true
|
||||
return true
|
||||
} else if choice%2 == 1 {
|
||||
// delete
|
||||
os.Remove(backupfile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
1091
internal/buffer/buffer.go
Normal file
1091
internal/buffer/buffer.go
Normal file
File diff suppressed because it is too large
Load Diff
1889
internal/buffer/buffer_generated_test.go
Normal file
1889
internal/buffer/buffer_generated_test.go
Normal file
File diff suppressed because it is too large
Load Diff
113
internal/buffer/buffer_test.go
Normal file
113
internal/buffer/buffer_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
testifyAssert "github.com/stretchr/testify/assert"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
|
||||
ulua "github.com/zyedidia/micro/internal/lua"
|
||||
)
|
||||
|
||||
type operation struct {
|
||||
start Loc
|
||||
end Loc
|
||||
text []string
|
||||
}
|
||||
|
||||
type asserter interface {
|
||||
Equal(interface{}, interface{}, ...interface{}) bool
|
||||
NotEqual(interface{}, interface{}, ...interface{}) bool
|
||||
}
|
||||
|
||||
type noOpAsserter struct {
|
||||
}
|
||||
|
||||
func (a *noOpAsserter) Equal(interface{}, interface{}, ...interface{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *noOpAsserter) NotEqual(interface{}, interface{}, ...interface{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func init() {
|
||||
ulua.L = lua.NewState()
|
||||
}
|
||||
|
||||
func check(t *testing.T, before []string, operations []operation, after []string) {
|
||||
var assert asserter
|
||||
if t == nil {
|
||||
// Benchmark mode; don't perform assertions
|
||||
assert = &noOpAsserter{}
|
||||
} else {
|
||||
assert = testifyAssert.New(t)
|
||||
}
|
||||
|
||||
b := NewBufferFromString(strings.Join(before, "\n"), "", BTDefault)
|
||||
|
||||
assert.NotEqual("", b.GetName())
|
||||
assert.Equal(false, b.ExternallyModified())
|
||||
assert.Equal(false, b.Modified())
|
||||
assert.Equal(1, b.NumCursors())
|
||||
|
||||
checkText := func(lines []string) {
|
||||
assert.Equal([]byte(strings.Join(lines, "\n")), b.Bytes())
|
||||
assert.Equal(len(lines), b.LinesNum())
|
||||
for i, s := range lines {
|
||||
assert.Equal(s, b.Line(i))
|
||||
assert.Equal([]byte(s), b.LineBytes(i))
|
||||
}
|
||||
}
|
||||
|
||||
checkText(before)
|
||||
|
||||
var cursors []*Cursor
|
||||
|
||||
for _, op := range operations {
|
||||
cursor := NewCursor(b, op.start)
|
||||
cursor.SetSelectionStart(op.start)
|
||||
cursor.SetSelectionEnd(op.end)
|
||||
b.AddCursor(cursor)
|
||||
cursors = append(cursors, cursor)
|
||||
}
|
||||
|
||||
assert.Equal(1+len(operations), b.NumCursors())
|
||||
|
||||
for i, op := range operations {
|
||||
cursor := cursors[i]
|
||||
b.SetCurCursor(cursor.Num)
|
||||
cursor.DeleteSelection()
|
||||
b.Insert(cursor.Loc, strings.Join(op.text, "\n"))
|
||||
}
|
||||
|
||||
checkText(after)
|
||||
|
||||
// must have exactly two events per operation (delete and insert)
|
||||
for range operations {
|
||||
b.UndoOneEvent()
|
||||
b.UndoOneEvent()
|
||||
}
|
||||
|
||||
checkText(before)
|
||||
|
||||
for i, op := range operations {
|
||||
cursor := cursors[i]
|
||||
if op.start == op.end {
|
||||
assert.Equal(op.start, cursor.Loc)
|
||||
} else {
|
||||
assert.Equal(op.start, cursor.CurSelection[0])
|
||||
assert.Equal(op.end, cursor.CurSelection[1])
|
||||
}
|
||||
}
|
||||
|
||||
for range operations {
|
||||
b.RedoOneEvent()
|
||||
b.RedoOneEvent()
|
||||
}
|
||||
|
||||
checkText(after)
|
||||
|
||||
b.Close()
|
||||
}
|
||||
453
internal/buffer/cursor.go
Normal file
453
internal/buffer/cursor.go
Normal file
@@ -0,0 +1,453 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/clipboard"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// InBounds returns whether the given location is a valid character position in the given buffer
|
||||
func InBounds(pos Loc, buf *Buffer) bool {
|
||||
if pos.Y < 0 || pos.Y >= len(buf.lines) || pos.X < 0 || pos.X > utf8.RuneCount(buf.LineBytes(pos.Y)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// The Cursor struct stores the location of the cursor in the buffer
|
||||
// as well as the selection
|
||||
type Cursor struct {
|
||||
buf *Buffer
|
||||
Loc
|
||||
|
||||
// Last cursor x position
|
||||
LastVisualX int
|
||||
|
||||
// The current selection as a range of character numbers (inclusive)
|
||||
CurSelection [2]Loc
|
||||
// The original selection as a range of character numbers
|
||||
// This is used for line and word selection where it is necessary
|
||||
// to know what the original selection was
|
||||
OrigSelection [2]Loc
|
||||
|
||||
// Which cursor index is this (for multiple cursors)
|
||||
Num int
|
||||
}
|
||||
|
||||
func NewCursor(b *Buffer, l Loc) *Cursor {
|
||||
c := &Cursor{
|
||||
buf: b,
|
||||
Loc: l,
|
||||
}
|
||||
c.StoreVisualX()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Cursor) SetBuf(b *Buffer) {
|
||||
c.buf = b
|
||||
}
|
||||
|
||||
func (c *Cursor) Buf() *Buffer {
|
||||
return c.buf
|
||||
}
|
||||
|
||||
// Goto puts the cursor at the given cursor's location and gives
|
||||
// the current cursor its selection too
|
||||
func (c *Cursor) Goto(b Cursor) {
|
||||
c.X, c.Y, c.LastVisualX = b.X, b.Y, b.LastVisualX
|
||||
c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
|
||||
}
|
||||
|
||||
// GotoLoc puts the cursor at the given cursor's location and gives
|
||||
// the current cursor its selection too
|
||||
func (c *Cursor) GotoLoc(l Loc) {
|
||||
c.X, c.Y = l.X, l.Y
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// GetVisualX returns the x value of the cursor in visual spaces
|
||||
func (c *Cursor) GetVisualX() int {
|
||||
if c.X <= 0 {
|
||||
c.X = 0
|
||||
return 0
|
||||
}
|
||||
|
||||
bytes := c.buf.LineBytes(c.Y)
|
||||
tabsize := int(c.buf.Settings["tabsize"].(float64))
|
||||
if c.X > utf8.RuneCount(bytes) {
|
||||
c.X = utf8.RuneCount(bytes) - 1
|
||||
}
|
||||
|
||||
return util.StringWidth(bytes, c.X, tabsize)
|
||||
}
|
||||
|
||||
// GetCharPosInLine gets the char position of a visual x y
|
||||
// coordinate (this is necessary because tabs are 1 char but
|
||||
// 4 visual spaces)
|
||||
func (c *Cursor) GetCharPosInLine(b []byte, visualPos int) int {
|
||||
tabsize := int(c.buf.Settings["tabsize"].(float64))
|
||||
return util.GetCharPosInLine(b, visualPos, tabsize)
|
||||
}
|
||||
|
||||
// Start moves the cursor to the start of the line it is on
|
||||
func (c *Cursor) Start() {
|
||||
c.X = 0
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
|
||||
// StartOfText moves the cursor to the first non-whitespace rune of
|
||||
// the line it is on
|
||||
func (c *Cursor) StartOfText() {
|
||||
c.Start()
|
||||
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == utf8.RuneCount(c.buf.LineBytes(c.Y)) {
|
||||
break
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
}
|
||||
|
||||
// IsStartOfText returns whether the cursor is at the first
|
||||
// non-whitespace rune of the line it is on
|
||||
func (c *Cursor) IsStartOfText() bool {
|
||||
x := 0
|
||||
for util.IsWhitespace(c.RuneUnder(x)) {
|
||||
if x == utf8.RuneCount(c.buf.LineBytes(c.Y)) {
|
||||
break
|
||||
}
|
||||
x++
|
||||
}
|
||||
return c.X == x
|
||||
}
|
||||
|
||||
// End moves the cursor to the end of the line it is on
|
||||
func (c *Cursor) End() {
|
||||
c.X = utf8.RuneCount(c.buf.LineBytes(c.Y))
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
|
||||
// CopySelection copies the user's selection to either "primary"
|
||||
// or "clipboard"
|
||||
func (c *Cursor) CopySelection(target string) {
|
||||
if c.HasSelection() {
|
||||
if target != "primary" || c.buf.Settings["useprimary"].(bool) {
|
||||
clipboard.WriteAll(string(c.GetSelection()), target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ResetSelection resets the user's selection
|
||||
func (c *Cursor) ResetSelection() {
|
||||
c.CurSelection[0] = c.buf.Start()
|
||||
c.CurSelection[1] = c.buf.Start()
|
||||
}
|
||||
|
||||
// SetSelectionStart sets the start of the selection
|
||||
func (c *Cursor) SetSelectionStart(pos Loc) {
|
||||
c.CurSelection[0] = pos
|
||||
}
|
||||
|
||||
// SetSelectionEnd sets the end of the selection
|
||||
func (c *Cursor) SetSelectionEnd(pos Loc) {
|
||||
c.CurSelection[1] = pos
|
||||
}
|
||||
|
||||
// HasSelection returns whether or not the user has selected anything
|
||||
func (c *Cursor) HasSelection() bool {
|
||||
return c.CurSelection[0] != c.CurSelection[1]
|
||||
}
|
||||
|
||||
// DeleteSelection deletes the currently selected text
|
||||
func (c *Cursor) DeleteSelection() {
|
||||
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
|
||||
c.buf.Remove(c.CurSelection[1], c.CurSelection[0])
|
||||
c.Loc = c.CurSelection[1]
|
||||
} else if !c.HasSelection() {
|
||||
return
|
||||
} else {
|
||||
c.buf.Remove(c.CurSelection[0], c.CurSelection[1])
|
||||
c.Loc = c.CurSelection[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Deselect closes the cursor's current selection
|
||||
// Start indicates whether the cursor should be placed
|
||||
// at the start or end of the selection
|
||||
func (c *Cursor) Deselect(start bool) {
|
||||
if c.HasSelection() {
|
||||
if start {
|
||||
c.Loc = c.CurSelection[0]
|
||||
} else {
|
||||
c.Loc = c.CurSelection[1].Move(-1, c.buf)
|
||||
}
|
||||
c.ResetSelection()
|
||||
c.StoreVisualX()
|
||||
}
|
||||
}
|
||||
|
||||
// GetSelection returns the cursor's selection
|
||||
func (c *Cursor) GetSelection() []byte {
|
||||
if InBounds(c.CurSelection[0], c.buf) && InBounds(c.CurSelection[1], c.buf) {
|
||||
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
|
||||
return c.buf.Substr(c.CurSelection[1], c.CurSelection[0])
|
||||
}
|
||||
return c.buf.Substr(c.CurSelection[0], c.CurSelection[1])
|
||||
}
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
// SelectLine selects the current line
|
||||
func (c *Cursor) SelectLine() {
|
||||
c.Start()
|
||||
c.SetSelectionStart(c.Loc)
|
||||
c.End()
|
||||
if len(c.buf.lines)-1 > c.Y {
|
||||
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
||||
} else {
|
||||
c.SetSelectionEnd(c.Loc)
|
||||
}
|
||||
|
||||
c.OrigSelection = c.CurSelection
|
||||
}
|
||||
|
||||
// AddLineToSelection adds the current line to the selection
|
||||
func (c *Cursor) AddLineToSelection() {
|
||||
if c.Loc.LessThan(c.OrigSelection[0]) {
|
||||
c.Start()
|
||||
c.SetSelectionStart(c.Loc)
|
||||
c.SetSelectionEnd(c.OrigSelection[1])
|
||||
}
|
||||
if c.Loc.GreaterThan(c.OrigSelection[1]) {
|
||||
c.End()
|
||||
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
||||
c.SetSelectionStart(c.OrigSelection[0])
|
||||
}
|
||||
|
||||
if c.Loc.LessThan(c.OrigSelection[1]) && c.Loc.GreaterThan(c.OrigSelection[0]) {
|
||||
c.CurSelection = c.OrigSelection
|
||||
}
|
||||
}
|
||||
|
||||
// UpN moves the cursor up N lines (if possible)
|
||||
func (c *Cursor) UpN(amount int) {
|
||||
proposedY := c.Y - amount
|
||||
if proposedY < 0 {
|
||||
proposedY = 0
|
||||
} else if proposedY >= len(c.buf.lines) {
|
||||
proposedY = len(c.buf.lines) - 1
|
||||
}
|
||||
|
||||
bytes := c.buf.LineBytes(proposedY)
|
||||
c.X = c.GetCharPosInLine(bytes, c.LastVisualX)
|
||||
|
||||
if c.X > utf8.RuneCount(bytes) || (amount < 0 && proposedY == c.Y) {
|
||||
c.X = utf8.RuneCount(bytes)
|
||||
}
|
||||
|
||||
c.Y = proposedY
|
||||
}
|
||||
|
||||
// DownN moves the cursor down N lines (if possible)
|
||||
func (c *Cursor) DownN(amount int) {
|
||||
c.UpN(-amount)
|
||||
}
|
||||
|
||||
// Up moves the cursor up one line (if possible)
|
||||
func (c *Cursor) Up() {
|
||||
c.UpN(1)
|
||||
}
|
||||
|
||||
// Down moves the cursor down one line (if possible)
|
||||
func (c *Cursor) Down() {
|
||||
c.DownN(1)
|
||||
}
|
||||
|
||||
// Left moves the cursor left one cell (if possible) or to
|
||||
// the previous line if it is at the beginning
|
||||
func (c *Cursor) Left() {
|
||||
if c.Loc == c.buf.Start() {
|
||||
return
|
||||
}
|
||||
if c.X > 0 {
|
||||
c.X--
|
||||
} else {
|
||||
c.Up()
|
||||
c.End()
|
||||
}
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// Right moves the cursor right one cell (if possible) or
|
||||
// to the next line if it is at the end
|
||||
func (c *Cursor) Right() {
|
||||
if c.Loc == c.buf.End() {
|
||||
return
|
||||
}
|
||||
if c.X < utf8.RuneCount(c.buf.LineBytes(c.Y)) {
|
||||
c.X++
|
||||
} else {
|
||||
c.Down()
|
||||
c.Start()
|
||||
}
|
||||
c.StoreVisualX()
|
||||
}
|
||||
|
||||
// Relocate makes sure that the cursor is inside the bounds
|
||||
// of the buffer If it isn't, it moves it to be within the
|
||||
// buffer's lines
|
||||
func (c *Cursor) Relocate() {
|
||||
if c.Y < 0 {
|
||||
c.Y = 0
|
||||
} else if c.Y >= len(c.buf.lines) {
|
||||
c.Y = len(c.buf.lines) - 1
|
||||
}
|
||||
|
||||
if c.X < 0 {
|
||||
c.X = 0
|
||||
} else if c.X > utf8.RuneCount(c.buf.LineBytes(c.Y)) {
|
||||
c.X = utf8.RuneCount(c.buf.LineBytes(c.Y))
|
||||
}
|
||||
}
|
||||
|
||||
// SelectWord selects the word the cursor is currently on
|
||||
func (c *Cursor) SelectWord() {
|
||||
if len(c.buf.LineBytes(c.Y)) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if !util.IsWordChar(c.RuneUnder(c.X)) {
|
||||
c.SetSelectionStart(c.Loc)
|
||||
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
|
||||
c.OrigSelection = c.CurSelection
|
||||
return
|
||||
}
|
||||
|
||||
forward, backward := c.X, c.X
|
||||
|
||||
for backward > 0 && util.IsWordChar(c.RuneUnder(backward-1)) {
|
||||
backward--
|
||||
}
|
||||
|
||||
c.SetSelectionStart(Loc{backward, c.Y})
|
||||
c.OrigSelection[0] = c.CurSelection[0]
|
||||
|
||||
lineLen := utf8.RuneCount(c.buf.LineBytes(c.Y)) - 1
|
||||
for forward < lineLen && util.IsWordChar(c.RuneUnder(forward+1)) {
|
||||
forward++
|
||||
}
|
||||
|
||||
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
|
||||
c.OrigSelection[1] = c.CurSelection[1]
|
||||
c.Loc = c.CurSelection[1]
|
||||
}
|
||||
|
||||
// AddWordToSelection adds the word the cursor is currently on
|
||||
// to the selection
|
||||
func (c *Cursor) AddWordToSelection() {
|
||||
if c.Loc.GreaterThan(c.OrigSelection[0]) && c.Loc.LessThan(c.OrigSelection[1]) {
|
||||
c.CurSelection = c.OrigSelection
|
||||
return
|
||||
}
|
||||
|
||||
if c.Loc.LessThan(c.OrigSelection[0]) {
|
||||
backward := c.X
|
||||
|
||||
for backward > 0 && util.IsWordChar(c.RuneUnder(backward-1)) {
|
||||
backward--
|
||||
}
|
||||
|
||||
c.SetSelectionStart(Loc{backward, c.Y})
|
||||
c.SetSelectionEnd(c.OrigSelection[1])
|
||||
}
|
||||
|
||||
if c.Loc.GreaterThan(c.OrigSelection[1]) {
|
||||
forward := c.X
|
||||
|
||||
lineLen := utf8.RuneCount(c.buf.LineBytes(c.Y)) - 1
|
||||
for forward < lineLen && util.IsWordChar(c.RuneUnder(forward+1)) {
|
||||
forward++
|
||||
}
|
||||
|
||||
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
|
||||
c.SetSelectionStart(c.OrigSelection[0])
|
||||
}
|
||||
|
||||
c.Loc = c.CurSelection[1]
|
||||
}
|
||||
|
||||
// SelectTo selects from the current cursor location to the given
|
||||
// location
|
||||
func (c *Cursor) SelectTo(loc Loc) {
|
||||
if loc.GreaterThan(c.OrigSelection[0]) {
|
||||
c.SetSelectionStart(c.OrigSelection[0])
|
||||
c.SetSelectionEnd(loc)
|
||||
} else {
|
||||
c.SetSelectionStart(loc)
|
||||
c.SetSelectionEnd(c.OrigSelection[0])
|
||||
}
|
||||
}
|
||||
|
||||
// WordRight moves the cursor one word to the right
|
||||
func (c *Cursor) WordRight() {
|
||||
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == utf8.RuneCount(c.buf.LineBytes(c.Y)) {
|
||||
c.Right()
|
||||
return
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
c.Right()
|
||||
for util.IsWordChar(c.RuneUnder(c.X)) {
|
||||
if c.X == utf8.RuneCount(c.buf.LineBytes(c.Y)) {
|
||||
return
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
}
|
||||
|
||||
// WordLeft moves the cursor one word to the left
|
||||
func (c *Cursor) WordLeft() {
|
||||
c.Left()
|
||||
for util.IsWhitespace(c.RuneUnder(c.X)) {
|
||||
if c.X == 0 {
|
||||
return
|
||||
}
|
||||
c.Left()
|
||||
}
|
||||
c.Left()
|
||||
for util.IsWordChar(c.RuneUnder(c.X)) {
|
||||
if c.X == 0 {
|
||||
return
|
||||
}
|
||||
c.Left()
|
||||
}
|
||||
c.Right()
|
||||
}
|
||||
|
||||
// RuneUnder returns the rune under the given x position
|
||||
func (c *Cursor) RuneUnder(x int) rune {
|
||||
line := c.buf.LineBytes(c.Y)
|
||||
if len(line) == 0 || x >= utf8.RuneCount(line) {
|
||||
return '\n'
|
||||
} else if x < 0 {
|
||||
x = 0
|
||||
}
|
||||
i := 0
|
||||
for len(line) > 0 {
|
||||
r, size := utf8.DecodeRune(line)
|
||||
line = line[size:]
|
||||
|
||||
if i == x {
|
||||
return r
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
return '\n'
|
||||
}
|
||||
|
||||
func (c *Cursor) StoreVisualX() {
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
344
internal/buffer/eventhandler.go
Normal file
344
internal/buffer/eventhandler.go
Normal file
@@ -0,0 +1,344 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
dmp "github.com/sergi/go-diff/diffmatchpatch"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
ulua "github.com/zyedidia/micro/internal/lua"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
luar "layeh.com/gopher-luar"
|
||||
)
|
||||
|
||||
const (
|
||||
// Opposite and undoing events must have opposite values
|
||||
|
||||
// TextEventInsert represents an insertion event
|
||||
TextEventInsert = 1
|
||||
// TextEventRemove represents a deletion event
|
||||
TextEventRemove = -1
|
||||
// TextEventReplace represents a replace event
|
||||
TextEventReplace = 0
|
||||
|
||||
undoThreshold = 1000 // If two events are less than n milliseconds apart, undo both of them
|
||||
)
|
||||
|
||||
// TextEvent holds data for a manipulation on some text that can be undone
|
||||
type TextEvent struct {
|
||||
C Cursor
|
||||
|
||||
EventType int
|
||||
Deltas []Delta
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// A Delta is a change to the buffer
|
||||
type Delta struct {
|
||||
Text []byte
|
||||
Start Loc
|
||||
End Loc
|
||||
}
|
||||
|
||||
// DoTextEvent runs a text event
|
||||
func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) {
|
||||
oldl := eh.buf.LinesNum()
|
||||
|
||||
if useUndo {
|
||||
eh.Execute(t)
|
||||
} else {
|
||||
ExecuteTextEvent(t, eh.buf)
|
||||
}
|
||||
|
||||
if len(t.Deltas) != 1 {
|
||||
return
|
||||
}
|
||||
|
||||
text := t.Deltas[0].Text
|
||||
start := t.Deltas[0].Start
|
||||
lastnl := -1
|
||||
var endX int
|
||||
var textX int
|
||||
if t.EventType == TextEventInsert {
|
||||
linecount := eh.buf.LinesNum() - oldl
|
||||
textcount := utf8.RuneCount(text)
|
||||
lastnl = bytes.LastIndex(text, []byte{'\n'})
|
||||
if lastnl >= 0 {
|
||||
endX = utf8.RuneCount(text[lastnl+1:])
|
||||
textX = endX
|
||||
} else {
|
||||
endX = start.X + textcount
|
||||
textX = textcount
|
||||
}
|
||||
t.Deltas[0].End = clamp(Loc{endX, start.Y + linecount}, eh.buf.LineArray)
|
||||
}
|
||||
end := t.Deltas[0].End
|
||||
|
||||
for _, c := range eh.cursors {
|
||||
move := func(loc Loc) Loc {
|
||||
if t.EventType == TextEventInsert {
|
||||
if start.Y != loc.Y && loc.GreaterThan(start) {
|
||||
loc.Y += end.Y - start.Y
|
||||
} else if loc.Y == start.Y && loc.GreaterEqual(start) {
|
||||
loc.Y += end.Y - start.Y
|
||||
if lastnl >= 0 {
|
||||
loc.X += textX - start.X
|
||||
} else {
|
||||
loc.X += textX
|
||||
}
|
||||
}
|
||||
return loc
|
||||
} else {
|
||||
if loc.Y != end.Y && loc.GreaterThan(end) {
|
||||
loc.Y -= end.Y - start.Y
|
||||
} else if loc.Y == end.Y && loc.GreaterEqual(end) {
|
||||
loc = loc.MoveLA(-DiffLA(start, end, eh.buf.LineArray), eh.buf.LineArray)
|
||||
}
|
||||
return loc
|
||||
}
|
||||
}
|
||||
c.Loc = move(c.Loc)
|
||||
c.CurSelection[0] = move(c.CurSelection[0])
|
||||
c.CurSelection[1] = move(c.CurSelection[1])
|
||||
c.OrigSelection[0] = move(c.OrigSelection[0])
|
||||
c.OrigSelection[1] = move(c.OrigSelection[1])
|
||||
c.Relocate()
|
||||
c.LastVisualX = c.GetVisualX()
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteTextEvent runs a text event
|
||||
func ExecuteTextEvent(t *TextEvent, buf *SharedBuffer) {
|
||||
if t.EventType == TextEventInsert {
|
||||
for _, d := range t.Deltas {
|
||||
buf.insert(d.Start, d.Text)
|
||||
}
|
||||
} else if t.EventType == TextEventRemove {
|
||||
for i, d := range t.Deltas {
|
||||
t.Deltas[i].Text = buf.remove(d.Start, d.End)
|
||||
}
|
||||
} else if t.EventType == TextEventReplace {
|
||||
for i, d := range t.Deltas {
|
||||
t.Deltas[i].Text = buf.remove(d.Start, d.End)
|
||||
buf.insert(d.Start, d.Text)
|
||||
t.Deltas[i].Start = d.Start
|
||||
t.Deltas[i].End = Loc{d.Start.X + utf8.RuneCount(d.Text), d.Start.Y}
|
||||
}
|
||||
for i, j := 0, len(t.Deltas)-1; i < j; i, j = i+1, j-1 {
|
||||
t.Deltas[i], t.Deltas[j] = t.Deltas[j], t.Deltas[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UndoTextEvent undoes a text event
|
||||
func (eh *EventHandler) UndoTextEvent(t *TextEvent) {
|
||||
t.EventType = -t.EventType
|
||||
eh.DoTextEvent(t, false)
|
||||
}
|
||||
|
||||
// EventHandler executes text manipulations and allows undoing and redoing
|
||||
type EventHandler struct {
|
||||
buf *SharedBuffer
|
||||
cursors []*Cursor
|
||||
active int
|
||||
UndoStack *TEStack
|
||||
RedoStack *TEStack
|
||||
}
|
||||
|
||||
// NewEventHandler returns a new EventHandler
|
||||
func NewEventHandler(buf *SharedBuffer, cursors []*Cursor) *EventHandler {
|
||||
eh := new(EventHandler)
|
||||
eh.UndoStack = new(TEStack)
|
||||
eh.RedoStack = new(TEStack)
|
||||
eh.buf = buf
|
||||
eh.cursors = cursors
|
||||
return eh
|
||||
}
|
||||
|
||||
// ApplyDiff takes a string and runs the necessary insertion and deletion events to make
|
||||
// the buffer equal to that string
|
||||
// This means that we can transform the buffer into any string and still preserve undo/redo
|
||||
// through insert and delete events
|
||||
func (eh *EventHandler) ApplyDiff(new string) {
|
||||
differ := dmp.New()
|
||||
diff := differ.DiffMain(string(eh.buf.Bytes()), new, false)
|
||||
loc := eh.buf.Start()
|
||||
for _, d := range diff {
|
||||
if d.Type == dmp.DiffDelete {
|
||||
eh.Remove(loc, loc.MoveLA(utf8.RuneCountInString(d.Text), eh.buf.LineArray))
|
||||
} else {
|
||||
if d.Type == dmp.DiffInsert {
|
||||
eh.Insert(loc, d.Text)
|
||||
}
|
||||
loc = loc.MoveLA(utf8.RuneCountInString(d.Text), eh.buf.LineArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert creates an insert text event and executes it
|
||||
func (eh *EventHandler) Insert(start Loc, textStr string) {
|
||||
text := []byte(textStr)
|
||||
eh.InsertBytes(start, text)
|
||||
}
|
||||
|
||||
// InsertBytes creates an insert text event and executes it
|
||||
func (eh *EventHandler) InsertBytes(start Loc, text []byte) {
|
||||
if len(text) == 0 {
|
||||
return
|
||||
}
|
||||
start = clamp(start, eh.buf.LineArray)
|
||||
e := &TextEvent{
|
||||
C: *eh.cursors[eh.active],
|
||||
EventType: TextEventInsert,
|
||||
Deltas: []Delta{{text, start, Loc{0, 0}}},
|
||||
Time: time.Now(),
|
||||
}
|
||||
eh.DoTextEvent(e, true)
|
||||
}
|
||||
|
||||
// Remove creates a remove text event and executes it
|
||||
func (eh *EventHandler) Remove(start, end Loc) {
|
||||
if start == end {
|
||||
return
|
||||
}
|
||||
start = clamp(start, eh.buf.LineArray)
|
||||
end = clamp(end, eh.buf.LineArray)
|
||||
e := &TextEvent{
|
||||
C: *eh.cursors[eh.active],
|
||||
EventType: TextEventRemove,
|
||||
Deltas: []Delta{{[]byte{}, start, end}},
|
||||
Time: time.Now(),
|
||||
}
|
||||
eh.DoTextEvent(e, true)
|
||||
}
|
||||
|
||||
// MultipleReplace creates an multiple insertions executes them
|
||||
func (eh *EventHandler) MultipleReplace(deltas []Delta) {
|
||||
e := &TextEvent{
|
||||
C: *eh.cursors[eh.active],
|
||||
EventType: TextEventReplace,
|
||||
Deltas: deltas,
|
||||
Time: time.Now(),
|
||||
}
|
||||
eh.Execute(e)
|
||||
}
|
||||
|
||||
// Replace deletes from start to end and replaces it with the given string
|
||||
func (eh *EventHandler) Replace(start, end Loc, replace string) {
|
||||
eh.Remove(start, end)
|
||||
eh.Insert(start, replace)
|
||||
}
|
||||
|
||||
// Execute a textevent and add it to the undo stack
|
||||
func (eh *EventHandler) Execute(t *TextEvent) {
|
||||
if eh.RedoStack.Len() > 0 {
|
||||
eh.RedoStack = new(TEStack)
|
||||
}
|
||||
eh.UndoStack.Push(t)
|
||||
|
||||
b, err := config.RunPluginFnBool("onBeforeTextEvent", luar.New(ulua.L, eh.buf), luar.New(ulua.L, t))
|
||||
if err != nil {
|
||||
screen.TermMessage(err)
|
||||
}
|
||||
|
||||
if !b {
|
||||
return
|
||||
}
|
||||
|
||||
ExecuteTextEvent(t, eh.buf)
|
||||
}
|
||||
|
||||
// Undo the first event in the undo stack
|
||||
func (eh *EventHandler) Undo() {
|
||||
t := eh.UndoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
||||
endTime := startTime - (startTime % undoThreshold)
|
||||
|
||||
for {
|
||||
t = eh.UndoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if t.Time.UnixNano()/int64(time.Millisecond) < endTime {
|
||||
return
|
||||
}
|
||||
|
||||
eh.UndoOneEvent()
|
||||
}
|
||||
}
|
||||
|
||||
// UndoOneEvent undoes one event
|
||||
func (eh *EventHandler) UndoOneEvent() {
|
||||
// This event should be undone
|
||||
// Pop it off the stack
|
||||
t := eh.UndoStack.Pop()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
// Undo it
|
||||
// Modifies the text event
|
||||
eh.UndoTextEvent(t)
|
||||
|
||||
// Set the cursor in the right place
|
||||
teCursor := t.C
|
||||
if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) {
|
||||
t.C = *eh.cursors[teCursor.Num]
|
||||
eh.cursors[teCursor.Num].Goto(teCursor)
|
||||
} else {
|
||||
teCursor.Num = -1
|
||||
}
|
||||
|
||||
// Push it to the redo stack
|
||||
eh.RedoStack.Push(t)
|
||||
}
|
||||
|
||||
// Redo the first event in the redo stack
|
||||
func (eh *EventHandler) Redo() {
|
||||
t := eh.RedoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
startTime := t.Time.UnixNano() / int64(time.Millisecond)
|
||||
endTime := startTime - (startTime % undoThreshold) + undoThreshold
|
||||
|
||||
for {
|
||||
t = eh.RedoStack.Peek()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if t.Time.UnixNano()/int64(time.Millisecond) > endTime {
|
||||
return
|
||||
}
|
||||
|
||||
eh.RedoOneEvent()
|
||||
}
|
||||
}
|
||||
|
||||
// RedoOneEvent redoes one event
|
||||
func (eh *EventHandler) RedoOneEvent() {
|
||||
t := eh.RedoStack.Pop()
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
|
||||
teCursor := t.C
|
||||
if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) {
|
||||
t.C = *eh.cursors[teCursor.Num]
|
||||
eh.cursors[teCursor.Num].Goto(teCursor)
|
||||
} else {
|
||||
teCursor.Num = -1
|
||||
}
|
||||
|
||||
// Modifies the text event
|
||||
eh.UndoTextEvent(t)
|
||||
|
||||
eh.UndoStack.Push(t)
|
||||
}
|
||||
351
internal/buffer/line_array.go
Normal file
351
internal/buffer/line_array.go
Normal file
@@ -0,0 +1,351 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/pkg/highlight"
|
||||
)
|
||||
|
||||
// Finds the byte index of the nth rune in a byte slice
|
||||
func runeToByteIndex(n int, txt []byte) int {
|
||||
if n == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
count := 0
|
||||
i := 0
|
||||
for len(txt) > 0 {
|
||||
_, size := utf8.DecodeRune(txt)
|
||||
|
||||
txt = txt[size:]
|
||||
count += size
|
||||
i++
|
||||
|
||||
if i == n {
|
||||
break
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// A Line contains the data in bytes as well as a highlight state, match
|
||||
// and a flag for whether the highlighting needs to be updated
|
||||
type Line struct {
|
||||
data []byte
|
||||
|
||||
state highlight.State
|
||||
match highlight.LineMatch
|
||||
rehighlight bool
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
const (
|
||||
// Line ending file formats
|
||||
FFAuto = 0 // Autodetect format
|
||||
FFUnix = 1 // LF line endings (unix style '\n')
|
||||
FFDos = 2 // CRLF line endings (dos style '\r\n')
|
||||
)
|
||||
|
||||
type FileFormat byte
|
||||
|
||||
// A LineArray simply stores and array of lines and makes it easy to insert
|
||||
// and delete in it
|
||||
type LineArray struct {
|
||||
lines []Line
|
||||
Endings FileFormat
|
||||
initsize uint64
|
||||
}
|
||||
|
||||
// Append efficiently appends lines together
|
||||
// It allocates an additional 10000 lines if the original estimate
|
||||
// is incorrect
|
||||
func Append(slice []Line, data ...Line) []Line {
|
||||
l := len(slice)
|
||||
if l+len(data) > cap(slice) { // reallocate
|
||||
newSlice := make([]Line, (l+len(data))+10000)
|
||||
copy(newSlice, slice)
|
||||
slice = newSlice
|
||||
}
|
||||
slice = slice[0 : l+len(data)]
|
||||
for i, c := range data {
|
||||
slice[l+i] = c
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
// NewLineArray returns a new line array from an array of bytes
|
||||
func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray {
|
||||
la := new(LineArray)
|
||||
|
||||
la.lines = make([]Line, 0, 1000)
|
||||
la.initsize = size
|
||||
|
||||
br := bufio.NewReader(reader)
|
||||
var loaded int
|
||||
|
||||
n := 0
|
||||
for {
|
||||
data, err := br.ReadBytes('\n')
|
||||
// Detect the line ending by checking to see if there is a '\r' char
|
||||
// before the '\n'
|
||||
// Even if the file format is set to DOS, the '\r' is removed so
|
||||
// that all lines end with '\n'
|
||||
dlen := len(data)
|
||||
if dlen > 1 && data[dlen-2] == '\r' {
|
||||
data = append(data[:dlen-2], '\n')
|
||||
if endings == FFAuto {
|
||||
la.Endings = FFDos
|
||||
}
|
||||
dlen = len(data)
|
||||
} else if dlen > 0 {
|
||||
if endings == FFAuto {
|
||||
la.Endings = FFUnix
|
||||
}
|
||||
}
|
||||
|
||||
// If we are loading a large file (greater than 1000) we use the file
|
||||
// size and the length of the first 1000 lines to try to estimate
|
||||
// how many lines will need to be allocated for the rest of the file
|
||||
// We add an extra 10000 to the original estimate to be safe and give
|
||||
// plenty of room for expansion
|
||||
if n >= 1000 && loaded >= 0 {
|
||||
totalLinesNum := int(float64(size) * (float64(n) / float64(loaded)))
|
||||
newSlice := make([]Line, len(la.lines), totalLinesNum+10000)
|
||||
copy(newSlice, la.lines)
|
||||
la.lines = newSlice
|
||||
loaded = -1
|
||||
}
|
||||
|
||||
// Counter for the number of bytes in the first 1000 lines
|
||||
if loaded >= 0 {
|
||||
loaded += dlen
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
la.lines = Append(la.lines, Line{
|
||||
data: data[:],
|
||||
state: nil,
|
||||
match: nil,
|
||||
rehighlight: false,
|
||||
})
|
||||
}
|
||||
// Last line was read
|
||||
break
|
||||
} else {
|
||||
la.lines = Append(la.lines, Line{
|
||||
data: data[:dlen-1],
|
||||
state: nil,
|
||||
match: nil,
|
||||
rehighlight: false,
|
||||
})
|
||||
}
|
||||
n++
|
||||
}
|
||||
|
||||
return la
|
||||
}
|
||||
|
||||
// Bytes returns the string that should be written to disk when
|
||||
// the line array is saved
|
||||
func (la *LineArray) Bytes() []byte {
|
||||
b := new(bytes.Buffer)
|
||||
// initsize should provide a good estimate
|
||||
b.Grow(int(la.initsize + 4096))
|
||||
for i, l := range la.lines {
|
||||
b.Write(l.data)
|
||||
if i != len(la.lines)-1 {
|
||||
if la.Endings == FFDos {
|
||||
b.WriteByte('\r')
|
||||
}
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
// newlineBelow adds a newline below the given line number
|
||||
func (la *LineArray) newlineBelow(y int) {
|
||||
la.lines = append(la.lines, Line{
|
||||
data: []byte{' '},
|
||||
state: nil,
|
||||
match: nil,
|
||||
rehighlight: false,
|
||||
})
|
||||
copy(la.lines[y+2:], la.lines[y+1:])
|
||||
la.lines[y+1] = Line{
|
||||
data: []byte{},
|
||||
state: la.lines[y].state,
|
||||
match: nil,
|
||||
rehighlight: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Inserts a byte array at a given location
|
||||
func (la *LineArray) insert(pos Loc, value []byte) {
|
||||
x, y := runeToByteIndex(pos.X, la.lines[pos.Y].data), pos.Y
|
||||
for i := 0; i < len(value); i++ {
|
||||
if value[i] == '\n' {
|
||||
la.split(Loc{x, y})
|
||||
x = 0
|
||||
y++
|
||||
continue
|
||||
}
|
||||
la.insertByte(Loc{x, y}, value[i])
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
// InsertByte inserts a byte at a given location
|
||||
func (la *LineArray) insertByte(pos Loc, value byte) {
|
||||
la.lines[pos.Y].data = append(la.lines[pos.Y].data, 0)
|
||||
copy(la.lines[pos.Y].data[pos.X+1:], la.lines[pos.Y].data[pos.X:])
|
||||
la.lines[pos.Y].data[pos.X] = value
|
||||
}
|
||||
|
||||
// joinLines joins the two lines a and b
|
||||
func (la *LineArray) joinLines(a, b int) {
|
||||
la.insert(Loc{len(la.lines[a].data), a}, la.lines[b].data)
|
||||
la.deleteLine(b)
|
||||
}
|
||||
|
||||
// split splits a line at a given position
|
||||
func (la *LineArray) split(pos Loc) {
|
||||
la.newlineBelow(pos.Y)
|
||||
la.insert(Loc{0, pos.Y + 1}, la.lines[pos.Y].data[pos.X:])
|
||||
la.lines[pos.Y+1].state = la.lines[pos.Y].state
|
||||
la.lines[pos.Y].state = nil
|
||||
la.lines[pos.Y].match = nil
|
||||
la.lines[pos.Y+1].match = nil
|
||||
la.lines[pos.Y].rehighlight = true
|
||||
la.deleteToEnd(Loc{pos.X, pos.Y})
|
||||
}
|
||||
|
||||
// removes from start to end
|
||||
func (la *LineArray) remove(start, end Loc) []byte {
|
||||
sub := la.Substr(start, end)
|
||||
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
|
||||
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
|
||||
if start.Y == end.Y {
|
||||
la.lines[start.Y].data = append(la.lines[start.Y].data[:startX], la.lines[start.Y].data[endX:]...)
|
||||
} else {
|
||||
la.deleteLines(start.Y+1, end.Y-1)
|
||||
la.deleteToEnd(Loc{startX, start.Y})
|
||||
la.deleteFromStart(Loc{endX - 1, start.Y + 1})
|
||||
la.joinLines(start.Y, start.Y+1)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
// deleteToEnd deletes from the end of a line to the position
|
||||
func (la *LineArray) deleteToEnd(pos Loc) {
|
||||
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X]
|
||||
}
|
||||
|
||||
// deleteFromStart deletes from the start of a line to the position
|
||||
func (la *LineArray) deleteFromStart(pos Loc) {
|
||||
la.lines[pos.Y].data = la.lines[pos.Y].data[pos.X+1:]
|
||||
}
|
||||
|
||||
// deleteLine deletes the line number
|
||||
func (la *LineArray) deleteLine(y int) {
|
||||
la.lines = la.lines[:y+copy(la.lines[y:], la.lines[y+1:])]
|
||||
}
|
||||
|
||||
func (la *LineArray) deleteLines(y1, y2 int) {
|
||||
la.lines = la.lines[:y1+copy(la.lines[y1:], la.lines[y2+1:])]
|
||||
}
|
||||
|
||||
// DeleteByte deletes the byte at a position
|
||||
func (la *LineArray) deleteByte(pos Loc) {
|
||||
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X+copy(la.lines[pos.Y].data[pos.X:], la.lines[pos.Y].data[pos.X+1:])]
|
||||
}
|
||||
|
||||
// Substr returns the string representation between two locations
|
||||
func (la *LineArray) Substr(start, end Loc) []byte {
|
||||
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
|
||||
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
|
||||
if start.Y == end.Y {
|
||||
src := la.lines[start.Y].data[startX:endX]
|
||||
dest := make([]byte, len(src))
|
||||
copy(dest, src)
|
||||
return dest
|
||||
}
|
||||
str := make([]byte, 0, len(la.lines[start.Y+1].data)*(end.Y-start.Y))
|
||||
str = append(str, la.lines[start.Y].data[startX:]...)
|
||||
str = append(str, '\n')
|
||||
for i := start.Y + 1; i <= end.Y-1; i++ {
|
||||
str = append(str, la.lines[i].data...)
|
||||
str = append(str, '\n')
|
||||
}
|
||||
str = append(str, la.lines[end.Y].data[:endX]...)
|
||||
return str
|
||||
}
|
||||
|
||||
// LinesNum returns the number of lines in the buffer
|
||||
func (la *LineArray) LinesNum() int {
|
||||
return len(la.lines)
|
||||
}
|
||||
|
||||
// Start returns the start of the buffer
|
||||
func (la *LineArray) Start() Loc {
|
||||
return Loc{0, 0}
|
||||
}
|
||||
|
||||
// End returns the location of the last character in the buffer
|
||||
func (la *LineArray) End() Loc {
|
||||
numlines := len(la.lines)
|
||||
return Loc{utf8.RuneCount(la.lines[numlines-1].data), numlines - 1}
|
||||
}
|
||||
|
||||
// LineBytes returns line n as an array of bytes
|
||||
func (la *LineArray) LineBytes(n int) []byte {
|
||||
if n >= len(la.lines) || n < 0 {
|
||||
return []byte{}
|
||||
}
|
||||
return la.lines[n].data
|
||||
}
|
||||
|
||||
// State gets the highlight state for the given line number
|
||||
func (la *LineArray) State(lineN int) highlight.State {
|
||||
la.lines[lineN].lock.Lock()
|
||||
defer la.lines[lineN].lock.Unlock()
|
||||
return la.lines[lineN].state
|
||||
}
|
||||
|
||||
// SetState sets the highlight state at the given line number
|
||||
func (la *LineArray) SetState(lineN int, s highlight.State) {
|
||||
la.lines[lineN].lock.Lock()
|
||||
defer la.lines[lineN].lock.Unlock()
|
||||
la.lines[lineN].state = s
|
||||
}
|
||||
|
||||
// SetMatch sets the match at the given line number
|
||||
func (la *LineArray) SetMatch(lineN int, m highlight.LineMatch) {
|
||||
la.lines[lineN].lock.Lock()
|
||||
defer la.lines[lineN].lock.Unlock()
|
||||
la.lines[lineN].match = m
|
||||
}
|
||||
|
||||
// Match retrieves the match for the given line number
|
||||
func (la *LineArray) Match(lineN int) highlight.LineMatch {
|
||||
la.lines[lineN].lock.Lock()
|
||||
defer la.lines[lineN].lock.Unlock()
|
||||
return la.lines[lineN].match
|
||||
}
|
||||
|
||||
func (la *LineArray) Rehighlight(lineN int) bool {
|
||||
la.lines[lineN].lock.Lock()
|
||||
defer la.lines[lineN].lock.Unlock()
|
||||
return la.lines[lineN].rehighlight
|
||||
}
|
||||
|
||||
func (la *LineArray) SetRehighlight(lineN int, on bool) {
|
||||
la.lines[lineN].lock.Lock()
|
||||
defer la.lines[lineN].lock.Unlock()
|
||||
la.lines[lineN].rehighlight = on
|
||||
}
|
||||
60
internal/buffer/line_array_test.go
Normal file
60
internal/buffer/line_array_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var unicode_txt = `An preost wes on leoden, Laȝamon was ihoten
|
||||
He wes Leovenaðes sone -- liðe him be Drihten.
|
||||
He wonede at Ernleȝe at æðelen are chirechen,
|
||||
Uppen Sevarne staþe, sel þar him þuhte,
|
||||
Onfest Radestone, þer he bock radde.`
|
||||
|
||||
var la *LineArray
|
||||
|
||||
func init() {
|
||||
reader := strings.NewReader(unicode_txt)
|
||||
la = NewLineArray(uint64(len(unicode_txt)), FFAuto, reader)
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
la.insert(Loc{17, 1}, []byte{'\n'})
|
||||
assert.Equal(t, len(la.lines), 6)
|
||||
sub1 := la.Substr(Loc{0, 1}, Loc{17, 1})
|
||||
sub2 := la.Substr(Loc{0, 2}, Loc{30, 2})
|
||||
|
||||
assert.Equal(t, []byte("He wes Leovenaðes"), sub1)
|
||||
assert.Equal(t, []byte(" sone -- liðe him be Drihten."), sub2)
|
||||
}
|
||||
|
||||
func TestJoin(t *testing.T) {
|
||||
la.remove(Loc{47, 1}, Loc{0, 2})
|
||||
assert.Equal(t, len(la.lines), 5)
|
||||
sub := la.Substr(Loc{0, 1}, Loc{47, 1})
|
||||
bytes := la.Bytes()
|
||||
|
||||
assert.Equal(t, []byte("He wes Leovenaðes sone -- liðe him be Drihten."), sub)
|
||||
assert.Equal(t, unicode_txt, string(bytes))
|
||||
}
|
||||
|
||||
func TestInsert(t *testing.T) {
|
||||
la.insert(Loc{20, 3}, []byte(" foobar"))
|
||||
sub1 := la.Substr(Loc{0, 3}, Loc{50, 3})
|
||||
|
||||
assert.Equal(t, []byte("Uppen Sevarne staþe, foobar sel þar him þuhte,"), sub1)
|
||||
|
||||
la.insert(Loc{25, 2}, []byte("ಮಣ್ಣಾಗಿ"))
|
||||
sub2 := la.Substr(Loc{0, 2}, Loc{60, 2})
|
||||
assert.Equal(t, []byte("He wonede at Ernleȝe at æಮಣ್ಣಾಗಿðelen are chirechen,"), sub2)
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
la.remove(Loc{20, 3}, Loc{27, 3})
|
||||
la.remove(Loc{25, 2}, Loc{32, 2})
|
||||
|
||||
bytes := la.Bytes()
|
||||
assert.Equal(t, unicode_txt, string(bytes))
|
||||
}
|
||||
150
internal/buffer/loc.go
Normal file
150
internal/buffer/loc.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// Loc stores a location
|
||||
type Loc struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
// LessThan returns true if b is smaller
|
||||
func (l Loc) LessThan(b Loc) bool {
|
||||
if l.Y < b.Y {
|
||||
return true
|
||||
}
|
||||
return l.Y == b.Y && l.X < b.X
|
||||
}
|
||||
|
||||
// GreaterThan returns true if b is bigger
|
||||
func (l Loc) GreaterThan(b Loc) bool {
|
||||
if l.Y > b.Y {
|
||||
return true
|
||||
}
|
||||
return l.Y == b.Y && l.X > b.X
|
||||
}
|
||||
|
||||
// GreaterEqual returns true if b is greater than or equal to b
|
||||
func (l Loc) GreaterEqual(b Loc) bool {
|
||||
if l.Y > b.Y {
|
||||
return true
|
||||
}
|
||||
if l.Y == b.Y && l.X > b.X {
|
||||
return true
|
||||
}
|
||||
return l == b
|
||||
}
|
||||
|
||||
// LessEqual returns true if b is less than or equal to b
|
||||
func (l Loc) LessEqual(b Loc) bool {
|
||||
if l.Y < b.Y {
|
||||
return true
|
||||
}
|
||||
if l.Y == b.Y && l.X < b.X {
|
||||
return true
|
||||
}
|
||||
return l == b
|
||||
}
|
||||
|
||||
// The following functions require a buffer to know where newlines are
|
||||
|
||||
// Diff returns the distance between two locations
|
||||
func DiffLA(a, b Loc, buf *LineArray) int {
|
||||
if a.Y == b.Y {
|
||||
if a.X > b.X {
|
||||
return a.X - b.X
|
||||
}
|
||||
return b.X - a.X
|
||||
}
|
||||
|
||||
// Make sure a is guaranteed to be less than b
|
||||
if b.LessThan(a) {
|
||||
a, b = b, a
|
||||
}
|
||||
|
||||
loc := 0
|
||||
for i := a.Y + 1; i < b.Y; i++ {
|
||||
// + 1 for the newline
|
||||
loc += utf8.RuneCount(buf.LineBytes(i)) + 1
|
||||
}
|
||||
loc += utf8.RuneCount(buf.LineBytes(a.Y)) - a.X + b.X + 1
|
||||
return loc
|
||||
}
|
||||
|
||||
// This moves the location one character to the right
|
||||
func (l Loc) right(buf *LineArray) Loc {
|
||||
if l == buf.End() {
|
||||
return Loc{l.X + 1, l.Y}
|
||||
}
|
||||
var res Loc
|
||||
if l.X < utf8.RuneCount(buf.LineBytes(l.Y)) {
|
||||
res = Loc{l.X + 1, l.Y}
|
||||
} else {
|
||||
res = Loc{0, l.Y + 1}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// This moves the given location one character to the left
|
||||
func (l Loc) left(buf *LineArray) Loc {
|
||||
if l == buf.Start() {
|
||||
return Loc{l.X - 1, l.Y}
|
||||
}
|
||||
var res Loc
|
||||
if l.X > 0 {
|
||||
res = Loc{l.X - 1, l.Y}
|
||||
} else {
|
||||
res = Loc{utf8.RuneCount(buf.LineBytes(l.Y - 1)), l.Y - 1}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// MoveLA moves the cursor n characters to the left or right
|
||||
// It moves the cursor left if n is negative
|
||||
func (l Loc) MoveLA(n int, buf *LineArray) Loc {
|
||||
if n > 0 {
|
||||
for i := 0; i < n; i++ {
|
||||
l = l.right(buf)
|
||||
}
|
||||
return l
|
||||
}
|
||||
for i := 0; i < util.Abs(n); i++ {
|
||||
l = l.left(buf)
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// Diff returns the difference between two locs
|
||||
func (l Loc) Diff(b Loc, buf *Buffer) int {
|
||||
return DiffLA(l, b, buf.LineArray)
|
||||
}
|
||||
|
||||
// Move moves a loc n characters
|
||||
func (l Loc) Move(n int, buf *Buffer) Loc {
|
||||
return l.MoveLA(n, buf.LineArray)
|
||||
}
|
||||
|
||||
// ByteOffset is just like ToCharPos except it counts bytes instead of runes
|
||||
func ByteOffset(pos Loc, buf *Buffer) int {
|
||||
x, y := pos.X, pos.Y
|
||||
loc := 0
|
||||
for i := 0; i < y; i++ {
|
||||
// + 1 for the newline
|
||||
loc += len(buf.Line(i)) + 1
|
||||
}
|
||||
loc += len(buf.Line(y)[:x])
|
||||
return loc
|
||||
}
|
||||
|
||||
// clamps a loc within a buffer
|
||||
func clamp(pos Loc, la *LineArray) Loc {
|
||||
if pos.GreaterEqual(la.End()) {
|
||||
return la.End()
|
||||
} else if pos.LessThan(la.Start()) {
|
||||
return la.Start()
|
||||
}
|
||||
return pos
|
||||
}
|
||||
84
internal/buffer/message.go
Normal file
84
internal/buffer/message.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
type MsgType int
|
||||
|
||||
const (
|
||||
MTInfo = iota
|
||||
MTWarning
|
||||
MTError
|
||||
)
|
||||
|
||||
// Message represents the information for a gutter message
|
||||
type Message struct {
|
||||
// The Msg iteslf
|
||||
Msg string
|
||||
// Start and End locations for the message
|
||||
Start, End Loc
|
||||
// The Kind stores the message type
|
||||
Kind MsgType
|
||||
// The Owner of the message
|
||||
Owner string
|
||||
}
|
||||
|
||||
// NewMessage creates a new gutter message
|
||||
func NewMessage(owner string, msg string, start, end Loc, kind MsgType) *Message {
|
||||
return &Message{
|
||||
Msg: msg,
|
||||
Start: start,
|
||||
End: end,
|
||||
Kind: kind,
|
||||
Owner: owner,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMessageAtLine creates a new gutter message at a given line
|
||||
func NewMessageAtLine(owner string, msg string, line int, kind MsgType) *Message {
|
||||
start := Loc{-1, line - 1}
|
||||
end := start
|
||||
return NewMessage(owner, msg, start, end, kind)
|
||||
}
|
||||
|
||||
func (m *Message) Style() tcell.Style {
|
||||
switch m.Kind {
|
||||
case MTInfo:
|
||||
if style, ok := config.Colorscheme["gutter-info"]; ok {
|
||||
return style
|
||||
}
|
||||
case MTWarning:
|
||||
if style, ok := config.Colorscheme["gutter-warning"]; ok {
|
||||
return style
|
||||
}
|
||||
case MTError:
|
||||
if style, ok := config.Colorscheme["gutter-error"]; ok {
|
||||
return style
|
||||
}
|
||||
}
|
||||
return config.DefStyle
|
||||
}
|
||||
|
||||
func (b *Buffer) AddMessage(m *Message) {
|
||||
b.Messages = append(b.Messages, m)
|
||||
}
|
||||
|
||||
func (b *Buffer) removeMsg(i int) {
|
||||
copy(b.Messages[i:], b.Messages[i+1:])
|
||||
b.Messages[len(b.Messages)-1] = nil
|
||||
b.Messages = b.Messages[:len(b.Messages)-1]
|
||||
}
|
||||
|
||||
func (b *Buffer) ClearMessages(owner string) {
|
||||
for i := len(b.Messages) - 1; i >= 0; i-- {
|
||||
if b.Messages[i].Owner == owner {
|
||||
b.removeMsg(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Buffer) ClearAllMessages() {
|
||||
b.Messages = make([]*Message, 0)
|
||||
}
|
||||
200
internal/buffer/save.go
Normal file
200
internal/buffer/save.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
"golang.org/x/text/encoding"
|
||||
"golang.org/x/text/encoding/htmlindex"
|
||||
"golang.org/x/text/transform"
|
||||
)
|
||||
|
||||
// LargeFileThreshold is the number of bytes when fastdirty is forced
|
||||
// because hashing is too slow
|
||||
const LargeFileThreshold = 50000
|
||||
|
||||
// overwriteFile opens the given file for writing, truncating if one exists, and then calls
|
||||
// the supplied function with the file as io.Writer object, also making sure the file is
|
||||
// closed afterwards.
|
||||
func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, withSudo bool) (err error) {
|
||||
var writeCloser io.WriteCloser
|
||||
|
||||
if withSudo {
|
||||
cmd := exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name)
|
||||
|
||||
if writeCloser, err = cmd.StdinPipe(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
<-c
|
||||
cmd.Process.Kill()
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
screenb := screen.TempFini()
|
||||
if e := cmd.Run(); e != nil && err == nil {
|
||||
err = e
|
||||
}
|
||||
screen.TempStart(screenb)
|
||||
}()
|
||||
} else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
w := bufio.NewWriter(transform.NewWriter(writeCloser, enc.NewEncoder()))
|
||||
err = fn(w)
|
||||
w.Flush()
|
||||
|
||||
if e := writeCloser.Close(); e != nil && err == nil {
|
||||
err = e
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Save saves the buffer to its default path
|
||||
func (b *Buffer) Save() error {
|
||||
return b.SaveAs(b.Path)
|
||||
}
|
||||
|
||||
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
|
||||
func (b *Buffer) SaveAs(filename string) error {
|
||||
return b.saveToFile(filename, false)
|
||||
}
|
||||
|
||||
func (b *Buffer) SaveWithSudo() error {
|
||||
return b.SaveAsWithSudo(b.Path)
|
||||
}
|
||||
|
||||
func (b *Buffer) SaveAsWithSudo(filename string) error {
|
||||
return b.saveToFile(filename, true)
|
||||
}
|
||||
|
||||
func (b *Buffer) saveToFile(filename string, withSudo bool) error {
|
||||
var err error
|
||||
if b.Type.Readonly {
|
||||
return errors.New("Cannot save readonly buffer")
|
||||
}
|
||||
if b.Type.Scratch {
|
||||
return errors.New("Cannot save scratch buffer")
|
||||
}
|
||||
if withSudo && runtime.GOOS == "windows" {
|
||||
return errors.New("Save with sudo not supported on Windows")
|
||||
}
|
||||
|
||||
b.UpdateRules()
|
||||
if b.Settings["rmtrailingws"].(bool) {
|
||||
for i, l := range b.lines {
|
||||
leftover := utf8.RuneCount(bytes.TrimRightFunc(l.data, unicode.IsSpace))
|
||||
|
||||
linelen := utf8.RuneCount(l.data)
|
||||
b.Remove(Loc{leftover, i}, Loc{linelen, i})
|
||||
}
|
||||
|
||||
b.RelocateCursors()
|
||||
}
|
||||
|
||||
if b.Settings["eofnewline"].(bool) {
|
||||
end := b.End()
|
||||
if b.RuneAt(Loc{end.X, end.Y}) != '\n' {
|
||||
b.insert(end, []byte{'\n'})
|
||||
}
|
||||
}
|
||||
|
||||
// Update the last time this file was updated after saving
|
||||
defer func() {
|
||||
b.ModTime, _ = util.GetModTime(filename)
|
||||
err = b.Serialize()
|
||||
}()
|
||||
|
||||
// Removes any tilde and replaces with the absolute path to home
|
||||
absFilename, _ := util.ReplaceHome(filename)
|
||||
|
||||
// Get the leading path to the file | "." is returned if there's no leading path provided
|
||||
if dirname := filepath.Dir(absFilename); dirname != "." {
|
||||
// Check if the parent dirs don't exist
|
||||
if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) {
|
||||
// Prompt to make sure they want to create the dirs that are missing
|
||||
if b.Settings["mkparents"].(bool) {
|
||||
// Create all leading dir(s) since they don't exist
|
||||
if mkdirallErr := os.MkdirAll(dirname, os.ModePerm); mkdirallErr != nil {
|
||||
// If there was an error creating the dirs
|
||||
return mkdirallErr
|
||||
}
|
||||
} else {
|
||||
return errors.New("Parent dirs don't exist, enable 'mkparents' for auto creation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fileSize int
|
||||
|
||||
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fwriter := func(file io.Writer) (e error) {
|
||||
if len(b.lines) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// end of line
|
||||
var eol []byte
|
||||
if b.Endings == FFDos {
|
||||
eol = []byte{'\r', '\n'}
|
||||
} else {
|
||||
eol = []byte{'\n'}
|
||||
}
|
||||
|
||||
// write lines
|
||||
if fileSize, e = file.Write(b.lines[0].data); e != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, l := range b.lines[1:] {
|
||||
if _, e = file.Write(eol); e != nil {
|
||||
return
|
||||
}
|
||||
if _, e = file.Write(l.data); e != nil {
|
||||
return
|
||||
}
|
||||
fileSize += len(eol) + len(l.data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = overwriteFile(absFilename, enc, fwriter, withSudo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !b.Settings["fastdirty"].(bool) {
|
||||
if fileSize > LargeFileThreshold {
|
||||
// For large files 'fastdirty' needs to be on
|
||||
b.Settings["fastdirty"] = true
|
||||
} else {
|
||||
calcHash(b, &b.origHash)
|
||||
}
|
||||
}
|
||||
|
||||
b.Path = filename
|
||||
absPath, _ := filepath.Abs(filename)
|
||||
b.AbsPath = absPath
|
||||
b.isModified = false
|
||||
return err
|
||||
}
|
||||
178
internal/buffer/search.go
Normal file
178
internal/buffer/search.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
|
||||
start.Y = util.Clamp(start.Y, 0, b.LinesNum()-1)
|
||||
end.Y = util.Clamp(end.Y, 0, b.LinesNum()-1)
|
||||
|
||||
if start.GreaterThan(end) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
for i := start.Y; i <= end.Y; i++ {
|
||||
l := b.LineBytes(i)
|
||||
charpos := 0
|
||||
|
||||
if i == start.Y && start.Y == end.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
start.X = util.Clamp(start.X, 0, nchars)
|
||||
end.X = util.Clamp(end.X, 0, nchars)
|
||||
l = util.SliceStart(l, end.X)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == start.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
start.X = util.Clamp(start.X, 0, nchars)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == end.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
end.X = util.Clamp(end.X, 0, nchars)
|
||||
l = util.SliceStart(l, end.X)
|
||||
}
|
||||
|
||||
match := r.FindIndex(l)
|
||||
|
||||
if match != nil {
|
||||
start := Loc{charpos + util.RunePos(l, match[0]), i}
|
||||
end := Loc{charpos + util.RunePos(l, match[1]), i}
|
||||
return [2]Loc{start, end}, true
|
||||
}
|
||||
}
|
||||
return [2]Loc{}, false
|
||||
}
|
||||
|
||||
func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
|
||||
start.Y = util.Clamp(start.Y, 0, b.LinesNum()-1)
|
||||
end.Y = util.Clamp(end.Y, 0, b.LinesNum()-1)
|
||||
|
||||
if start.GreaterThan(end) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
for i := end.Y; i >= start.Y; i-- {
|
||||
l := b.LineBytes(i)
|
||||
charpos := 0
|
||||
|
||||
if i == start.Y && start.Y == end.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
start.X = util.Clamp(start.X, 0, nchars)
|
||||
end.X = util.Clamp(end.X, 0, nchars)
|
||||
l = util.SliceStart(l, end.X)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == start.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
start.X = util.Clamp(start.X, 0, nchars)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == end.Y {
|
||||
nchars := utf8.RuneCount(l)
|
||||
end.X = util.Clamp(end.X, 0, nchars)
|
||||
l = util.SliceStart(l, end.X)
|
||||
}
|
||||
|
||||
match := r.FindIndex(l)
|
||||
|
||||
if match != nil {
|
||||
start := Loc{charpos + util.RunePos(l, match[0]), i}
|
||||
end := Loc{charpos + util.RunePos(l, match[1]), i}
|
||||
return [2]Loc{start, end}, true
|
||||
}
|
||||
}
|
||||
return [2]Loc{}, false
|
||||
}
|
||||
|
||||
// FindNext finds the next occurrence of a given string in the buffer
|
||||
// It returns the start and end location of the match (if found) and
|
||||
// a boolean indicating if it was found
|
||||
// May also return an error if the search regex is invalid
|
||||
func (b *Buffer) FindNext(s string, start, end, from Loc, down bool, useRegex bool) ([2]Loc, bool, error) {
|
||||
if s == "" {
|
||||
return [2]Loc{}, false, nil
|
||||
}
|
||||
|
||||
var r *regexp.Regexp
|
||||
var err error
|
||||
|
||||
if !useRegex {
|
||||
s = regexp.QuoteMeta(s)
|
||||
}
|
||||
|
||||
if b.Settings["ignorecase"].(bool) {
|
||||
r, err = regexp.Compile("(?i)" + s)
|
||||
} else {
|
||||
r, err = regexp.Compile(s)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return [2]Loc{}, false, err
|
||||
}
|
||||
|
||||
var found bool
|
||||
var l [2]Loc
|
||||
if down {
|
||||
l, found = b.findDown(r, from, end)
|
||||
if !found {
|
||||
l, found = b.findDown(r, start, end)
|
||||
}
|
||||
} else {
|
||||
l, found = b.findUp(r, from, start)
|
||||
if !found {
|
||||
l, found = b.findUp(r, end, start)
|
||||
}
|
||||
}
|
||||
return l, found, nil
|
||||
}
|
||||
|
||||
// ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area
|
||||
// and returns the number of replacements made and the number of runes
|
||||
// added or removed
|
||||
func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte) (int, int) {
|
||||
if start.GreaterThan(end) {
|
||||
start, end = end, start
|
||||
}
|
||||
|
||||
netrunes := 0
|
||||
|
||||
found := 0
|
||||
var deltas []Delta
|
||||
for i := start.Y; i <= end.Y; i++ {
|
||||
l := b.lines[i].data
|
||||
charpos := 0
|
||||
|
||||
if start.Y == end.Y && i == start.Y {
|
||||
l = util.SliceStart(l, end.X)
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == start.Y {
|
||||
l = util.SliceEnd(l, start.X)
|
||||
charpos = start.X
|
||||
} else if i == end.Y {
|
||||
l = util.SliceStart(l, end.X)
|
||||
}
|
||||
newText := search.ReplaceAllFunc(l, func(in []byte) []byte {
|
||||
result := []byte{}
|
||||
for _, submatches := range search.FindAllSubmatchIndex(in, -1) {
|
||||
result = search.Expand(result, replace, in, submatches)
|
||||
}
|
||||
found++
|
||||
netrunes += utf8.RuneCount(in) - utf8.RuneCount(result)
|
||||
return result
|
||||
})
|
||||
|
||||
from := Loc{charpos, i}
|
||||
to := Loc{charpos + utf8.RuneCount(l), i}
|
||||
|
||||
deltas = append(deltas, Delta{newText, from, to})
|
||||
}
|
||||
b.MultipleReplace(deltas)
|
||||
|
||||
return found, netrunes
|
||||
}
|
||||
76
internal/buffer/serialize.go
Normal file
76
internal/buffer/serialize.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/encoding"
|
||||
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// The SerializedBuffer holds the types that get serialized when a buffer is saved
|
||||
// These are used for the savecursor and saveundo options
|
||||
type SerializedBuffer struct {
|
||||
EventHandler *EventHandler
|
||||
Cursor Loc
|
||||
ModTime time.Time
|
||||
}
|
||||
|
||||
// Serialize serializes the buffer to config.ConfigDir/buffers
|
||||
func (b *Buffer) Serialize() error {
|
||||
if !b.Settings["savecursor"].(bool) && !b.Settings["saveundo"].(bool) {
|
||||
return nil
|
||||
}
|
||||
if b.Path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
name := filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath))
|
||||
|
||||
return overwriteFile(name, encoding.Nop, func(file io.Writer) error {
|
||||
err := gob.NewEncoder(file).Encode(SerializedBuffer{
|
||||
b.EventHandler,
|
||||
b.GetActiveCursor().Loc,
|
||||
b.ModTime,
|
||||
})
|
||||
return err
|
||||
}, false)
|
||||
}
|
||||
|
||||
// Unserialize loads the buffer info from config.ConfigDir/buffers
|
||||
func (b *Buffer) Unserialize() error {
|
||||
// If either savecursor or saveundo is turned on, we need to load the serialized information
|
||||
// from ~/.config/micro/buffers
|
||||
if b.Path == "" {
|
||||
return nil
|
||||
}
|
||||
file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath)))
|
||||
defer file.Close()
|
||||
if err == nil {
|
||||
var buffer SerializedBuffer
|
||||
decoder := gob.NewDecoder(file)
|
||||
err = decoder.Decode(&buffer)
|
||||
if err != nil {
|
||||
return errors.New(err.Error() + "\nYou may want to remove the files in ~/.config/micro/buffers (these files\nstore the information for the 'saveundo' and 'savecursor' options) if\nthis problem persists.\nThis may be caused by upgrading to version 2.0, and removing the 'buffers'\ndirectory will reset the cursor and undo history and solve the problem.")
|
||||
}
|
||||
if b.Settings["savecursor"].(bool) {
|
||||
b.StartCursor = buffer.Cursor
|
||||
}
|
||||
|
||||
if b.Settings["saveundo"].(bool) {
|
||||
// We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime
|
||||
if b.ModTime == buffer.ModTime {
|
||||
b.EventHandler = buffer.EventHandler
|
||||
b.EventHandler.cursors = b.cursors
|
||||
b.EventHandler.buf = b.SharedBuffer
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
57
internal/buffer/settings.go
Normal file
57
internal/buffer/settings.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
)
|
||||
|
||||
func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
|
||||
b.Settings[option] = nativeValue
|
||||
|
||||
if option == "fastdirty" {
|
||||
if !nativeValue.(bool) {
|
||||
e := calcHash(b, &b.origHash)
|
||||
if e == ErrFileTooLarge {
|
||||
b.Settings["fastdirty"] = false
|
||||
}
|
||||
}
|
||||
} else if option == "statusline" {
|
||||
screen.Redraw()
|
||||
} else if option == "filetype" {
|
||||
b.UpdateRules()
|
||||
} else if option == "fileformat" {
|
||||
switch b.Settings["fileformat"].(string) {
|
||||
case "unix":
|
||||
b.Endings = FFUnix
|
||||
case "dos":
|
||||
b.Endings = FFDos
|
||||
}
|
||||
b.isModified = true
|
||||
} else if option == "syntax" {
|
||||
if !nativeValue.(bool) {
|
||||
b.ClearMatches()
|
||||
} else {
|
||||
b.UpdateRules()
|
||||
}
|
||||
} else if option == "encoding" {
|
||||
b.isModified = true
|
||||
} else if option == "readonly" && b.Type.Kind == BTDefault.Kind {
|
||||
b.Type.Readonly = nativeValue.(bool)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetOption sets a given option to a value just for this buffer
|
||||
func (b *Buffer) SetOption(option, value string) error {
|
||||
if _, ok := b.Settings[option]; !ok {
|
||||
return config.ErrInvalidOption
|
||||
}
|
||||
|
||||
nativeValue, err := config.GetNativeValue(option, b.Settings[option], value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.SetOptionNative(option, nativeValue)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package main
|
||||
package buffer
|
||||
|
||||
// Stack is a simple implementation of a LIFO stack for text events
|
||||
type Stack struct {
|
||||
// TEStack is a simple implementation of a LIFO stack for text events
|
||||
type TEStack struct {
|
||||
Top *Element
|
||||
Size int
|
||||
}
|
||||
@@ -13,19 +13,19 @@ type Element struct {
|
||||
}
|
||||
|
||||
// Len returns the stack's length
|
||||
func (s *Stack) Len() int {
|
||||
func (s *TEStack) Len() int {
|
||||
return s.Size
|
||||
}
|
||||
|
||||
// Push a new element onto the stack
|
||||
func (s *Stack) Push(value *TextEvent) {
|
||||
func (s *TEStack) Push(value *TextEvent) {
|
||||
s.Top = &Element{value, s.Top}
|
||||
s.Size++
|
||||
}
|
||||
|
||||
// Pop removes the top element from the stack and returns its value
|
||||
// If the stack is empty, return nil
|
||||
func (s *Stack) Pop() (value *TextEvent) {
|
||||
func (s *TEStack) Pop() (value *TextEvent) {
|
||||
if s.Size > 0 {
|
||||
value, s.Top = s.Top.Value, s.Top.Next
|
||||
s.Size--
|
||||
@@ -35,7 +35,7 @@ func (s *Stack) Pop() (value *TextEvent) {
|
||||
}
|
||||
|
||||
// Peek returns the top element of the stack without removing it
|
||||
func (s *Stack) Peek() *TextEvent {
|
||||
func (s *TEStack) Peek() *TextEvent {
|
||||
if s.Size > 0 {
|
||||
return s.Top.Value
|
||||
}
|
||||
35
internal/buffer/stack_test.go
Normal file
35
internal/buffer/stack_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStack(t *testing.T) {
|
||||
s := new(TEStack)
|
||||
e1 := &TextEvent{
|
||||
EventType: TextEventReplace,
|
||||
Time: time.Now(),
|
||||
}
|
||||
e2 := &TextEvent{
|
||||
EventType: TextEventInsert,
|
||||
Time: time.Now(),
|
||||
}
|
||||
s.Push(e1)
|
||||
s.Push(e2)
|
||||
|
||||
p := s.Peek()
|
||||
assert.Equal(t, p.EventType, TextEventInsert)
|
||||
p = s.Pop()
|
||||
assert.Equal(t, p.EventType, TextEventInsert)
|
||||
p = s.Peek()
|
||||
assert.Equal(t, p.EventType, TextEventReplace)
|
||||
p = s.Pop()
|
||||
assert.Equal(t, p.EventType, TextEventReplace)
|
||||
p = s.Pop()
|
||||
assert.Nil(t, p)
|
||||
p = s.Peek()
|
||||
assert.Nil(t, p)
|
||||
}
|
||||
45
internal/config/autosave.go
Normal file
45
internal/config/autosave.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Autosave chan bool
|
||||
var autotime int
|
||||
|
||||
// lock for autosave
|
||||
var autolock sync.Mutex
|
||||
|
||||
func init() {
|
||||
Autosave = make(chan bool)
|
||||
}
|
||||
|
||||
func SetAutoTime(a int) {
|
||||
autolock.Lock()
|
||||
autotime = a
|
||||
autolock.Unlock()
|
||||
}
|
||||
|
||||
func GetAutoTime() int {
|
||||
autolock.Lock()
|
||||
a := autotime
|
||||
autolock.Unlock()
|
||||
return a
|
||||
}
|
||||
|
||||
func StartAutoSave() {
|
||||
go func() {
|
||||
for {
|
||||
if autotime < 1 {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Duration(autotime) * time.Second)
|
||||
// it's possible autotime was changed while sleeping
|
||||
if autotime < 1 {
|
||||
break
|
||||
}
|
||||
Autosave <- true
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package main
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -10,60 +9,85 @@ import (
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
// Colorscheme is a map from string to style -- it represents a colorscheme
|
||||
type Colorscheme map[string]tcell.Style
|
||||
// Micro's default style
|
||||
var DefStyle tcell.Style = tcell.StyleDefault
|
||||
|
||||
// The current colorscheme
|
||||
var colorscheme Colorscheme
|
||||
var Colorscheme map[string]tcell.Style
|
||||
|
||||
var preInstalledColors = []string{"default", "simple", "solarized", "solarized-tc", "atom-dark-tc", "monokai", "gruvbox", "zenburn"}
|
||||
// GetColor takes in a syntax group and returns the colorscheme's style for that group
|
||||
func GetColor(color string) tcell.Style {
|
||||
st := DefStyle
|
||||
if color == "" {
|
||||
return st
|
||||
}
|
||||
groups := strings.Split(color, ".")
|
||||
if len(groups) > 1 {
|
||||
curGroup := ""
|
||||
for i, g := range groups {
|
||||
if i != 0 {
|
||||
curGroup += "."
|
||||
}
|
||||
curGroup += g
|
||||
if style, ok := Colorscheme[curGroup]; ok {
|
||||
st = style
|
||||
}
|
||||
}
|
||||
} else if style, ok := Colorscheme[color]; ok {
|
||||
st = style
|
||||
} else {
|
||||
st = StringToStyle(color)
|
||||
}
|
||||
|
||||
// InitColorscheme picks and initializes the colorscheme when micro starts
|
||||
func InitColorscheme() {
|
||||
LoadDefaultColorscheme()
|
||||
return st
|
||||
}
|
||||
|
||||
// LoadDefaultColorscheme loads the default colorscheme from $(configDir)/colorschemes
|
||||
func LoadDefaultColorscheme() {
|
||||
LoadColorscheme(globalSettings["colorscheme"].(string), configDir+"/colorschemes")
|
||||
// ColorschemeExists checks if a given colorscheme exists
|
||||
func ColorschemeExists(colorschemeName string) bool {
|
||||
return FindRuntimeFile(RTColorscheme, colorschemeName) != nil
|
||||
}
|
||||
|
||||
// InitColorscheme picks and initializes the colorscheme when micro starts
|
||||
func InitColorscheme() error {
|
||||
Colorscheme = make(map[string]tcell.Style)
|
||||
DefStyle = tcell.StyleDefault
|
||||
|
||||
return LoadDefaultColorscheme()
|
||||
}
|
||||
|
||||
// LoadDefaultColorscheme loads the default colorscheme from $(ConfigDir)/colorschemes
|
||||
func LoadDefaultColorscheme() error {
|
||||
return LoadColorscheme(GlobalSettings["colorscheme"].(string))
|
||||
}
|
||||
|
||||
// LoadColorscheme loads the given colorscheme from a directory
|
||||
func LoadColorscheme(colorschemeName, dir string) {
|
||||
files, _ := ioutil.ReadDir(dir)
|
||||
for _, f := range files {
|
||||
if f.Name() == colorschemeName+".micro" {
|
||||
text, err := ioutil.ReadFile(dir + "/" + f.Name())
|
||||
if err != nil {
|
||||
fmt.Println("Error loading colorscheme:", err)
|
||||
continue
|
||||
}
|
||||
colorscheme = ParseColorscheme(string(text))
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range preInstalledColors {
|
||||
if name == colorschemeName {
|
||||
data, err := Asset("runtime/colorschemes/" + name + ".micro")
|
||||
if err != nil {
|
||||
TermMessage("Unable to load pre-installed colorscheme " + name)
|
||||
continue
|
||||
}
|
||||
colorscheme = ParseColorscheme(string(data))
|
||||
func LoadColorscheme(colorschemeName string) error {
|
||||
file := FindRuntimeFile(RTColorscheme, colorschemeName)
|
||||
if file == nil {
|
||||
return errors.New(colorschemeName + " is not a valid colorscheme")
|
||||
}
|
||||
if data, err := file.Data(); err != nil {
|
||||
return errors.New("Error loading colorscheme: " + err.Error())
|
||||
} else {
|
||||
Colorscheme, err = ParseColorscheme(string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseColorscheme parses the text definition for a colorscheme and returns the corresponding object
|
||||
// Colorschemes are made up of color-link statements linking a color group to a list of colors
|
||||
// For example, color-link keyword (blue,red) makes all keywords have a blue foreground and
|
||||
// red background
|
||||
func ParseColorscheme(text string) Colorscheme {
|
||||
func ParseColorscheme(text string) (map[string]tcell.Style, error) {
|
||||
var err error
|
||||
parser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`)
|
||||
|
||||
lines := strings.Split(text, "\n")
|
||||
|
||||
c := make(Colorscheme)
|
||||
c := make(map[string]tcell.Style)
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == "" ||
|
||||
@@ -77,22 +101,32 @@ func ParseColorscheme(text string) Colorscheme {
|
||||
link := string(matches[1])
|
||||
colors := string(matches[2])
|
||||
|
||||
c[link] = StringToStyle(colors)
|
||||
style := StringToStyle(colors)
|
||||
c[link] = style
|
||||
|
||||
if link == "default" {
|
||||
DefStyle = style
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Color-link statement is not valid:", line)
|
||||
err = errors.New("Color-link statement is not valid: " + line)
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
return c, err
|
||||
}
|
||||
|
||||
// StringToStyle returns a style from a string
|
||||
// The strings must be in the format "extra foregroundcolor,backgroundcolor"
|
||||
// The 'extra' can be bold, reverse, or underline
|
||||
func StringToStyle(str string) tcell.Style {
|
||||
var fg string
|
||||
bg := "default"
|
||||
split := strings.Split(str, ",")
|
||||
var fg, bg string
|
||||
spaceSplit := strings.Split(str, " ")
|
||||
var split []string
|
||||
if len(spaceSplit) > 1 {
|
||||
split = strings.Split(spaceSplit[1], ",")
|
||||
} else {
|
||||
split = strings.Split(str, ",")
|
||||
}
|
||||
if len(split) > 1 {
|
||||
fg, bg = split[0], split[1]
|
||||
} else {
|
||||
@@ -101,7 +135,19 @@ func StringToStyle(str string) tcell.Style {
|
||||
fg = strings.TrimSpace(fg)
|
||||
bg = strings.TrimSpace(bg)
|
||||
|
||||
style := defStyle.Foreground(StringToColor(fg)).Background(StringToColor(bg))
|
||||
var fgColor, bgColor tcell.Color
|
||||
if fg == "" {
|
||||
fgColor, _, _ = DefStyle.Decompose()
|
||||
} else {
|
||||
fgColor = StringToColor(fg)
|
||||
}
|
||||
if bg == "" {
|
||||
_, bgColor, _ = DefStyle.Decompose()
|
||||
} else {
|
||||
bgColor = StringToColor(bg)
|
||||
}
|
||||
|
||||
style := DefStyle.Foreground(fgColor).Background(bgColor)
|
||||
if strings.Contains(str, "bold") {
|
||||
style = style.Bold(true)
|
||||
}
|
||||
@@ -202,5 +248,9 @@ func GetColor256(color int) tcell.Color {
|
||||
tcell.Color253, tcell.Color254, tcell.Color255,
|
||||
}
|
||||
|
||||
return colors[color]
|
||||
if color >= 0 && color < len(colors) {
|
||||
return colors[color]
|
||||
}
|
||||
|
||||
return tcell.ColorDefault
|
||||
}
|
||||
62
internal/config/colorscheme_test.go
Normal file
62
internal/config/colorscheme_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
func TestSimpleStringToStyle(t *testing.T) {
|
||||
s := StringToStyle("lightblue,magenta")
|
||||
|
||||
fg, bg, _ := s.Decompose()
|
||||
|
||||
assert.Equal(t, tcell.ColorBlue, fg)
|
||||
assert.Equal(t, tcell.ColorPurple, bg)
|
||||
}
|
||||
|
||||
func TestAttributeStringToStyle(t *testing.T) {
|
||||
s := StringToStyle("bold cyan,brightcyan")
|
||||
|
||||
fg, bg, attr := s.Decompose()
|
||||
|
||||
assert.Equal(t, tcell.ColorTeal, fg)
|
||||
assert.Equal(t, tcell.ColorAqua, bg)
|
||||
assert.NotEqual(t, 0, attr&tcell.AttrBold)
|
||||
}
|
||||
|
||||
func TestColor256StringToStyle(t *testing.T) {
|
||||
s := StringToStyle("128,60")
|
||||
|
||||
fg, bg, _ := s.Decompose()
|
||||
|
||||
assert.Equal(t, tcell.Color128, fg)
|
||||
assert.Equal(t, tcell.Color60, bg)
|
||||
}
|
||||
|
||||
func TestColorHexStringToStyle(t *testing.T) {
|
||||
s := StringToStyle("#deadbe,#ef1234")
|
||||
|
||||
fg, bg, _ := s.Decompose()
|
||||
|
||||
assert.Equal(t, tcell.NewRGBColor(222, 173, 190), fg)
|
||||
assert.Equal(t, tcell.NewRGBColor(239, 18, 52), bg)
|
||||
}
|
||||
|
||||
func TestColorschemeParser(t *testing.T) {
|
||||
testColorscheme := `color-link default "#F8F8F2,#282828"
|
||||
color-link comment "#75715E,#282828"
|
||||
# comment
|
||||
color-link identifier "#66D9EF,#282828" #comment
|
||||
color-link constant "#AE81FF,#282828"
|
||||
color-link constant.string "#E6DB74,#282828"
|
||||
color-link constant.string.char "#BDE6AD,#282828"`
|
||||
|
||||
c, err := ParseColorscheme(testColorscheme)
|
||||
assert.Nil(t, err)
|
||||
|
||||
fg, bg, _ := c["comment"].Decompose()
|
||||
assert.Equal(t, tcell.NewRGBColor(117, 113, 94), fg)
|
||||
assert.Equal(t, tcell.NewRGBColor(40, 40, 40), bg)
|
||||
}
|
||||
52
internal/config/config.go
Normal file
52
internal/config/config.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
)
|
||||
|
||||
var ConfigDir string
|
||||
|
||||
// InitConfigDir finds the configuration directory for micro according to the XDG spec.
|
||||
// If no directory is found, it creates one.
|
||||
func InitConfigDir(flagConfigDir string) error {
|
||||
var e error
|
||||
|
||||
microHome := os.Getenv("MICRO_CONFIG_HOME")
|
||||
if microHome == "" {
|
||||
// The user has not set $MICRO_CONFIG_HOME so we'll try $XDG_CONFIG_HOME
|
||||
xdgHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if xdgHome == "" {
|
||||
// The user has not set $XDG_CONFIG_HOME so we should act like it was set to ~/.config
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return errors.New("Error finding your home directory\nCan't load config files: " + err.Error())
|
||||
}
|
||||
xdgHome = filepath.Join(home, ".config")
|
||||
}
|
||||
|
||||
microHome = filepath.Join(xdgHome, "micro")
|
||||
}
|
||||
ConfigDir = microHome
|
||||
|
||||
if len(flagConfigDir) > 0 {
|
||||
if _, err := os.Stat(flagConfigDir); os.IsNotExist(err) {
|
||||
e = errors.New("Error: " + flagConfigDir + " does not exist. Defaulting to " + ConfigDir + ".")
|
||||
} else {
|
||||
ConfigDir = flagConfigDir
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Create micro config home directory if it does not exist
|
||||
// This creates parent directories and does nothing if it already exists
|
||||
err := os.MkdirAll(ConfigDir, os.ModePerm)
|
||||
if err != nil {
|
||||
return errors.New("Error creating configuration directory: " + err.Error())
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
7
internal/config/globals.go
Normal file
7
internal/config/globals.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
const (
|
||||
DoubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click
|
||||
)
|
||||
|
||||
var Bindings map[string]string
|
||||
157
internal/config/plugin.go
Normal file
157
internal/config/plugin.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
ulua "github.com/zyedidia/micro/internal/lua"
|
||||
)
|
||||
|
||||
// ErrNoSuchFunction is returned when Call is executed on a function that does not exist
|
||||
var ErrNoSuchFunction = errors.New("No such function exists")
|
||||
|
||||
// LoadAllPlugins loads all detected plugins (in runtime/plugins and ConfigDir/plugins)
|
||||
func LoadAllPlugins() error {
|
||||
var reterr error
|
||||
for _, p := range Plugins {
|
||||
err := p.Load()
|
||||
if err != nil {
|
||||
reterr = err
|
||||
}
|
||||
}
|
||||
return reterr
|
||||
}
|
||||
|
||||
// RunPluginFn runs a given function in all plugins
|
||||
// returns an error if any of the plugins had an error
|
||||
func RunPluginFn(fn string, args ...lua.LValue) error {
|
||||
var reterr error
|
||||
for _, p := range Plugins {
|
||||
if !p.IsEnabled() {
|
||||
continue
|
||||
}
|
||||
_, err := p.Call(fn, args...)
|
||||
if err != nil && err != ErrNoSuchFunction {
|
||||
reterr = errors.New("Plugin " + p.Name + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
return reterr
|
||||
}
|
||||
|
||||
// RunPluginFnBool runs a function in all plugins and returns
|
||||
// false if any one of them returned false
|
||||
// also returns an error if any of the plugins had an error
|
||||
func RunPluginFnBool(fn string, args ...lua.LValue) (bool, error) {
|
||||
var reterr error
|
||||
retbool := true
|
||||
for _, p := range Plugins {
|
||||
if !p.IsEnabled() {
|
||||
continue
|
||||
}
|
||||
val, err := p.Call(fn, args...)
|
||||
if err == ErrNoSuchFunction {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
reterr = errors.New("Plugin " + p.Name + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
if v, ok := val.(lua.LBool); ok {
|
||||
retbool = retbool && bool(v)
|
||||
}
|
||||
}
|
||||
return retbool, reterr
|
||||
}
|
||||
|
||||
// Plugin stores information about the source files/info for a plugin
|
||||
type Plugin struct {
|
||||
DirName string // name of plugin folder
|
||||
Name string // name of plugin
|
||||
Info *PluginInfo // json file containing info
|
||||
Srcs []RuntimeFile // lua files
|
||||
Loaded bool
|
||||
Default bool // pre-installed plugin
|
||||
}
|
||||
|
||||
// IsEnabled returns if a plugin is enabled
|
||||
func (p *Plugin) IsEnabled() bool {
|
||||
if v, ok := GlobalSettings[p.Name]; ok {
|
||||
return v.(bool) && p.Loaded
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Plugins is a list of all detected plugins (enabled or disabled)
|
||||
var Plugins []*Plugin
|
||||
|
||||
// Load creates an option for the plugin and runs all source files
|
||||
func (p *Plugin) Load() error {
|
||||
if v, ok := GlobalSettings[p.Name]; ok && !v.(bool) {
|
||||
return nil
|
||||
}
|
||||
for _, f := range p.Srcs {
|
||||
dat, err := f.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ulua.LoadFile(p.Name, f.Name(), dat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
p.Loaded = true
|
||||
RegisterGlobalOption(p.Name, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Call calls a given function in this plugin
|
||||
func (p *Plugin) Call(fn string, args ...lua.LValue) (lua.LValue, error) {
|
||||
plug := ulua.L.GetGlobal(p.Name)
|
||||
if plug == lua.LNil {
|
||||
log.Println("Plugin does not exist:", p.Name, "at", p.DirName, ":", p)
|
||||
return nil, nil
|
||||
}
|
||||
luafn := ulua.L.GetField(plug, fn)
|
||||
if luafn == lua.LNil {
|
||||
return nil, ErrNoSuchFunction
|
||||
}
|
||||
err := ulua.L.CallByParam(lua.P{
|
||||
Fn: luafn,
|
||||
NRet: 1,
|
||||
Protect: true,
|
||||
}, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := ulua.L.Get(-1)
|
||||
ulua.L.Pop(1)
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// FindPlugin returns the plugin with the given name
|
||||
func FindPlugin(name string) *Plugin {
|
||||
var pl *Plugin
|
||||
for _, p := range Plugins {
|
||||
if !p.IsEnabled() {
|
||||
continue
|
||||
}
|
||||
if p.Name == name {
|
||||
pl = p
|
||||
break
|
||||
}
|
||||
}
|
||||
return pl
|
||||
}
|
||||
|
||||
// FindAnyPlugin does not require the plugin to be enabled
|
||||
func FindAnyPlugin(name string) *Plugin {
|
||||
var pl *Plugin
|
||||
for _, p := range Plugins {
|
||||
if p.Name == name {
|
||||
pl = p
|
||||
break
|
||||
}
|
||||
}
|
||||
return pl
|
||||
}
|
||||
712
internal/config/plugin_installer.go
Normal file
712
internal/config/plugin_installer.go
Normal file
@@ -0,0 +1,712 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/blang/semver"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"github.com/zyedidia/json5"
|
||||
ulua "github.com/zyedidia/micro/internal/lua"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
var (
|
||||
allPluginPackages PluginPackages
|
||||
)
|
||||
|
||||
// CorePluginName is a plugin dependency name for the micro core.
|
||||
const CorePluginName = "micro"
|
||||
|
||||
// PluginChannel contains an url to a json list of PluginRepository
|
||||
type PluginChannel string
|
||||
|
||||
// PluginChannels is a slice of PluginChannel
|
||||
type PluginChannels []PluginChannel
|
||||
|
||||
// PluginRepository contains an url to json file containing PluginPackages
|
||||
type PluginRepository string
|
||||
|
||||
// PluginPackage contains the meta-data of a plugin and all available versions
|
||||
type PluginPackage struct {
|
||||
Name string
|
||||
Description string
|
||||
Author string
|
||||
Tags []string
|
||||
Versions PluginVersions
|
||||
}
|
||||
|
||||
// PluginPackages is a list of PluginPackage instances.
|
||||
type PluginPackages []*PluginPackage
|
||||
|
||||
// PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies.
|
||||
type PluginVersion struct {
|
||||
pack *PluginPackage
|
||||
Version semver.Version
|
||||
Url string
|
||||
Require PluginDependencies
|
||||
}
|
||||
|
||||
func (pv *PluginVersion) Pack() *PluginPackage {
|
||||
return pv.pack
|
||||
}
|
||||
|
||||
// PluginVersions is a slice of PluginVersion
|
||||
type PluginVersions []*PluginVersion
|
||||
|
||||
// PluginDependency descripes a dependency to another plugin or micro itself.
|
||||
type PluginDependency struct {
|
||||
Name string
|
||||
Range semver.Range
|
||||
}
|
||||
|
||||
// PluginDependencies is a slice of PluginDependency
|
||||
type PluginDependencies []*PluginDependency
|
||||
|
||||
func (pp *PluginPackage) String() string {
|
||||
buf := new(bytes.Buffer)
|
||||
buf.WriteString("Plugin: ")
|
||||
buf.WriteString(pp.Name)
|
||||
buf.WriteRune('\n')
|
||||
if pp.Author != "" {
|
||||
buf.WriteString("Author: ")
|
||||
buf.WriteString(pp.Author)
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
if pp.Description != "" {
|
||||
buf.WriteRune('\n')
|
||||
buf.WriteString(pp.Description)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages {
|
||||
wgQuery := new(sync.WaitGroup)
|
||||
wgQuery.Add(count)
|
||||
|
||||
results := make(chan PluginPackages)
|
||||
|
||||
wgDone := new(sync.WaitGroup)
|
||||
wgDone.Add(1)
|
||||
var packages PluginPackages
|
||||
for i := 0; i < count; i++ {
|
||||
go func(i int) {
|
||||
results <- fetcher(i)
|
||||
wgQuery.Done()
|
||||
}(i)
|
||||
}
|
||||
go func() {
|
||||
packages = make(PluginPackages, 0)
|
||||
for res := range results {
|
||||
packages = append(packages, res...)
|
||||
}
|
||||
wgDone.Done()
|
||||
}()
|
||||
wgQuery.Wait()
|
||||
close(results)
|
||||
wgDone.Wait()
|
||||
return packages
|
||||
}
|
||||
|
||||
// Fetch retrieves all available PluginPackages from the given channels
|
||||
func (pc PluginChannels) Fetch(out io.Writer) PluginPackages {
|
||||
return fetchAllSources(len(pc), func(i int) PluginPackages {
|
||||
return pc[i].Fetch(out)
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch retrieves all available PluginPackages from the given channel
|
||||
func (pc PluginChannel) Fetch(out io.Writer) PluginPackages {
|
||||
resp, err := http.Get(string(pc))
|
||||
if err != nil {
|
||||
fmt.Fprintln(out, "Failed to query plugin channel:\n", err)
|
||||
return PluginPackages{}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
decoder := json5.NewDecoder(resp.Body)
|
||||
|
||||
var repositories []PluginRepository
|
||||
if err := decoder.Decode(&repositories); err != nil {
|
||||
fmt.Fprintln(out, "Failed to decode channel data:\n", err)
|
||||
return PluginPackages{}
|
||||
}
|
||||
return fetchAllSources(len(repositories), func(i int) PluginPackages {
|
||||
return repositories[i].Fetch(out)
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch retrieves all available PluginPackages from the given repository
|
||||
func (pr PluginRepository) Fetch(out io.Writer) PluginPackages {
|
||||
resp, err := http.Get(string(pr))
|
||||
if err != nil {
|
||||
fmt.Fprintln(out, "Failed to query plugin repository:\n", err)
|
||||
return PluginPackages{}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
decoder := json5.NewDecoder(resp.Body)
|
||||
|
||||
var plugins PluginPackages
|
||||
if err := decoder.Decode(&plugins); err != nil {
|
||||
fmt.Fprintln(out, "Failed to decode repository data:\n", err)
|
||||
return PluginPackages{}
|
||||
}
|
||||
if len(plugins) > 0 {
|
||||
return PluginPackages{plugins[0]}
|
||||
}
|
||||
return nil
|
||||
// return plugins
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals raw json to a PluginVersion
|
||||
func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
|
||||
var values struct {
|
||||
Version semver.Version
|
||||
Url string
|
||||
Require map[string]string
|
||||
}
|
||||
|
||||
if err := json5.Unmarshal(data, &values); err != nil {
|
||||
return err
|
||||
}
|
||||
pv.Version = values.Version
|
||||
pv.Url = values.Url
|
||||
pv.Require = make(PluginDependencies, 0)
|
||||
|
||||
for k, v := range values.Require {
|
||||
// don't add the dependency if it's the core and
|
||||
// we have a unknown version number.
|
||||
// in that case just accept that dependency (which equals to not adding it.)
|
||||
if k != CorePluginName || !isUnknownCoreVersion() {
|
||||
if vRange, err := semver.ParseRange(v); err == nil {
|
||||
pv.Require = append(pv.Require, &PluginDependency{k, vRange})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals raw json to a PluginPackage
|
||||
func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
|
||||
var values struct {
|
||||
Name string
|
||||
Description string
|
||||
Author string
|
||||
Tags []string
|
||||
Versions PluginVersions
|
||||
}
|
||||
if err := json5.Unmarshal(data, &values); err != nil {
|
||||
return err
|
||||
}
|
||||
pp.Name = values.Name
|
||||
pp.Description = values.Description
|
||||
pp.Author = values.Author
|
||||
pp.Tags = values.Tags
|
||||
pp.Versions = values.Versions
|
||||
for _, v := range pp.Versions {
|
||||
v.pack = pp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllPluginPackages gets all PluginPackages which may be available.
|
||||
func GetAllPluginPackages(out io.Writer) PluginPackages {
|
||||
if allPluginPackages == nil {
|
||||
getOption := func(name string) []string {
|
||||
data := GetGlobalOption(name)
|
||||
if strs, ok := data.([]string); ok {
|
||||
return strs
|
||||
}
|
||||
if ifs, ok := data.([]interface{}); ok {
|
||||
result := make([]string, len(ifs))
|
||||
for i, urlIf := range ifs {
|
||||
if url, ok := urlIf.(string); ok {
|
||||
result[i] = url
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
channels := PluginChannels{}
|
||||
for _, url := range getOption("pluginchannels") {
|
||||
channels = append(channels, PluginChannel(url))
|
||||
}
|
||||
repos := []PluginRepository{}
|
||||
for _, url := range getOption("pluginrepos") {
|
||||
repos = append(repos, PluginRepository(url))
|
||||
}
|
||||
allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages {
|
||||
if i == 0 {
|
||||
return channels.Fetch(out)
|
||||
}
|
||||
return repos[i-1].Fetch(out)
|
||||
})
|
||||
}
|
||||
return allPluginPackages
|
||||
}
|
||||
|
||||
func (pv PluginVersions) find(ppName string) *PluginVersion {
|
||||
for _, v := range pv {
|
||||
if v.pack.Name == ppName {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Len returns the number of pluginversions in this slice
|
||||
func (pv PluginVersions) Len() int {
|
||||
return len(pv)
|
||||
}
|
||||
|
||||
// Swap two entries of the slice
|
||||
func (pv PluginVersions) Swap(i, j int) {
|
||||
pv[i], pv[j] = pv[j], pv[i]
|
||||
}
|
||||
|
||||
// Less returns true if the version at position i is greater then the version at position j (used for sorting)
|
||||
func (pv PluginVersions) Less(i, j int) bool {
|
||||
return pv[i].Version.GT(pv[j].Version)
|
||||
}
|
||||
|
||||
// Match returns true if the package matches a given search text
|
||||
func (pp PluginPackage) Match(text string) bool {
|
||||
text = strings.ToLower(text)
|
||||
for _, t := range pp.Tags {
|
||||
if strings.ToLower(t) == text {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if strings.Contains(strings.ToLower(pp.Name), text) {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(pp.Description), text) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsInstallable returns true if the package can be installed.
|
||||
func (pp PluginPackage) IsInstallable(out io.Writer) error {
|
||||
_, err := GetAllPluginPackages(out).Resolve(GetInstalledVersions(true), PluginDependencies{
|
||||
&PluginDependency{
|
||||
Name: pp.Name,
|
||||
Range: semver.Range(func(v semver.Version) bool { return true }),
|
||||
}})
|
||||
return err
|
||||
}
|
||||
|
||||
// SearchPlugin retrieves a list of all PluginPackages which match the given search text and
|
||||
// could be or are already installed
|
||||
func SearchPlugin(out io.Writer, texts []string) (plugins PluginPackages) {
|
||||
plugins = make(PluginPackages, 0)
|
||||
|
||||
pluginLoop:
|
||||
for _, pp := range GetAllPluginPackages(out) {
|
||||
for _, text := range texts {
|
||||
if !pp.Match(text) {
|
||||
continue pluginLoop
|
||||
}
|
||||
}
|
||||
|
||||
if err := pp.IsInstallable(out); err == nil {
|
||||
plugins = append(plugins, pp)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func isUnknownCoreVersion() bool {
|
||||
_, err := semver.ParseTolerant(util.Version)
|
||||
return err != nil
|
||||
}
|
||||
|
||||
func newStaticPluginVersion(name, version string) *PluginVersion {
|
||||
vers, err := semver.ParseTolerant(version)
|
||||
|
||||
if err != nil {
|
||||
if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
|
||||
vers = semver.MustParse("0.0.0-unknown")
|
||||
}
|
||||
}
|
||||
pl := &PluginPackage{
|
||||
Name: name,
|
||||
}
|
||||
pv := &PluginVersion{
|
||||
pack: pl,
|
||||
Version: vers,
|
||||
}
|
||||
pl.Versions = PluginVersions{pv}
|
||||
return pv
|
||||
}
|
||||
|
||||
// GetInstalledVersions returns a list of all currently installed plugins including an entry for
|
||||
// micro itself. This can be used to resolve dependencies.
|
||||
func GetInstalledVersions(withCore bool) PluginVersions {
|
||||
result := PluginVersions{}
|
||||
if withCore {
|
||||
result = append(result, newStaticPluginVersion(CorePluginName, util.Version))
|
||||
}
|
||||
|
||||
for _, p := range Plugins {
|
||||
if !p.IsEnabled() {
|
||||
continue
|
||||
}
|
||||
version := GetInstalledPluginVersion(p.Name)
|
||||
if pv := newStaticPluginVersion(p.Name, version); pv != nil {
|
||||
result = append(result, pv)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
|
||||
func GetInstalledPluginVersion(name string) string {
|
||||
plugin := ulua.L.GetGlobal(name)
|
||||
if plugin != lua.LNil {
|
||||
version := ulua.L.GetField(plugin, "VERSION")
|
||||
if str, ok := version.(lua.LString); ok {
|
||||
return string(str)
|
||||
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DownloadAndInstall downloads and installs the given plugin and version
|
||||
func (pv *PluginVersion) DownloadAndInstall(out io.Writer) error {
|
||||
fmt.Fprintf(out, "Downloading %q (%s) from %q\n", pv.pack.Name, pv.Version, pv.Url)
|
||||
resp, err := http.Get(pv.Url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
zipbuf := bytes.NewReader(data)
|
||||
z, err := zip.NewReader(zipbuf, zipbuf.Size())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetDir := filepath.Join(ConfigDir, "plug", pv.pack.Name)
|
||||
dirPerm := os.FileMode(0755)
|
||||
if err = os.MkdirAll(targetDir, dirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if all files in zip are in the same directory.
|
||||
// this might be the case if the plugin zip contains the whole plugin dir
|
||||
// instead of its content.
|
||||
var prefix string
|
||||
allPrefixed := false
|
||||
for i, f := range z.File {
|
||||
parts := strings.Split(f.Name, "/")
|
||||
if i == 0 {
|
||||
prefix = parts[0]
|
||||
} else if parts[0] != prefix {
|
||||
allPrefixed = false
|
||||
break
|
||||
} else {
|
||||
// switch to true since we have at least a second file
|
||||
allPrefixed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Install files and directory's
|
||||
for _, f := range z.File {
|
||||
parts := strings.Split(f.Name, "/")
|
||||
if allPrefixed {
|
||||
parts = parts[1:]
|
||||
}
|
||||
|
||||
targetName := filepath.Join(targetDir, filepath.Join(parts...))
|
||||
if f.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(targetName, dirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
basepath := filepath.Dir(targetName)
|
||||
|
||||
if err := os.MkdirAll(basepath, dirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
content, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer content.Close()
|
||||
target, err := os.Create(targetName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer target.Close()
|
||||
if _, err = io.Copy(target, content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pl PluginPackages) Get(name string) *PluginPackage {
|
||||
for _, p := range pl {
|
||||
if p.Name == name {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
|
||||
result := make(PluginVersions, 0)
|
||||
p := pl.Get(name)
|
||||
if p != nil {
|
||||
for _, v := range p.Versions {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
|
||||
m := make(map[string]*PluginDependency)
|
||||
for _, r := range req {
|
||||
m[r.Name] = r
|
||||
}
|
||||
for _, o := range other {
|
||||
cur, ok := m[o.Name]
|
||||
if ok {
|
||||
m[o.Name] = &PluginDependency{
|
||||
o.Name,
|
||||
o.Range.AND(cur.Range),
|
||||
}
|
||||
} else {
|
||||
m[o.Name] = o
|
||||
}
|
||||
}
|
||||
result := make(PluginDependencies, 0, len(m))
|
||||
for _, v := range m {
|
||||
result = append(result, v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Resolve resolves dependencies between different plugins
|
||||
func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
|
||||
if len(open) == 0 {
|
||||
return selectedVersions, nil
|
||||
}
|
||||
currentRequirement, stillOpen := open[0], open[1:]
|
||||
if currentRequirement != nil {
|
||||
if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
|
||||
if currentRequirement.Range(selVersion.Version) {
|
||||
return all.Resolve(selectedVersions, stillOpen)
|
||||
}
|
||||
return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
|
||||
}
|
||||
availableVersions := all.GetAllVersions(currentRequirement.Name)
|
||||
sort.Sort(availableVersions)
|
||||
|
||||
for _, version := range availableVersions {
|
||||
if currentRequirement.Range(version.Version) {
|
||||
resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
|
||||
|
||||
if err == nil {
|
||||
return resolved, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
|
||||
}
|
||||
return selectedVersions, nil
|
||||
}
|
||||
|
||||
func (pv PluginVersions) install(out io.Writer) {
|
||||
anyInstalled := false
|
||||
currentlyInstalled := GetInstalledVersions(true)
|
||||
|
||||
for _, sel := range pv {
|
||||
if sel.pack.Name != CorePluginName {
|
||||
shouldInstall := true
|
||||
if pv := currentlyInstalled.find(sel.pack.Name); pv != nil {
|
||||
if pv.Version.NE(sel.Version) {
|
||||
fmt.Fprintln(out, "Uninstalling", sel.pack.Name)
|
||||
UninstallPlugin(out, sel.pack.Name)
|
||||
} else {
|
||||
shouldInstall = false
|
||||
}
|
||||
}
|
||||
|
||||
if shouldInstall {
|
||||
if err := sel.DownloadAndInstall(out); err != nil {
|
||||
fmt.Fprintln(out, err)
|
||||
return
|
||||
}
|
||||
anyInstalled = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if anyInstalled {
|
||||
fmt.Fprintln(out, "One or more plugins installed.")
|
||||
} else {
|
||||
fmt.Fprintln(out, "Nothing to install / update")
|
||||
}
|
||||
}
|
||||
|
||||
// UninstallPlugin deletes the plugin folder of the given plugin
|
||||
func UninstallPlugin(out io.Writer, name string) {
|
||||
for _, p := range Plugins {
|
||||
if !p.IsEnabled() {
|
||||
continue
|
||||
}
|
||||
if p.Name == name {
|
||||
p.Loaded = false
|
||||
if err := os.RemoveAll(filepath.Join(ConfigDir, "plug", p.DirName)); err != nil {
|
||||
fmt.Fprintln(out, err)
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Install installs the plugin
|
||||
func (pl PluginPackage) Install(out io.Writer) {
|
||||
selected, err := GetAllPluginPackages(out).Resolve(GetInstalledVersions(true), PluginDependencies{
|
||||
&PluginDependency{
|
||||
Name: pl.Name,
|
||||
Range: semver.Range(func(v semver.Version) bool { return true }),
|
||||
}})
|
||||
if err != nil {
|
||||
fmt.Fprintln(out, err)
|
||||
return
|
||||
}
|
||||
selected.install(out)
|
||||
}
|
||||
|
||||
// UpdatePlugins updates the given plugins
|
||||
func UpdatePlugins(out io.Writer, plugins []string) {
|
||||
// if no plugins are specified, update all installed plugins.
|
||||
if len(plugins) == 0 {
|
||||
for _, p := range Plugins {
|
||||
if !p.IsEnabled() {
|
||||
continue
|
||||
}
|
||||
plugins = append(plugins, p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintln(out, "Checking for plugin updates")
|
||||
microVersion := PluginVersions{
|
||||
newStaticPluginVersion(CorePluginName, util.Version),
|
||||
}
|
||||
|
||||
var updates = make(PluginDependencies, 0)
|
||||
for _, name := range plugins {
|
||||
pv := GetInstalledPluginVersion(name)
|
||||
r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
|
||||
if err == nil {
|
||||
updates = append(updates, &PluginDependency{
|
||||
Name: name,
|
||||
Range: r,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
selected, err := GetAllPluginPackages(out).Resolve(microVersion, updates)
|
||||
if err != nil {
|
||||
fmt.Fprintln(out, err)
|
||||
return
|
||||
}
|
||||
selected.install(out)
|
||||
}
|
||||
|
||||
func PluginCommand(out io.Writer, cmd string, args []string) {
|
||||
switch cmd {
|
||||
case "install":
|
||||
installedVersions := GetInstalledVersions(false)
|
||||
for _, plugin := range args {
|
||||
pp := GetAllPluginPackages(out).Get(plugin)
|
||||
if pp == nil {
|
||||
fmt.Fprintln(out, "Unknown plugin \""+plugin+"\"")
|
||||
} else if err := pp.IsInstallable(out); err != nil {
|
||||
fmt.Fprintln(out, "Error installing ", plugin, ": ", err)
|
||||
} else {
|
||||
for _, installed := range installedVersions {
|
||||
if pp.Name == installed.Pack().Name {
|
||||
if pp.Versions[0].Version.Compare(installed.Version) == 1 {
|
||||
fmt.Fprintln(out, pp.Name, " is already installed but out-of-date: use 'plugin update ", pp.Name, "' to update")
|
||||
} else {
|
||||
fmt.Fprintln(out, pp.Name, " is already installed")
|
||||
}
|
||||
}
|
||||
}
|
||||
pp.Install(out)
|
||||
}
|
||||
}
|
||||
|
||||
case "remove":
|
||||
removed := ""
|
||||
for _, plugin := range args {
|
||||
// check if the plugin exists.
|
||||
for _, p := range Plugins {
|
||||
if p.Name == plugin && p.Default {
|
||||
fmt.Fprintln(out, "Default plugins cannot be removed, but can be disabled via settings.")
|
||||
continue
|
||||
}
|
||||
if p.Name == plugin {
|
||||
UninstallPlugin(out, plugin)
|
||||
removed += plugin + " "
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if removed != "" {
|
||||
fmt.Fprintln(out, "Removed ", removed)
|
||||
} else {
|
||||
fmt.Fprintln(out, "No plugins removed")
|
||||
}
|
||||
case "update":
|
||||
UpdatePlugins(out, args)
|
||||
case "list":
|
||||
plugins := GetInstalledVersions(false)
|
||||
fmt.Fprintln(out, "The following plugins are currently installed:")
|
||||
for _, p := range plugins {
|
||||
fmt.Fprintf(out, "%s (%s)\n", p.Pack().Name, p.Version)
|
||||
}
|
||||
case "search":
|
||||
plugins := SearchPlugin(out, args)
|
||||
fmt.Fprintln(out, len(plugins), " plugins found")
|
||||
for _, p := range plugins {
|
||||
fmt.Fprintln(out, "----------------")
|
||||
fmt.Fprintln(out, p.String())
|
||||
}
|
||||
fmt.Fprintln(out, "----------------")
|
||||
case "available":
|
||||
packages := GetAllPluginPackages(out)
|
||||
fmt.Fprintln(out, "Available Plugins:")
|
||||
for _, pkg := range packages {
|
||||
fmt.Fprintln(out, pkg.Name)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintln(out, "Invalid plugin command")
|
||||
}
|
||||
}
|
||||
56
internal/config/plugin_installer_test.go
Normal file
56
internal/config/plugin_installer_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/blang/semver"
|
||||
|
||||
"github.com/zyedidia/json5"
|
||||
)
|
||||
|
||||
func TestDependencyResolving(t *testing.T) {
|
||||
js := `
|
||||
[{
|
||||
"Name": "Foo",
|
||||
"Versions": [{ "Version": "1.0.0" }, { "Version": "1.5.0" },{ "Version": "2.0.0" }]
|
||||
}, {
|
||||
"Name": "Bar",
|
||||
"Versions": [{ "Version": "1.0.0", "Require": {"Foo": ">1.0.0 <2.0.0"} }]
|
||||
}, {
|
||||
"Name": "Unresolvable",
|
||||
"Versions": [{ "Version": "1.0.0", "Require": {"Foo": "<=1.0.0", "Bar": ">0.0.0"} }]
|
||||
}]
|
||||
`
|
||||
var all PluginPackages
|
||||
err := json5.Unmarshal([]byte(js), &all)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
selected, err := all.Resolve(PluginVersions{}, PluginDependencies{
|
||||
&PluginDependency{"Bar", semver.MustParseRange(">=1.0.0")},
|
||||
})
|
||||
|
||||
check := func(name, version string) {
|
||||
v := selected.find(name)
|
||||
expected := semver.MustParse(version)
|
||||
if v == nil {
|
||||
t.Errorf("Failed to resolve %s", name)
|
||||
} else if expected.NE(v.Version) {
|
||||
t.Errorf("%s resolved in wrong version %v", name, v)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
} else {
|
||||
check("Foo", "1.5.0")
|
||||
check("Bar", "1.0.0")
|
||||
}
|
||||
|
||||
selected, err = all.Resolve(PluginVersions{}, PluginDependencies{
|
||||
&PluginDependency{"Unresolvable", semver.MustParseRange(">0.0.0")},
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("Unresolvable package resolved:", selected)
|
||||
}
|
||||
}
|
||||
46
internal/config/plugin_manager.go
Normal file
46
internal/config/plugin_manager.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingName = errors.New("Missing or empty name field")
|
||||
ErrMissingDesc = errors.New("Missing or empty description field")
|
||||
ErrMissingSite = errors.New("Missing or empty website field")
|
||||
)
|
||||
|
||||
// PluginInfo contains all the needed info about a plugin
|
||||
// The info is just strings and are not used beyond that (except
|
||||
// the Site and Install fields should be valid URLs). This means
|
||||
// that the requirements for example can be formatted however the
|
||||
// plugin maker decides, the fields will only be parsed by humans
|
||||
// Name: name of plugin
|
||||
// Desc: description of plugin
|
||||
// Site: home website of plugin
|
||||
// Install: install link for plugin (can be link to repo or zip file)
|
||||
// Vstr: version
|
||||
// Require: list of dependencies and requirements
|
||||
type PluginInfo struct {
|
||||
Name string `json:"Name"`
|
||||
Desc string `json:"Description"`
|
||||
Site string `json:"Website"`
|
||||
}
|
||||
|
||||
// NewPluginInfo parses a JSON input into a valid PluginInfo struct
|
||||
// Returns an error if there are any missing fields or any invalid fields
|
||||
// There are no optional fields in a plugin info json file
|
||||
func NewPluginInfo(data []byte) (*PluginInfo, error) {
|
||||
var info []PluginInfo
|
||||
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
// dec.DisallowUnknownFields() // Force errors
|
||||
|
||||
if err := dec.Decode(&info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &info[0], nil
|
||||
}
|
||||
306
internal/config/rtfiles.go
Normal file
306
internal/config/rtfiles.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
RTColorscheme = 0
|
||||
RTSyntax = 1
|
||||
RTHelp = 2
|
||||
RTPlugin = 3
|
||||
RTSyntaxHeader = 4
|
||||
)
|
||||
|
||||
var (
|
||||
NumTypes = 5 // How many filetypes are there
|
||||
)
|
||||
|
||||
type RTFiletype int
|
||||
|
||||
// RuntimeFile allows the program to read runtime data like colorschemes or syntax files
|
||||
type RuntimeFile interface {
|
||||
// Name returns a name of the file without paths or extensions
|
||||
Name() string
|
||||
// Data returns the content of the file.
|
||||
Data() ([]byte, error)
|
||||
}
|
||||
|
||||
// allFiles contains all available files, mapped by filetype
|
||||
var allFiles [][]RuntimeFile
|
||||
var realFiles [][]RuntimeFile
|
||||
|
||||
func init() {
|
||||
allFiles = make([][]RuntimeFile, NumTypes)
|
||||
realFiles = make([][]RuntimeFile, NumTypes)
|
||||
}
|
||||
|
||||
// NewRTFiletype creates a new RTFiletype
|
||||
func NewRTFiletype() int {
|
||||
NumTypes++
|
||||
allFiles = append(allFiles, []RuntimeFile{})
|
||||
realFiles = append(realFiles, []RuntimeFile{})
|
||||
return NumTypes - 1
|
||||
}
|
||||
|
||||
// some file on filesystem
|
||||
type realFile string
|
||||
|
||||
// some asset file
|
||||
type assetFile string
|
||||
|
||||
// some file on filesystem but with a different name
|
||||
type namedFile struct {
|
||||
realFile
|
||||
name string
|
||||
}
|
||||
|
||||
// a file with the data stored in memory
|
||||
type memoryFile struct {
|
||||
name string
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (mf memoryFile) Name() string {
|
||||
return mf.name
|
||||
}
|
||||
func (mf memoryFile) Data() ([]byte, error) {
|
||||
return mf.data, nil
|
||||
}
|
||||
|
||||
func (rf realFile) Name() string {
|
||||
fn := filepath.Base(string(rf))
|
||||
return fn[:len(fn)-len(filepath.Ext(fn))]
|
||||
}
|
||||
|
||||
func (rf realFile) Data() ([]byte, error) {
|
||||
return ioutil.ReadFile(string(rf))
|
||||
}
|
||||
|
||||
func (af assetFile) Name() string {
|
||||
fn := path.Base(string(af))
|
||||
return fn[:len(fn)-len(path.Ext(fn))]
|
||||
}
|
||||
|
||||
func (af assetFile) Data() ([]byte, error) {
|
||||
return Asset(string(af))
|
||||
}
|
||||
|
||||
func (nf namedFile) Name() string {
|
||||
return nf.name
|
||||
}
|
||||
|
||||
// AddRuntimeFile registers a file for the given filetype
|
||||
func AddRuntimeFile(fileType RTFiletype, file RuntimeFile) {
|
||||
allFiles[fileType] = append(allFiles[fileType], file)
|
||||
}
|
||||
|
||||
// AddRealRuntimeFile registers a file for the given filetype
|
||||
func AddRealRuntimeFile(fileType RTFiletype, file RuntimeFile) {
|
||||
allFiles[fileType] = append(allFiles[fileType], file)
|
||||
realFiles[fileType] = append(realFiles[fileType], file)
|
||||
}
|
||||
|
||||
// AddRuntimeFilesFromDirectory registers each file from the given directory for
|
||||
// the filetype which matches the file-pattern
|
||||
func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) {
|
||||
files, _ := ioutil.ReadDir(directory)
|
||||
for _, f := range files {
|
||||
if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok {
|
||||
fullPath := filepath.Join(directory, f.Name())
|
||||
AddRealRuntimeFile(fileType, realFile(fullPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddRuntimeFilesFromAssets registers each file from the given asset-directory for
|
||||
// the filetype which matches the file-pattern
|
||||
func AddRuntimeFilesFromAssets(fileType RTFiletype, directory, pattern string) {
|
||||
files, err := AssetDir(directory)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, f := range files {
|
||||
if ok, _ := path.Match(pattern, f); ok {
|
||||
AddRuntimeFile(fileType, assetFile(path.Join(directory, f)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FindRuntimeFile finds a runtime file of the given filetype and name
|
||||
// will return nil if no file was found
|
||||
func FindRuntimeFile(fileType RTFiletype, name string) RuntimeFile {
|
||||
for _, f := range ListRuntimeFiles(fileType) {
|
||||
if f.Name() == name {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRuntimeFiles lists all known runtime files for the given filetype
|
||||
func ListRuntimeFiles(fileType RTFiletype) []RuntimeFile {
|
||||
return allFiles[fileType]
|
||||
}
|
||||
|
||||
// ListRealRuntimeFiles lists all real runtime files (on disk) for a filetype
|
||||
// these runtime files will be ones defined by the user and loaded from the config directory
|
||||
func ListRealRuntimeFiles(fileType RTFiletype) []RuntimeFile {
|
||||
return realFiles[fileType]
|
||||
}
|
||||
|
||||
// InitRuntimeFiles initializes all assets file and the config directory
|
||||
func InitRuntimeFiles() {
|
||||
add := func(fileType RTFiletype, dir, pattern string) {
|
||||
AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern)
|
||||
AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern)
|
||||
}
|
||||
|
||||
add(RTColorscheme, "colorschemes", "*.micro")
|
||||
add(RTSyntax, "syntax", "*.yaml")
|
||||
add(RTSyntaxHeader, "syntax", "*.hdr")
|
||||
add(RTHelp, "help", "*.md")
|
||||
|
||||
initlua := filepath.Join(ConfigDir, "init.lua")
|
||||
if _, err := os.Stat(initlua); !os.IsNotExist(err) {
|
||||
p := new(Plugin)
|
||||
p.Name = "initlua"
|
||||
p.DirName = "initlua"
|
||||
p.Srcs = append(p.Srcs, realFile(initlua))
|
||||
Plugins = append(Plugins, p)
|
||||
}
|
||||
|
||||
// Search ConfigDir for plugin-scripts
|
||||
plugdir := filepath.Join(ConfigDir, "plug")
|
||||
files, _ := ioutil.ReadDir(plugdir)
|
||||
|
||||
isID := regexp.MustCompile(`^[_A-Za-z0-9]+$`).MatchString
|
||||
|
||||
for _, d := range files {
|
||||
if d.IsDir() {
|
||||
srcs, _ := ioutil.ReadDir(filepath.Join(plugdir, d.Name()))
|
||||
p := new(Plugin)
|
||||
p.Name = d.Name()
|
||||
p.DirName = d.Name()
|
||||
for _, f := range srcs {
|
||||
if strings.HasSuffix(f.Name(), ".lua") {
|
||||
p.Srcs = append(p.Srcs, realFile(filepath.Join(plugdir, d.Name(), f.Name())))
|
||||
} else if strings.HasSuffix(f.Name(), ".json") {
|
||||
data, err := ioutil.ReadFile(filepath.Join(plugdir, d.Name(), f.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
p.Info, err = NewPluginInfo(data)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
p.Name = p.Info.Name
|
||||
}
|
||||
}
|
||||
|
||||
if !isID(p.Name) || len(p.Srcs) <= 0 {
|
||||
log.Println(p.Name, "is not a plugin")
|
||||
continue
|
||||
}
|
||||
Plugins = append(Plugins, p)
|
||||
}
|
||||
}
|
||||
|
||||
plugdir = filepath.Join("runtime", "plugins")
|
||||
if files, err := AssetDir(plugdir); err == nil {
|
||||
for _, d := range files {
|
||||
if srcs, err := AssetDir(filepath.Join(plugdir, d)); err == nil {
|
||||
p := new(Plugin)
|
||||
p.Name = d
|
||||
p.DirName = d
|
||||
p.Default = true
|
||||
for _, f := range srcs {
|
||||
if strings.HasSuffix(f, ".lua") {
|
||||
p.Srcs = append(p.Srcs, assetFile(filepath.Join(plugdir, d, f)))
|
||||
} else if strings.HasSuffix(f, ".json") {
|
||||
data, err := Asset(filepath.Join(plugdir, d, f))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
p.Info, err = NewPluginInfo(data)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
p.Name = p.Info.Name
|
||||
}
|
||||
}
|
||||
if !isID(p.Name) || len(p.Srcs) <= 0 {
|
||||
log.Println(p.Name, "is not a plugin")
|
||||
continue
|
||||
}
|
||||
Plugins = append(Plugins, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PluginReadRuntimeFile allows plugin scripts to read the content of a runtime file
|
||||
func PluginReadRuntimeFile(fileType RTFiletype, name string) string {
|
||||
if file := FindRuntimeFile(fileType, name); file != nil {
|
||||
if data, err := file.Data(); err == nil {
|
||||
return string(data)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// PluginListRuntimeFiles allows plugins to lists all runtime files of the given type
|
||||
func PluginListRuntimeFiles(fileType RTFiletype) []string {
|
||||
files := ListRuntimeFiles(fileType)
|
||||
result := make([]string, len(files))
|
||||
for i, f := range files {
|
||||
result[i] = f.Name()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PluginAddRuntimeFile adds a file to the runtime files for a plugin
|
||||
func PluginAddRuntimeFile(plugin string, filetype RTFiletype, filePath string) error {
|
||||
pl := FindPlugin(plugin)
|
||||
if pl == nil {
|
||||
return errors.New("Plugin " + plugin + " does not exist")
|
||||
}
|
||||
pldir := pl.DirName
|
||||
fullpath := filepath.Join(ConfigDir, "plug", pldir, filePath)
|
||||
if _, err := os.Stat(fullpath); err == nil {
|
||||
AddRealRuntimeFile(filetype, realFile(fullpath))
|
||||
} else {
|
||||
fullpath = path.Join("runtime", "plugins", pldir, filePath)
|
||||
AddRuntimeFile(filetype, assetFile(fullpath))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PluginAddRuntimeFilesFromDirectory adds files from a directory to the runtime files for a plugin
|
||||
func PluginAddRuntimeFilesFromDirectory(plugin string, filetype RTFiletype, directory, pattern string) error {
|
||||
pl := FindPlugin(plugin)
|
||||
if pl == nil {
|
||||
return errors.New("Plugin " + plugin + " does not exist")
|
||||
}
|
||||
pldir := pl.DirName
|
||||
fullpath := filepath.Join(ConfigDir, "plug", pldir, directory)
|
||||
if _, err := os.Stat(fullpath); err == nil {
|
||||
AddRuntimeFilesFromDirectory(filetype, fullpath, pattern)
|
||||
} else {
|
||||
fullpath = path.Join("runtime", "plugins", pldir, directory)
|
||||
AddRuntimeFilesFromAssets(filetype, fullpath, pattern)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PluginAddRuntimeFileFromMemory adds a file to the runtime files for a plugin from a given string
|
||||
func PluginAddRuntimeFileFromMemory(filetype RTFiletype, filename, data string) {
|
||||
AddRealRuntimeFile(filetype, memoryFile{filename, []byte(data)})
|
||||
}
|
||||
42
internal/config/rtfiles_test.go
Normal file
42
internal/config/rtfiles_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
InitRuntimeFiles()
|
||||
}
|
||||
|
||||
func TestAddFile(t *testing.T) {
|
||||
AddRuntimeFile(RTPlugin, memoryFile{"foo.lua", []byte("hello world\n")})
|
||||
AddRuntimeFile(RTSyntax, memoryFile{"bar", []byte("some syntax file\n")})
|
||||
|
||||
f1 := FindRuntimeFile(RTPlugin, "foo.lua")
|
||||
assert.NotNil(t, f1)
|
||||
assert.Equal(t, "foo.lua", f1.Name())
|
||||
data, err := f1.Data()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte("hello world\n"), data)
|
||||
|
||||
f2 := FindRuntimeFile(RTSyntax, "bar")
|
||||
assert.NotNil(t, f2)
|
||||
assert.Equal(t, "bar", f2.Name())
|
||||
data, err = f2.Data()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte("some syntax file\n"), data)
|
||||
}
|
||||
|
||||
func TestFindFile(t *testing.T) {
|
||||
f := FindRuntimeFile(RTSyntax, "go")
|
||||
assert.NotNil(t, f)
|
||||
assert.Equal(t, "go", f.Name())
|
||||
data, err := f.Data()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []byte("filetype: go"), data[:12])
|
||||
|
||||
e := FindRuntimeFile(RTSyntax, "foobar")
|
||||
assert.Nil(t, e)
|
||||
}
|
||||
7556
internal/config/runtime.go
Normal file
7556
internal/config/runtime.go
Normal file
File diff suppressed because one or more lines are too long
392
internal/config/settings.go
Normal file
392
internal/config/settings.go
Normal file
@@ -0,0 +1,392 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/zyedidia/glob"
|
||||
"github.com/zyedidia/json5"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
"golang.org/x/text/encoding/htmlindex"
|
||||
)
|
||||
|
||||
type optionValidator func(string, interface{}) error
|
||||
|
||||
var (
|
||||
ErrInvalidOption = errors.New("Invalid option")
|
||||
ErrInvalidValue = errors.New("Invalid value")
|
||||
|
||||
// The options that the user can set
|
||||
GlobalSettings map[string]interface{}
|
||||
|
||||
// This is the raw parsed json
|
||||
parsedSettings map[string]interface{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
parsedSettings = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Options with validators
|
||||
var optionValidators = map[string]optionValidator{
|
||||
"autosave": validateNonNegativeValue,
|
||||
"tabsize": validatePositiveValue,
|
||||
"scrollmargin": validateNonNegativeValue,
|
||||
"scrollspeed": validateNonNegativeValue,
|
||||
"colorscheme": validateColorscheme,
|
||||
"colorcolumn": validateNonNegativeValue,
|
||||
"fileformat": validateLineEnding,
|
||||
"encoding": validateEncoding,
|
||||
}
|
||||
|
||||
func ReadSettings() error {
|
||||
filename := filepath.Join(ConfigDir, "settings.json")
|
||||
if _, e := os.Stat(filename); e == nil {
|
||||
input, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return errors.New("Error reading settings.json file: " + err.Error())
|
||||
}
|
||||
if !strings.HasPrefix(string(input), "null") {
|
||||
// Unmarshal the input into the parsed map
|
||||
err = json5.Unmarshal(input, &parsedSettings)
|
||||
if err != nil {
|
||||
return errors.New("Error reading settings.json: " + err.Error())
|
||||
}
|
||||
|
||||
// check if autosave is a boolean and convert it to float if so
|
||||
if v, ok := parsedSettings["autosave"]; ok {
|
||||
s, ok := v.(bool)
|
||||
if ok {
|
||||
if s {
|
||||
parsedSettings["autosave"] = 8.0
|
||||
} else {
|
||||
parsedSettings["autosave"] = 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitGlobalSettings initializes the options map and sets all options to their default values
|
||||
// Must be called after ReadSettings
|
||||
func InitGlobalSettings() {
|
||||
GlobalSettings = DefaultGlobalSettings()
|
||||
|
||||
for k, v := range parsedSettings {
|
||||
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
GlobalSettings[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InitLocalSettings scans the json in settings.json and sets the options locally based
|
||||
// on whether the filetype or path matches ft or glob local settings
|
||||
// Must be called after ReadSettings
|
||||
func InitLocalSettings(settings map[string]interface{}, path string) error {
|
||||
var parseError error
|
||||
for k, v := range parsedSettings {
|
||||
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
|
||||
if strings.HasPrefix(k, "ft:") {
|
||||
if settings["filetype"].(string) == k[3:] {
|
||||
for k1, v1 := range v.(map[string]interface{}) {
|
||||
settings[k1] = v1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
g, err := glob.Compile(k)
|
||||
if err != nil {
|
||||
parseError = errors.New("Error with glob setting " + k + ": " + err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if g.MatchString(path) {
|
||||
for k1, v1 := range v.(map[string]interface{}) {
|
||||
settings[k1] = v1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return parseError
|
||||
}
|
||||
|
||||
// WriteSettings writes the settings to the specified filename as JSON
|
||||
func WriteSettings(filename string) error {
|
||||
var err error
|
||||
if _, e := os.Stat(ConfigDir); e == nil {
|
||||
for k, v := range GlobalSettings {
|
||||
parsedSettings[k] = v
|
||||
}
|
||||
|
||||
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
|
||||
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func OverwriteSettings(filename string) error {
|
||||
var err error
|
||||
if _, e := os.Stat(ConfigDir); e == nil {
|
||||
txt, _ := json.MarshalIndent(GlobalSettings, "", " ")
|
||||
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
|
||||
func RegisterCommonOptionPlug(pl string, name string, defaultvalue interface{}) error {
|
||||
name = pl + "." + name
|
||||
if v, ok := GlobalSettings[name]; !ok {
|
||||
defaultCommonSettings[name] = defaultvalue
|
||||
GlobalSettings[name] = defaultvalue
|
||||
err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
|
||||
if err != nil {
|
||||
return errors.New("Error writing settings.json file: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
defaultCommonSettings[name] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
|
||||
func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
|
||||
return RegisterGlobalOption(pl+"."+name, defaultvalue)
|
||||
}
|
||||
|
||||
// RegisterGlobalOption creates a new global-only option
|
||||
func RegisterGlobalOption(name string, defaultvalue interface{}) error {
|
||||
if v, ok := GlobalSettings[name]; !ok {
|
||||
DefaultGlobalOnlySettings[name] = defaultvalue
|
||||
GlobalSettings[name] = defaultvalue
|
||||
err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
|
||||
if err != nil {
|
||||
return errors.New("Error writing settings.json file: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
DefaultGlobalOnlySettings[name] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGlobalOption returns the global value of the given option
|
||||
func GetGlobalOption(name string) interface{} {
|
||||
return GlobalSettings[name]
|
||||
}
|
||||
|
||||
var defaultCommonSettings = map[string]interface{}{
|
||||
"autoindent": true,
|
||||
"autosu": false,
|
||||
"backup": true,
|
||||
"basename": false,
|
||||
"colorcolumn": float64(0),
|
||||
"cursorline": true,
|
||||
"diffgutter": false,
|
||||
"encoding": "utf-8",
|
||||
"eofnewline": true,
|
||||
"fastdirty": false,
|
||||
"fileformat": "unix",
|
||||
"filetype": "unknown",
|
||||
"ignorecase": false,
|
||||
"indentchar": " ",
|
||||
"keepautoindent": false,
|
||||
"matchbrace": true,
|
||||
"mkparents": false,
|
||||
"readonly": false,
|
||||
"rmtrailingws": false,
|
||||
"ruler": true,
|
||||
"savecursor": false,
|
||||
"saveundo": false,
|
||||
"scrollbar": false,
|
||||
"scrollmargin": float64(3),
|
||||
"scrollspeed": float64(2),
|
||||
"smartpaste": true,
|
||||
"softwrap": false,
|
||||
"splitbottom": true,
|
||||
"splitright": true,
|
||||
"statusformatl": "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
|
||||
"statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
|
||||
"statusline": true,
|
||||
"syntax": true,
|
||||
"tabmovement": false,
|
||||
"tabsize": float64(4),
|
||||
"tabstospaces": false,
|
||||
"useprimary": true,
|
||||
}
|
||||
|
||||
func GetInfoBarOffset() int {
|
||||
offset := 0
|
||||
if GetGlobalOption("infobar").(bool) {
|
||||
offset++
|
||||
}
|
||||
if GetGlobalOption("keymenu").(bool) {
|
||||
offset += 2
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
// DefaultCommonSettings returns the default global settings for micro
|
||||
// Note that colorscheme is a global only option
|
||||
func DefaultCommonSettings() map[string]interface{} {
|
||||
commonsettings := make(map[string]interface{})
|
||||
for k, v := range defaultCommonSettings {
|
||||
commonsettings[k] = v
|
||||
}
|
||||
return commonsettings
|
||||
}
|
||||
|
||||
// a list of settings that should only be globally modified and their
|
||||
// default values
|
||||
var DefaultGlobalOnlySettings = map[string]interface{}{
|
||||
"autosave": float64(0),
|
||||
"colorscheme": "default",
|
||||
"infobar": true,
|
||||
"keymenu": false,
|
||||
"mouse": true,
|
||||
"paste": false,
|
||||
"savehistory": true,
|
||||
"sucmd": "sudo",
|
||||
"pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"},
|
||||
"pluginrepos": []string{},
|
||||
"xterm": false,
|
||||
}
|
||||
|
||||
// a list of settings that should never be globally modified
|
||||
var LocalSettings = []string{
|
||||
"filetype",
|
||||
"readonly",
|
||||
}
|
||||
|
||||
// DefaultGlobalSettings returns the default global settings for micro
|
||||
// Note that colorscheme is a global only option
|
||||
func DefaultGlobalSettings() map[string]interface{} {
|
||||
globalsettings := make(map[string]interface{})
|
||||
for k, v := range defaultCommonSettings {
|
||||
globalsettings[k] = v
|
||||
}
|
||||
for k, v := range DefaultGlobalOnlySettings {
|
||||
globalsettings[k] = v
|
||||
}
|
||||
return globalsettings
|
||||
}
|
||||
|
||||
// DefaultAllSettings returns a map of all settings and their
|
||||
// default values (both common and global settings)
|
||||
func DefaultAllSettings() map[string]interface{} {
|
||||
allsettings := make(map[string]interface{})
|
||||
for k, v := range defaultCommonSettings {
|
||||
allsettings[k] = v
|
||||
}
|
||||
for k, v := range DefaultGlobalOnlySettings {
|
||||
allsettings[k] = v
|
||||
}
|
||||
return allsettings
|
||||
}
|
||||
|
||||
// GetNativeValue parses and validates a value for a given option
|
||||
func GetNativeValue(option string, realValue interface{}, value string) (interface{}, error) {
|
||||
var native interface{}
|
||||
kind := reflect.TypeOf(realValue).Kind()
|
||||
if kind == reflect.Bool {
|
||||
b, err := util.ParseBool(value)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidValue
|
||||
}
|
||||
native = b
|
||||
} else if kind == reflect.String {
|
||||
native = value
|
||||
} else if kind == reflect.Float64 {
|
||||
i, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidValue
|
||||
}
|
||||
native = float64(i)
|
||||
} else {
|
||||
return nil, ErrInvalidValue
|
||||
}
|
||||
|
||||
if err := OptionIsValid(option, native); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return native, nil
|
||||
}
|
||||
|
||||
// OptionIsValid checks if a value is valid for a certain option
|
||||
func OptionIsValid(option string, value interface{}) error {
|
||||
if validator, ok := optionValidators[option]; ok {
|
||||
return validator(option, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Option validators
|
||||
|
||||
func validatePositiveValue(option string, value interface{}) error {
|
||||
tabsize, ok := value.(float64)
|
||||
|
||||
if !ok {
|
||||
return errors.New("Expected numeric type for " + option)
|
||||
}
|
||||
|
||||
if tabsize < 1 {
|
||||
return errors.New(option + " must be greater than 0")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateNonNegativeValue(option string, value interface{}) error {
|
||||
nativeValue, ok := value.(float64)
|
||||
|
||||
if !ok {
|
||||
return errors.New("Expected numeric type for " + option)
|
||||
}
|
||||
|
||||
if nativeValue < 0 {
|
||||
return errors.New(option + " must be non-negative")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateColorscheme(option string, value interface{}) error {
|
||||
colorscheme, ok := value.(string)
|
||||
|
||||
if !ok {
|
||||
return errors.New("Expected string type for colorscheme")
|
||||
}
|
||||
|
||||
if !ColorschemeExists(colorscheme) {
|
||||
return errors.New(colorscheme + " is not a valid colorscheme")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLineEnding(option string, value interface{}) error {
|
||||
endingType, ok := value.(string)
|
||||
|
||||
if !ok {
|
||||
return errors.New("Expected string type for file format")
|
||||
}
|
||||
|
||||
if endingType != "unix" && endingType != "dos" {
|
||||
return errors.New("File format must be either 'unix' or 'dos'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateEncoding(option string, value interface{}) error {
|
||||
_, err := htmlindex.Get(value.(string))
|
||||
return err
|
||||
}
|
||||
719
internal/display/bufwindow.go
Normal file
719
internal/display/bufwindow.go
Normal file
@@ -0,0 +1,719 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"unicode/utf8"
|
||||
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
// The BufWindow provides a way of displaying a certain section
|
||||
// of a buffer
|
||||
type BufWindow struct {
|
||||
*View
|
||||
|
||||
// Buffer being shown in this window
|
||||
Buf *buffer.Buffer
|
||||
|
||||
active bool
|
||||
|
||||
sline *StatusLine
|
||||
|
||||
gutterOffset int
|
||||
drawStatus bool
|
||||
}
|
||||
|
||||
// NewBufWindow creates a new window at a location in the screen with a width and height
|
||||
func NewBufWindow(x, y, width, height int, buf *buffer.Buffer) *BufWindow {
|
||||
w := new(BufWindow)
|
||||
w.View = new(View)
|
||||
w.X, w.Y, w.Width, w.Height, w.Buf = x, y, width, height, buf
|
||||
w.active = true
|
||||
|
||||
w.sline = NewStatusLine(w)
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *BufWindow) SetBuffer(b *buffer.Buffer) {
|
||||
w.Buf = b
|
||||
}
|
||||
|
||||
func (w *BufWindow) GetView() *View {
|
||||
return w.View
|
||||
}
|
||||
|
||||
func (w *BufWindow) SetView(view *View) {
|
||||
w.View = view
|
||||
}
|
||||
|
||||
func (w *BufWindow) Resize(width, height int) {
|
||||
w.Width, w.Height = width, height
|
||||
w.Relocate()
|
||||
}
|
||||
|
||||
func (w *BufWindow) SetActive(b bool) {
|
||||
w.active = b
|
||||
}
|
||||
|
||||
func (w *BufWindow) IsActive() bool {
|
||||
return w.active
|
||||
}
|
||||
|
||||
func (w *BufWindow) getStartInfo(n, lineN int) ([]byte, int, int, *tcell.Style) {
|
||||
tabsize := util.IntOpt(w.Buf.Settings["tabsize"])
|
||||
width := 0
|
||||
bloc := buffer.Loc{0, lineN}
|
||||
b := w.Buf.LineBytes(lineN)
|
||||
curStyle := config.DefStyle
|
||||
var s *tcell.Style
|
||||
for len(b) > 0 {
|
||||
r, size := utf8.DecodeRune(b)
|
||||
|
||||
curStyle, found := w.getStyle(curStyle, bloc, r)
|
||||
if found {
|
||||
s = &curStyle
|
||||
}
|
||||
|
||||
w := 0
|
||||
switch r {
|
||||
case '\t':
|
||||
ts := tabsize - (width % tabsize)
|
||||
w = ts
|
||||
default:
|
||||
w = runewidth.RuneWidth(r)
|
||||
}
|
||||
if width+w > n {
|
||||
return b, n - width, bloc.X, s
|
||||
}
|
||||
width += w
|
||||
b = b[size:]
|
||||
bloc.X++
|
||||
}
|
||||
return b, n - width, bloc.X, s
|
||||
}
|
||||
|
||||
// Clear resets all cells in this window to the default style
|
||||
func (w *BufWindow) Clear() {
|
||||
for y := 0; y < w.Height; y++ {
|
||||
for x := 0; x < w.Width; x++ {
|
||||
screen.SetContent(w.X+x, w.Y+y, ' ', nil, config.DefStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottomline returns the line number of the lowest line in the view
|
||||
// You might think that this is obviously just v.StartLine + v.Height
|
||||
// but if softwrap is enabled things get complicated since one buffer
|
||||
// line can take up multiple lines in the view
|
||||
func (w *BufWindow) Bottomline() int {
|
||||
if !w.Buf.Settings["softwrap"].(bool) {
|
||||
h := w.StartLine + w.Height - 1
|
||||
if w.drawStatus {
|
||||
h--
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
l := w.LocFromVisual(buffer.Loc{0, w.Y + w.Height})
|
||||
|
||||
return l.Y
|
||||
}
|
||||
|
||||
// Relocate moves the view window so that the cursor is in view
|
||||
// This is useful if the user has scrolled far away, and then starts typing
|
||||
// Returns true if the window location is moved
|
||||
func (w *BufWindow) Relocate() bool {
|
||||
b := w.Buf
|
||||
// how many buffer lines are in the view
|
||||
height := w.Bottomline() + 1 - w.StartLine
|
||||
h := w.Height
|
||||
if w.drawStatus {
|
||||
h--
|
||||
}
|
||||
ret := false
|
||||
activeC := w.Buf.GetActiveCursor()
|
||||
cy := activeC.Y
|
||||
if activeC.HasSelection() {
|
||||
cy = activeC.CurSelection[0].Y
|
||||
}
|
||||
scrollmargin := int(b.Settings["scrollmargin"].(float64))
|
||||
if cy < w.StartLine+scrollmargin && cy > scrollmargin-1 {
|
||||
w.StartLine = cy - scrollmargin
|
||||
ret = true
|
||||
} else if cy < w.StartLine {
|
||||
w.StartLine = cy
|
||||
ret = true
|
||||
}
|
||||
if cy > w.StartLine+height-1-scrollmargin && cy < b.LinesNum()-scrollmargin {
|
||||
w.StartLine = cy - height + 1 + scrollmargin
|
||||
ret = true
|
||||
} else if cy >= b.LinesNum()-scrollmargin && cy >= height {
|
||||
w.StartLine = b.LinesNum() - height
|
||||
ret = true
|
||||
}
|
||||
|
||||
// horizontal relocation (scrolling)
|
||||
if !b.Settings["softwrap"].(bool) {
|
||||
cx := activeC.GetVisualX()
|
||||
if cx < w.StartCol {
|
||||
w.StartCol = cx
|
||||
ret = true
|
||||
}
|
||||
if cx+w.gutterOffset+1 > w.StartCol+w.Width {
|
||||
w.StartCol = cx - w.Width + w.gutterOffset + 1
|
||||
ret = true
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// LocFromVisual takes a visual location (x and y position) and returns the
|
||||
// position in the buffer corresponding to the visual location
|
||||
// Computing the buffer location requires essentially drawing the entire screen
|
||||
// to account for complications like softwrap, wide characters, and horizontal scrolling
|
||||
// If the requested position does not correspond to a buffer location it returns
|
||||
// the nearest position
|
||||
func (w *BufWindow) LocFromVisual(svloc buffer.Loc) buffer.Loc {
|
||||
b := w.Buf
|
||||
|
||||
hasMessage := len(b.Messages) > 0
|
||||
bufHeight := w.Height
|
||||
if w.drawStatus {
|
||||
bufHeight--
|
||||
}
|
||||
|
||||
bufWidth := w.Width
|
||||
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
|
||||
bufWidth--
|
||||
}
|
||||
|
||||
// We need to know the string length of the largest line number
|
||||
// so we can pad appropriately when displaying line numbers
|
||||
maxLineNumLength := len(strconv.Itoa(b.LinesNum()))
|
||||
|
||||
tabsize := int(b.Settings["tabsize"].(float64))
|
||||
softwrap := b.Settings["softwrap"].(bool)
|
||||
|
||||
// this represents the current draw position
|
||||
// within the current window
|
||||
vloc := buffer.Loc{X: 0, Y: 0}
|
||||
|
||||
// this represents the current draw position in the buffer (char positions)
|
||||
bloc := buffer.Loc{X: -1, Y: w.StartLine}
|
||||
|
||||
for vloc.Y = 0; vloc.Y < bufHeight; vloc.Y++ {
|
||||
vloc.X = 0
|
||||
if hasMessage {
|
||||
vloc.X += 2
|
||||
}
|
||||
if b.Settings["diffgutter"].(bool) {
|
||||
vloc.X++
|
||||
}
|
||||
if b.Settings["ruler"].(bool) {
|
||||
vloc.X += maxLineNumLength + 1
|
||||
}
|
||||
|
||||
line := b.LineBytes(bloc.Y)
|
||||
line, nColsBeforeStart, bslice := util.SliceVisualEnd(line, w.StartCol, tabsize)
|
||||
bloc.X = bslice
|
||||
|
||||
draw := func() {
|
||||
if nColsBeforeStart <= 0 {
|
||||
vloc.X++
|
||||
}
|
||||
nColsBeforeStart--
|
||||
}
|
||||
|
||||
totalwidth := w.StartCol - nColsBeforeStart
|
||||
|
||||
if svloc.X <= vloc.X+w.X && vloc.Y+w.Y == svloc.Y {
|
||||
return bloc
|
||||
}
|
||||
for len(line) > 0 {
|
||||
if vloc.X+w.X == svloc.X && vloc.Y+w.Y == svloc.Y {
|
||||
return bloc
|
||||
}
|
||||
|
||||
r, size := utf8.DecodeRune(line)
|
||||
draw()
|
||||
width := 0
|
||||
|
||||
switch r {
|
||||
case '\t':
|
||||
ts := tabsize - (totalwidth % tabsize)
|
||||
width = ts
|
||||
default:
|
||||
width = runewidth.RuneWidth(r)
|
||||
}
|
||||
|
||||
// Draw any extra characters either spaces for tabs or @ for incomplete wide runes
|
||||
if width > 1 {
|
||||
for i := 1; i < width; i++ {
|
||||
if vloc.X+w.X == svloc.X && vloc.Y+w.Y == svloc.Y {
|
||||
return bloc
|
||||
}
|
||||
draw()
|
||||
}
|
||||
}
|
||||
bloc.X++
|
||||
line = line[size:]
|
||||
|
||||
totalwidth += width
|
||||
|
||||
// If we reach the end of the window then we either stop or we wrap for softwrap
|
||||
if vloc.X >= bufWidth {
|
||||
if !softwrap {
|
||||
break
|
||||
} else {
|
||||
vloc.Y++
|
||||
if vloc.Y >= bufHeight {
|
||||
break
|
||||
}
|
||||
vloc.X = w.gutterOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
if vloc.Y+w.Y == svloc.Y {
|
||||
return bloc
|
||||
}
|
||||
|
||||
if bloc.Y+1 >= b.LinesNum() || vloc.Y+1 >= bufHeight {
|
||||
return bloc
|
||||
}
|
||||
|
||||
bloc.X = w.StartCol
|
||||
bloc.Y++
|
||||
}
|
||||
|
||||
return buffer.Loc{}
|
||||
}
|
||||
|
||||
func (w *BufWindow) drawGutter(vloc *buffer.Loc, bloc *buffer.Loc) {
|
||||
char := ' '
|
||||
s := config.DefStyle
|
||||
for _, m := range w.Buf.Messages {
|
||||
if m.Start.Y == bloc.Y || m.End.Y == bloc.Y {
|
||||
s = m.Style()
|
||||
char = '>'
|
||||
break
|
||||
}
|
||||
}
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
|
||||
vloc.X++
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
|
||||
vloc.X++
|
||||
}
|
||||
|
||||
func (w *BufWindow) drawDiffGutter(backgroundStyle tcell.Style, softwrapped bool, vloc *buffer.Loc, bloc *buffer.Loc) {
|
||||
symbol := ' '
|
||||
styleName := ""
|
||||
|
||||
switch w.Buf.DiffStatus(bloc.Y) {
|
||||
case buffer.DSAdded:
|
||||
symbol = '\u258C' // Left half block
|
||||
styleName = "diff-added"
|
||||
case buffer.DSModified:
|
||||
symbol = '\u258C' // Left half block
|
||||
styleName = "diff-modified"
|
||||
case buffer.DSDeletedAbove:
|
||||
if !softwrapped {
|
||||
symbol = '\u2594' // Upper one eighth block
|
||||
styleName = "diff-deleted"
|
||||
}
|
||||
}
|
||||
|
||||
style := backgroundStyle
|
||||
if s, ok := config.Colorscheme[styleName]; ok {
|
||||
foreground, _, _ := s.Decompose()
|
||||
style = style.Foreground(foreground)
|
||||
}
|
||||
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, symbol, nil, style)
|
||||
vloc.X++
|
||||
}
|
||||
|
||||
func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, maxLineNumLength int, vloc *buffer.Loc, bloc *buffer.Loc) {
|
||||
lineNum := strconv.Itoa(bloc.Y + 1)
|
||||
|
||||
// Write the spaces before the line number if necessary
|
||||
for i := 0; i < maxLineNumLength-len(lineNum); i++ {
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
|
||||
vloc.X++
|
||||
}
|
||||
// Write the actual line number
|
||||
for _, ch := range lineNum {
|
||||
if softwrapped {
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
|
||||
} else {
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ch, nil, lineNumStyle)
|
||||
}
|
||||
vloc.X++
|
||||
}
|
||||
|
||||
// Write the extra space
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
|
||||
vloc.X++
|
||||
}
|
||||
|
||||
// getStyle returns the highlight style for the given character position
|
||||
// If there is no change to the current highlight style it just returns that
|
||||
func (w *BufWindow) getStyle(style tcell.Style, bloc buffer.Loc, r rune) (tcell.Style, bool) {
|
||||
if group, ok := w.Buf.Match(bloc.Y)[bloc.X]; ok {
|
||||
s := config.GetColor(group.String())
|
||||
return s, true
|
||||
}
|
||||
return style, false
|
||||
}
|
||||
|
||||
func (w *BufWindow) showCursor(x, y int, main bool) {
|
||||
if w.active {
|
||||
if main {
|
||||
screen.ShowCursor(x, y)
|
||||
} else {
|
||||
screen.ShowFakeCursorMulti(x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// displayBuffer draws the buffer being shown in this window on the screen.Screen
|
||||
func (w *BufWindow) displayBuffer() {
|
||||
b := w.Buf
|
||||
|
||||
if w.Height <= 0 || w.Width <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
hasMessage := len(b.Messages) > 0
|
||||
bufHeight := w.Height
|
||||
if w.drawStatus {
|
||||
bufHeight--
|
||||
}
|
||||
|
||||
bufWidth := w.Width
|
||||
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
|
||||
bufWidth--
|
||||
}
|
||||
|
||||
if b.ModifiedThisFrame {
|
||||
if b.Settings["diffgutter"].(bool) {
|
||||
b.UpdateDiff(func(synchronous bool) {
|
||||
// If the diff was updated asynchronously, the outer call to
|
||||
// displayBuffer might already be completed and we need to
|
||||
// schedule a redraw in order to display the new diff.
|
||||
// Note that this cannot lead to an infinite recursion
|
||||
// because the modifications were cleared above so there won't
|
||||
// be another call to UpdateDiff when displayBuffer is called
|
||||
// during the redraw.
|
||||
if !synchronous {
|
||||
screen.Redraw()
|
||||
}
|
||||
})
|
||||
}
|
||||
b.ModifiedThisFrame = false
|
||||
}
|
||||
|
||||
var matchingBraces []buffer.Loc
|
||||
// bracePairs is defined in buffer.go
|
||||
if b.Settings["matchbrace"].(bool) {
|
||||
for _, bp := range buffer.BracePairs {
|
||||
for _, c := range b.GetCursors() {
|
||||
if c.HasSelection() {
|
||||
continue
|
||||
}
|
||||
curX := c.X
|
||||
curLoc := c.Loc
|
||||
|
||||
r := c.RuneUnder(curX)
|
||||
rl := c.RuneUnder(curX - 1)
|
||||
if r == bp[0] || r == bp[1] || rl == bp[0] || rl == bp[1] {
|
||||
mb, left, found := b.FindMatchingBrace(bp, curLoc)
|
||||
if found {
|
||||
matchingBraces = append(matchingBraces, mb)
|
||||
if !left {
|
||||
matchingBraces = append(matchingBraces, curLoc)
|
||||
} else {
|
||||
matchingBraces = append(matchingBraces, curLoc.Move(-1, b))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lineNumStyle := config.DefStyle
|
||||
if style, ok := config.Colorscheme["line-number"]; ok {
|
||||
lineNumStyle = style
|
||||
}
|
||||
curNumStyle := config.DefStyle
|
||||
if style, ok := config.Colorscheme["current-line-number"]; ok {
|
||||
if !b.Settings["cursorline"].(bool) {
|
||||
curNumStyle = lineNumStyle
|
||||
} else {
|
||||
curNumStyle = style
|
||||
}
|
||||
}
|
||||
|
||||
// We need to know the string length of the largest line number
|
||||
// so we can pad appropriately when displaying line numbers
|
||||
maxLineNumLength := len(strconv.Itoa(b.LinesNum()))
|
||||
|
||||
softwrap := b.Settings["softwrap"].(bool)
|
||||
tabsize := util.IntOpt(b.Settings["tabsize"])
|
||||
colorcolumn := util.IntOpt(b.Settings["colorcolumn"])
|
||||
|
||||
// this represents the current draw position
|
||||
// within the current window
|
||||
vloc := buffer.Loc{X: 0, Y: 0}
|
||||
|
||||
// this represents the current draw position in the buffer (char positions)
|
||||
bloc := buffer.Loc{X: -1, Y: w.StartLine}
|
||||
|
||||
cursors := b.GetCursors()
|
||||
|
||||
curStyle := config.DefStyle
|
||||
for vloc.Y = 0; vloc.Y < bufHeight; vloc.Y++ {
|
||||
vloc.X = 0
|
||||
|
||||
currentLine := false
|
||||
for _, c := range cursors {
|
||||
if bloc.Y == c.Y && w.active {
|
||||
currentLine = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
s := lineNumStyle
|
||||
if currentLine {
|
||||
s = curNumStyle
|
||||
}
|
||||
|
||||
if hasMessage {
|
||||
w.drawGutter(&vloc, &bloc)
|
||||
}
|
||||
|
||||
if b.Settings["diffgutter"].(bool) {
|
||||
w.drawDiffGutter(s, false, &vloc, &bloc)
|
||||
}
|
||||
|
||||
if b.Settings["ruler"].(bool) {
|
||||
w.drawLineNum(s, false, maxLineNumLength, &vloc, &bloc)
|
||||
}
|
||||
|
||||
w.gutterOffset = vloc.X
|
||||
|
||||
line, nColsBeforeStart, bslice, startStyle := w.getStartInfo(w.StartCol, bloc.Y)
|
||||
if startStyle != nil {
|
||||
curStyle = *startStyle
|
||||
}
|
||||
bloc.X = bslice
|
||||
|
||||
draw := func(r rune, style tcell.Style, showcursor bool) {
|
||||
if nColsBeforeStart <= 0 {
|
||||
for _, c := range cursors {
|
||||
if c.HasSelection() &&
|
||||
(bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) ||
|
||||
bloc.LessThan(c.CurSelection[0]) && bloc.GreaterEqual(c.CurSelection[1])) {
|
||||
// The current character is selected
|
||||
style = config.DefStyle.Reverse(true)
|
||||
|
||||
if s, ok := config.Colorscheme["selection"]; ok {
|
||||
style = s
|
||||
}
|
||||
}
|
||||
|
||||
if b.Settings["cursorline"].(bool) && w.active &&
|
||||
!c.HasSelection() && c.Y == bloc.Y {
|
||||
if s, ok := config.Colorscheme["cursor-line"]; ok {
|
||||
fg, _, _ := s.Decompose()
|
||||
style = style.Background(fg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range b.Messages {
|
||||
if bloc.GreaterEqual(m.Start) && bloc.LessThan(m.End) ||
|
||||
bloc.LessThan(m.End) && bloc.GreaterEqual(m.Start) {
|
||||
style = style.Underline(true)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if r == '\t' {
|
||||
indentrunes := []rune(b.Settings["indentchar"].(string))
|
||||
// if empty indentchar settings, use space
|
||||
if indentrunes == nil || len(indentrunes) == 0 {
|
||||
indentrunes = []rune{' '}
|
||||
}
|
||||
|
||||
r = indentrunes[0]
|
||||
if s, ok := config.Colorscheme["indent-char"]; ok && r != ' ' {
|
||||
fg, _, _ := s.Decompose()
|
||||
style = style.Foreground(fg)
|
||||
}
|
||||
}
|
||||
|
||||
if s, ok := config.Colorscheme["color-column"]; ok {
|
||||
if colorcolumn != 0 && vloc.X-w.gutterOffset+w.StartCol == colorcolumn {
|
||||
fg, _, _ := s.Decompose()
|
||||
style = style.Background(fg)
|
||||
}
|
||||
}
|
||||
|
||||
for _, mb := range matchingBraces {
|
||||
if mb.X == bloc.X && mb.Y == bloc.Y {
|
||||
style = style.Underline(true)
|
||||
}
|
||||
}
|
||||
|
||||
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, r, nil, style)
|
||||
|
||||
if showcursor {
|
||||
for _, c := range cursors {
|
||||
if c.X == bloc.X && c.Y == bloc.Y && !c.HasSelection() {
|
||||
w.showCursor(w.X+vloc.X, w.Y+vloc.Y, c.Num == 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
vloc.X++
|
||||
}
|
||||
nColsBeforeStart--
|
||||
}
|
||||
|
||||
totalwidth := w.StartCol - nColsBeforeStart
|
||||
for len(line) > 0 {
|
||||
r, size := utf8.DecodeRune(line)
|
||||
curStyle, _ = w.getStyle(curStyle, bloc, r)
|
||||
|
||||
draw(r, curStyle, true)
|
||||
|
||||
width := 0
|
||||
|
||||
char := ' '
|
||||
switch r {
|
||||
case '\t':
|
||||
ts := tabsize - (totalwidth % tabsize)
|
||||
width = ts
|
||||
default:
|
||||
width = runewidth.RuneWidth(r)
|
||||
char = '@'
|
||||
}
|
||||
|
||||
// Draw any extra characters either spaces for tabs or @ for incomplete wide runes
|
||||
if width > 1 {
|
||||
for i := 1; i < width; i++ {
|
||||
draw(char, curStyle, false)
|
||||
}
|
||||
}
|
||||
bloc.X++
|
||||
line = line[size:]
|
||||
|
||||
totalwidth += width
|
||||
|
||||
// If we reach the end of the window then we either stop or we wrap for softwrap
|
||||
if vloc.X >= bufWidth {
|
||||
if !softwrap {
|
||||
break
|
||||
} else {
|
||||
vloc.Y++
|
||||
if vloc.Y >= bufHeight {
|
||||
break
|
||||
}
|
||||
vloc.X = 0
|
||||
if hasMessage {
|
||||
w.drawGutter(&vloc, &bloc)
|
||||
}
|
||||
if b.Settings["diffgutter"].(bool) {
|
||||
w.drawDiffGutter(lineNumStyle, true, &vloc, &bloc)
|
||||
}
|
||||
|
||||
// This will draw an empty line number because the current line is wrapped
|
||||
if b.Settings["ruler"].(bool) {
|
||||
w.drawLineNum(lineNumStyle, true, maxLineNumLength, &vloc, &bloc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style := config.DefStyle
|
||||
for _, c := range cursors {
|
||||
if b.Settings["cursorline"].(bool) && w.active &&
|
||||
!c.HasSelection() && c.Y == bloc.Y {
|
||||
if s, ok := config.Colorscheme["cursor-line"]; ok {
|
||||
fg, _, _ := s.Decompose()
|
||||
style = style.Background(fg)
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := vloc.X; i < bufWidth; i++ {
|
||||
curStyle := style
|
||||
if s, ok := config.Colorscheme["color-column"]; ok {
|
||||
if colorcolumn != 0 && i-w.gutterOffset+w.StartCol == colorcolumn {
|
||||
fg, _, _ := s.Decompose()
|
||||
curStyle = style.Background(fg)
|
||||
}
|
||||
}
|
||||
screen.SetContent(i+w.X, vloc.Y+w.Y, ' ', nil, curStyle)
|
||||
}
|
||||
|
||||
if vloc.X != bufWidth {
|
||||
draw(' ', curStyle, true)
|
||||
}
|
||||
|
||||
bloc.X = w.StartCol
|
||||
bloc.Y++
|
||||
if bloc.Y >= b.LinesNum() {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *BufWindow) displayStatusLine() {
|
||||
_, h := screen.Screen.Size()
|
||||
infoY := h
|
||||
if config.GetGlobalOption("infobar").(bool) {
|
||||
infoY--
|
||||
}
|
||||
|
||||
if w.Buf.Settings["statusline"].(bool) {
|
||||
w.drawStatus = true
|
||||
w.sline.Display()
|
||||
} else if w.Y+w.Height != infoY {
|
||||
w.drawStatus = true
|
||||
for x := w.X; x < w.X+w.Width; x++ {
|
||||
screen.SetContent(x, w.Y+w.Height-1, '-', nil, config.DefStyle.Reverse(true))
|
||||
}
|
||||
} else {
|
||||
w.drawStatus = false
|
||||
}
|
||||
}
|
||||
|
||||
func (w *BufWindow) displayScrollBar() {
|
||||
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
|
||||
scrollX := w.X + w.Width - 1
|
||||
bufHeight := w.Height
|
||||
if w.drawStatus {
|
||||
bufHeight--
|
||||
}
|
||||
barsize := int(float64(w.Height) / float64(w.Buf.LinesNum()) * float64(w.Height))
|
||||
if barsize < 1 {
|
||||
barsize = 1
|
||||
}
|
||||
barstart := w.Y + int(float64(w.StartLine)/float64(w.Buf.LinesNum())*float64(w.Height))
|
||||
for y := barstart; y < util.Min(barstart+barsize, w.Y+bufHeight); y++ {
|
||||
screen.SetContent(scrollX, y, '|', nil, config.DefStyle.Reverse(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Display displays the buffer and the statusline
|
||||
func (w *BufWindow) Display() {
|
||||
w.displayStatusLine()
|
||||
w.displayScrollBar()
|
||||
w.displayBuffer()
|
||||
}
|
||||
293
internal/display/infowindow.go
Normal file
293
internal/display/infowindow.go
Normal file
@@ -0,0 +1,293 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/info"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
"github.com/zyedidia/tcell"
|
||||
)
|
||||
|
||||
type InfoWindow struct {
|
||||
*info.InfoBuf
|
||||
*View
|
||||
|
||||
hscroll int
|
||||
}
|
||||
|
||||
func (i *InfoWindow) errStyle() tcell.Style {
|
||||
errStyle := config.DefStyle.
|
||||
Foreground(tcell.ColorBlack).
|
||||
Background(tcell.ColorMaroon)
|
||||
|
||||
if _, ok := config.Colorscheme["error-message"]; ok {
|
||||
errStyle = config.Colorscheme["error-message"]
|
||||
}
|
||||
|
||||
return errStyle
|
||||
}
|
||||
|
||||
func (i *InfoWindow) defStyle() tcell.Style {
|
||||
defStyle := config.DefStyle
|
||||
|
||||
if _, ok := config.Colorscheme["message"]; ok {
|
||||
defStyle = config.Colorscheme["message"]
|
||||
}
|
||||
|
||||
return defStyle
|
||||
}
|
||||
|
||||
func NewInfoWindow(b *info.InfoBuf) *InfoWindow {
|
||||
iw := new(InfoWindow)
|
||||
iw.InfoBuf = b
|
||||
iw.View = new(View)
|
||||
|
||||
iw.Width, iw.Y = screen.Screen.Size()
|
||||
iw.Y--
|
||||
|
||||
return iw
|
||||
}
|
||||
|
||||
func (i *InfoWindow) Resize(w, h int) {
|
||||
i.Width = w
|
||||
i.Y = h
|
||||
}
|
||||
|
||||
func (i *InfoWindow) SetBuffer(b *buffer.Buffer) {
|
||||
i.InfoBuf.Buffer = b
|
||||
}
|
||||
|
||||
func (i *InfoWindow) Relocate() bool { return false }
|
||||
func (i *InfoWindow) GetView() *View { return i.View }
|
||||
func (i *InfoWindow) SetView(v *View) {}
|
||||
func (i *InfoWindow) SetActive(b bool) {}
|
||||
func (i *InfoWindow) IsActive() bool { return true }
|
||||
|
||||
func (i *InfoWindow) LocFromVisual(vloc buffer.Loc) buffer.Loc {
|
||||
c := i.Buffer.GetActiveCursor()
|
||||
l := i.Buffer.LineBytes(0)
|
||||
n := utf8.RuneCountInString(i.Msg)
|
||||
return buffer.Loc{c.GetCharPosInLine(l, vloc.X-n), 0}
|
||||
}
|
||||
|
||||
func (i *InfoWindow) Clear() {
|
||||
for x := 0; x < i.Width; x++ {
|
||||
screen.SetContent(x, i.Y, ' ', nil, i.defStyle())
|
||||
}
|
||||
}
|
||||
|
||||
func (i *InfoWindow) displayBuffer() {
|
||||
b := i.Buffer
|
||||
line := b.LineBytes(0)
|
||||
activeC := b.GetActiveCursor()
|
||||
|
||||
blocX := 0
|
||||
vlocX := utf8.RuneCountInString(i.Msg)
|
||||
|
||||
tabsize := 4
|
||||
line, nColsBeforeStart, bslice := util.SliceVisualEnd(line, blocX, tabsize)
|
||||
blocX = bslice
|
||||
|
||||
draw := func(r rune, style tcell.Style) {
|
||||
if nColsBeforeStart <= 0 {
|
||||
bloc := buffer.Loc{X: blocX, Y: 0}
|
||||
if activeC.HasSelection() &&
|
||||
(bloc.GreaterEqual(activeC.CurSelection[0]) && bloc.LessThan(activeC.CurSelection[1]) ||
|
||||
bloc.LessThan(activeC.CurSelection[0]) && bloc.GreaterEqual(activeC.CurSelection[1])) {
|
||||
// The current character is selected
|
||||
style = i.defStyle().Reverse(true)
|
||||
|
||||
if s, ok := config.Colorscheme["selection"]; ok {
|
||||
style = s
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
rw := runewidth.RuneWidth(r)
|
||||
for j := 0; j < rw; j++ {
|
||||
c := r
|
||||
if j > 0 {
|
||||
c = ' '
|
||||
}
|
||||
screen.SetContent(vlocX, i.Y, c, nil, style)
|
||||
}
|
||||
vlocX++
|
||||
}
|
||||
nColsBeforeStart--
|
||||
}
|
||||
|
||||
totalwidth := blocX - nColsBeforeStart
|
||||
for len(line) > 0 {
|
||||
curVX := vlocX
|
||||
curBX := blocX
|
||||
r, size := utf8.DecodeRune(line)
|
||||
|
||||
draw(r, i.defStyle())
|
||||
|
||||
width := 0
|
||||
|
||||
char := ' '
|
||||
switch r {
|
||||
case '\t':
|
||||
ts := tabsize - (totalwidth % tabsize)
|
||||
width = ts
|
||||
default:
|
||||
width = runewidth.RuneWidth(r)
|
||||
char = '@'
|
||||
}
|
||||
|
||||
blocX++
|
||||
line = line[size:]
|
||||
|
||||
// Draw any extra characters either spaces for tabs or @ for incomplete wide runes
|
||||
if width > 1 {
|
||||
for j := 1; j < width; j++ {
|
||||
draw(char, i.defStyle())
|
||||
}
|
||||
}
|
||||
if activeC.X == curBX {
|
||||
screen.ShowCursor(curVX, i.Y)
|
||||
}
|
||||
totalwidth += width
|
||||
if vlocX >= i.Width {
|
||||
break
|
||||
}
|
||||
}
|
||||
if activeC.X == blocX {
|
||||
screen.ShowCursor(vlocX, i.Y)
|
||||
}
|
||||
}
|
||||
|
||||
var keydisplay = []string{"^Q Quit, ^S Save, ^O Open, ^G Help, ^E Command Bar, ^K Cut Line", "^F Find, ^Z Undo, ^Y Redo, ^A Select All, ^D Duplicate Line, ^T New Tab"}
|
||||
|
||||
func (i *InfoWindow) displayKeyMenu() {
|
||||
// TODO: maybe make this based on the actual keybindings
|
||||
|
||||
for y := 0; y < len(keydisplay); y++ {
|
||||
for x := 0; x < i.Width; x++ {
|
||||
if x < len(keydisplay[y]) {
|
||||
screen.SetContent(x, i.Y-len(keydisplay)+y, rune(keydisplay[y][x]), nil, i.defStyle())
|
||||
} else {
|
||||
screen.SetContent(x, i.Y-len(keydisplay)+y, ' ', nil, i.defStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *InfoWindow) totalSize() int {
|
||||
sum := 0
|
||||
for _, n := range i.Suggestions {
|
||||
sum += runewidth.StringWidth(n) + 1
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func (i *InfoWindow) scrollToSuggestion() {
|
||||
x := 0
|
||||
s := i.totalSize()
|
||||
|
||||
for j, n := range i.Suggestions {
|
||||
c := utf8.RuneCountInString(n)
|
||||
if j == i.CurSuggestion {
|
||||
if x+c >= i.hscroll+i.Width {
|
||||
i.hscroll = util.Clamp(x+c+1-i.Width, 0, s-i.Width)
|
||||
} else if x < i.hscroll {
|
||||
i.hscroll = util.Clamp(x-1, 0, s-i.Width)
|
||||
}
|
||||
break
|
||||
}
|
||||
x += c + 1
|
||||
}
|
||||
|
||||
if s-i.Width <= 0 {
|
||||
i.hscroll = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (i *InfoWindow) Display() {
|
||||
if i.HasPrompt || config.GlobalSettings["infobar"].(bool) {
|
||||
i.Clear()
|
||||
x := 0
|
||||
if config.GetGlobalOption("keymenu").(bool) {
|
||||
i.displayKeyMenu()
|
||||
}
|
||||
|
||||
if !i.HasPrompt && !i.HasMessage && !i.HasError {
|
||||
return
|
||||
}
|
||||
i.Clear()
|
||||
style := i.defStyle()
|
||||
|
||||
if i.HasError {
|
||||
style = i.errStyle()
|
||||
}
|
||||
|
||||
display := i.Msg
|
||||
for _, c := range display {
|
||||
screen.SetContent(x, i.Y, c, nil, style)
|
||||
x += runewidth.RuneWidth(c)
|
||||
}
|
||||
|
||||
if i.HasPrompt {
|
||||
i.displayBuffer()
|
||||
}
|
||||
}
|
||||
|
||||
if i.HasSuggestions && len(i.Suggestions) > 1 {
|
||||
i.scrollToSuggestion()
|
||||
|
||||
x := -i.hscroll
|
||||
done := false
|
||||
|
||||
statusLineStyle := config.DefStyle.Reverse(true)
|
||||
if style, ok := config.Colorscheme["statusline"]; ok {
|
||||
statusLineStyle = style
|
||||
}
|
||||
keymenuOffset := 0
|
||||
if config.GetGlobalOption("keymenu").(bool) {
|
||||
keymenuOffset = len(keydisplay)
|
||||
}
|
||||
|
||||
draw := func(r rune, s tcell.Style) {
|
||||
y := i.Y - keymenuOffset - 1
|
||||
rw := runewidth.RuneWidth(r)
|
||||
for j := 0; j < rw; j++ {
|
||||
c := r
|
||||
if j > 0 {
|
||||
c = ' '
|
||||
}
|
||||
|
||||
if x == i.Width-1 && !done {
|
||||
screen.SetContent(i.Width-1, y, '>', nil, s)
|
||||
x++
|
||||
break
|
||||
} else if x == 0 && i.hscroll > 0 {
|
||||
screen.SetContent(0, y, '<', nil, s)
|
||||
} else if x >= 0 && x < i.Width {
|
||||
screen.SetContent(x, y, c, nil, s)
|
||||
}
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
for j, s := range i.Suggestions {
|
||||
style := statusLineStyle
|
||||
if i.CurSuggestion == j {
|
||||
style = style.Reverse(true)
|
||||
}
|
||||
for _, r := range s {
|
||||
draw(r, style)
|
||||
// screen.SetContent(x, i.Y-keymenuOffset-1, r, nil, style)
|
||||
}
|
||||
draw(' ', statusLineStyle)
|
||||
}
|
||||
|
||||
for x < i.Width {
|
||||
draw(' ', statusLineStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
204
internal/display/statusline.go
Normal file
204
internal/display/statusline.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
luar "layeh.com/gopher-luar"
|
||||
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
ulua "github.com/zyedidia/micro/internal/lua"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
// StatusLine represents the information line at the bottom
|
||||
// of each window
|
||||
// It gives information such as filename, whether the file has been
|
||||
// modified, filetype, cursor location
|
||||
type StatusLine struct {
|
||||
Info map[string]func(*buffer.Buffer) string
|
||||
|
||||
win *BufWindow
|
||||
}
|
||||
|
||||
var statusInfo = map[string]func(*buffer.Buffer) string{
|
||||
"filename": func(b *buffer.Buffer) string {
|
||||
return b.GetName()
|
||||
},
|
||||
"line": func(b *buffer.Buffer) string {
|
||||
return strconv.Itoa(b.GetActiveCursor().Y + 1)
|
||||
},
|
||||
"col": func(b *buffer.Buffer) string {
|
||||
return strconv.Itoa(b.GetActiveCursor().X + 1)
|
||||
},
|
||||
"modified": func(b *buffer.Buffer) string {
|
||||
if b.Modified() {
|
||||
return "+ "
|
||||
}
|
||||
if b.Type.Readonly {
|
||||
return "[ro] "
|
||||
}
|
||||
return ""
|
||||
},
|
||||
}
|
||||
|
||||
func SetStatusInfoFnLua(fn string) {
|
||||
luaFn := strings.Split(fn, ".")
|
||||
if len(luaFn) <= 1 {
|
||||
return
|
||||
}
|
||||
plName, plFn := luaFn[0], luaFn[1]
|
||||
pl := config.FindPlugin(plName)
|
||||
if pl == nil {
|
||||
return
|
||||
}
|
||||
statusInfo[fn] = func(b *buffer.Buffer) string {
|
||||
if pl == nil || !pl.IsEnabled() {
|
||||
return ""
|
||||
}
|
||||
val, err := pl.Call(plFn, luar.New(ulua.L, b))
|
||||
if err == nil {
|
||||
if v, ok := val.(lua.LString); !ok {
|
||||
screen.TermMessage(plFn, "should return a string")
|
||||
return ""
|
||||
} else {
|
||||
return string(v)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// NewStatusLine returns a statusline bound to a window
|
||||
func NewStatusLine(win *BufWindow) *StatusLine {
|
||||
s := new(StatusLine)
|
||||
s.win = win
|
||||
return s
|
||||
}
|
||||
|
||||
// FindOpt finds a given option in the current buffer's settings
|
||||
func (s *StatusLine) FindOpt(opt string) interface{} {
|
||||
if val, ok := s.win.Buf.Settings[opt]; ok {
|
||||
return val
|
||||
}
|
||||
return "null"
|
||||
}
|
||||
|
||||
var formatParser = regexp.MustCompile(`\$\(.+?\)`)
|
||||
|
||||
// Display draws the statusline to the screen
|
||||
func (s *StatusLine) Display() {
|
||||
// We'll draw the line at the lowest line in the window
|
||||
y := s.win.Height + s.win.Y - 1
|
||||
|
||||
b := s.win.Buf
|
||||
// autocomplete suggestions (for the buffer, not for the infowindow)
|
||||
if b.HasSuggestions && len(b.Suggestions) > 1 {
|
||||
statusLineStyle := config.DefStyle.Reverse(true)
|
||||
if style, ok := config.Colorscheme["statusline"]; ok {
|
||||
statusLineStyle = style
|
||||
}
|
||||
keymenuOffset := 0
|
||||
if config.GetGlobalOption("keymenu").(bool) {
|
||||
keymenuOffset = len(keydisplay)
|
||||
}
|
||||
x := 0
|
||||
for j, sug := range b.Suggestions {
|
||||
style := statusLineStyle
|
||||
if b.CurSuggestion == j {
|
||||
style = style.Reverse(true)
|
||||
}
|
||||
for _, r := range sug {
|
||||
screen.SetContent(x, y-keymenuOffset, r, nil, style)
|
||||
x++
|
||||
if x >= s.win.Width {
|
||||
return
|
||||
}
|
||||
}
|
||||
screen.SetContent(x, y-keymenuOffset, ' ', nil, statusLineStyle)
|
||||
x++
|
||||
if x >= s.win.Width {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for x < s.win.Width {
|
||||
screen.SetContent(x, y-keymenuOffset, ' ', nil, statusLineStyle)
|
||||
x++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
formatter := func(match []byte) []byte {
|
||||
name := match[2 : len(match)-1]
|
||||
if bytes.HasPrefix(name, []byte("opt")) {
|
||||
option := name[4:]
|
||||
return []byte(fmt.Sprint(s.FindOpt(string(option))))
|
||||
} else if bytes.HasPrefix(name, []byte("bind")) {
|
||||
binding := string(name[5:])
|
||||
for k, v := range config.Bindings {
|
||||
if v == binding {
|
||||
return []byte(k)
|
||||
}
|
||||
}
|
||||
return []byte("null")
|
||||
} else {
|
||||
if fn, ok := statusInfo[string(name)]; ok {
|
||||
return []byte(fn(s.win.Buf))
|
||||
}
|
||||
return []byte{}
|
||||
}
|
||||
}
|
||||
|
||||
leftText := []byte(s.win.Buf.Settings["statusformatl"].(string))
|
||||
leftText = formatParser.ReplaceAllFunc(leftText, formatter)
|
||||
rightText := []byte(s.win.Buf.Settings["statusformatr"].(string))
|
||||
rightText = formatParser.ReplaceAllFunc(rightText, formatter)
|
||||
|
||||
statusLineStyle := config.DefStyle.Reverse(true)
|
||||
if style, ok := config.Colorscheme["statusline"]; ok {
|
||||
statusLineStyle = style
|
||||
}
|
||||
|
||||
leftLen := util.StringWidth(leftText, utf8.RuneCount(leftText), 1)
|
||||
rightLen := util.StringWidth(rightText, utf8.RuneCount(rightText), 1)
|
||||
|
||||
winX := s.win.X
|
||||
for x := 0; x < s.win.Width; x++ {
|
||||
if x < leftLen {
|
||||
r, size := utf8.DecodeRune(leftText)
|
||||
leftText = leftText[size:]
|
||||
rw := runewidth.RuneWidth(r)
|
||||
for j := 0; j < rw; j++ {
|
||||
c := r
|
||||
if j > 0 {
|
||||
c = ' '
|
||||
x++
|
||||
}
|
||||
screen.SetContent(winX+x, y, c, nil, statusLineStyle)
|
||||
}
|
||||
} else if x >= s.win.Width-rightLen && x < rightLen+s.win.Width-rightLen {
|
||||
r, size := utf8.DecodeRune(rightText)
|
||||
rightText = rightText[size:]
|
||||
rw := runewidth.RuneWidth(r)
|
||||
for j := 0; j < rw; j++ {
|
||||
c := r
|
||||
if j > 0 {
|
||||
c = ' '
|
||||
x++
|
||||
}
|
||||
screen.SetContent(winX+x, y, c, nil, statusLineStyle)
|
||||
}
|
||||
} else {
|
||||
screen.SetContent(winX+x, y, ' ', nil, statusLineStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
152
internal/display/tabwindow.go
Normal file
152
internal/display/tabwindow.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/util"
|
||||
)
|
||||
|
||||
type TabWindow struct {
|
||||
Names []string
|
||||
active int
|
||||
Y int
|
||||
Width int
|
||||
hscroll int
|
||||
}
|
||||
|
||||
func NewTabWindow(w int, y int) *TabWindow {
|
||||
tw := new(TabWindow)
|
||||
tw.Width = w
|
||||
tw.Y = y
|
||||
return tw
|
||||
}
|
||||
|
||||
func (w *TabWindow) Resize(width, height int) {
|
||||
w.Width = width
|
||||
}
|
||||
|
||||
func (w *TabWindow) LocFromVisual(vloc buffer.Loc) int {
|
||||
x := -w.hscroll
|
||||
|
||||
for i, n := range w.Names {
|
||||
x++
|
||||
s := utf8.RuneCountInString(n)
|
||||
if vloc.Y == w.Y && vloc.X < x+s {
|
||||
return i
|
||||
}
|
||||
x += s
|
||||
x += 3
|
||||
if x >= w.Width {
|
||||
break
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (w *TabWindow) Scroll(amt int) {
|
||||
w.hscroll += amt
|
||||
s := w.TotalSize()
|
||||
w.hscroll = util.Clamp(w.hscroll, 0, s-w.Width)
|
||||
|
||||
if s-w.Width <= 0 {
|
||||
w.hscroll = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (w *TabWindow) TotalSize() int {
|
||||
sum := 2
|
||||
for _, n := range w.Names {
|
||||
sum += runewidth.StringWidth(n) + 4
|
||||
}
|
||||
return sum - 4
|
||||
}
|
||||
|
||||
func (w *TabWindow) Active() int {
|
||||
return w.active
|
||||
}
|
||||
|
||||
func (w *TabWindow) SetActive(a int) {
|
||||
w.active = a
|
||||
x := 2
|
||||
s := w.TotalSize()
|
||||
|
||||
for i, n := range w.Names {
|
||||
c := utf8.RuneCountInString(n)
|
||||
if i == a {
|
||||
if x+c >= w.hscroll+w.Width {
|
||||
w.hscroll = util.Clamp(x+c+1-w.Width, 0, s-w.Width)
|
||||
} else if x < w.hscroll {
|
||||
w.hscroll = util.Clamp(x-4, 0, s-w.Width)
|
||||
}
|
||||
break
|
||||
}
|
||||
x += c + 4
|
||||
}
|
||||
|
||||
if s-w.Width <= 0 {
|
||||
w.hscroll = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (w *TabWindow) Display() {
|
||||
x := -w.hscroll
|
||||
done := false
|
||||
|
||||
tabBarStyle := config.DefStyle.Reverse(true)
|
||||
if style, ok := config.Colorscheme["tabbar"]; ok {
|
||||
tabBarStyle = style
|
||||
}
|
||||
|
||||
draw := func(r rune, n int) {
|
||||
for i := 0; i < n; i++ {
|
||||
rw := runewidth.RuneWidth(r)
|
||||
for j := 0; j < rw; j++ {
|
||||
c := r
|
||||
if j > 0 {
|
||||
c = ' '
|
||||
}
|
||||
if x == w.Width-1 && !done {
|
||||
screen.SetContent(w.Width-1, w.Y, '>', nil, tabBarStyle)
|
||||
x++
|
||||
break
|
||||
} else if x == 0 && w.hscroll > 0 {
|
||||
screen.SetContent(0, w.Y, '<', nil, tabBarStyle)
|
||||
} else if x >= 0 && x < w.Width {
|
||||
screen.SetContent(x, w.Y, c, nil, tabBarStyle)
|
||||
}
|
||||
x++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, n := range w.Names {
|
||||
if i == w.active {
|
||||
draw('[', 1)
|
||||
} else {
|
||||
draw(' ', 1)
|
||||
}
|
||||
for _, c := range n {
|
||||
draw(c, 1)
|
||||
}
|
||||
if i == len(w.Names)-1 {
|
||||
done = true
|
||||
}
|
||||
if i == w.active {
|
||||
draw(']', 1)
|
||||
draw(' ', 2)
|
||||
} else {
|
||||
draw(' ', 3)
|
||||
}
|
||||
if x >= w.Width {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if x < w.Width {
|
||||
draw(' ', w.Width-x)
|
||||
}
|
||||
}
|
||||
116
internal/display/termwindow.go
Normal file
116
internal/display/termwindow.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/shell"
|
||||
"github.com/zyedidia/tcell"
|
||||
"github.com/zyedidia/terminal"
|
||||
)
|
||||
|
||||
type TermWindow struct {
|
||||
*View
|
||||
*shell.Terminal
|
||||
|
||||
active bool
|
||||
}
|
||||
|
||||
func NewTermWindow(x, y, w, h int, term *shell.Terminal) *TermWindow {
|
||||
tw := new(TermWindow)
|
||||
tw.View = new(View)
|
||||
tw.Terminal = term
|
||||
tw.X, tw.Y = x, y
|
||||
tw.Resize(w, h)
|
||||
return tw
|
||||
}
|
||||
|
||||
// Resize informs the terminal of a resize event
|
||||
func (w *TermWindow) Resize(width, height int) {
|
||||
if config.GetGlobalOption("statusline").(bool) {
|
||||
height--
|
||||
}
|
||||
w.Term.Resize(width, height)
|
||||
w.Width, w.Height = width, height
|
||||
}
|
||||
|
||||
func (w *TermWindow) SetActive(b bool) {
|
||||
w.active = b
|
||||
}
|
||||
|
||||
func (w *TermWindow) IsActive() bool {
|
||||
return w.active
|
||||
}
|
||||
|
||||
func (w *TermWindow) LocFromVisual(vloc buffer.Loc) buffer.Loc {
|
||||
return vloc
|
||||
}
|
||||
|
||||
func (w *TermWindow) Clear() {
|
||||
for y := 0; y < w.Height; y++ {
|
||||
for x := 0; x < w.Width; x++ {
|
||||
screen.SetContent(w.X+x, w.Y+y, ' ', nil, config.DefStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *TermWindow) Relocate() bool { return true }
|
||||
func (w *TermWindow) GetView() *View {
|
||||
return w.View
|
||||
}
|
||||
func (w *TermWindow) SetView(v *View) {
|
||||
w.View = v
|
||||
}
|
||||
|
||||
// Display displays this terminal in a view
|
||||
func (w *TermWindow) Display() {
|
||||
w.State.Lock()
|
||||
defer w.State.Unlock()
|
||||
|
||||
var l buffer.Loc
|
||||
for y := 0; y < w.Height; y++ {
|
||||
for x := 0; x < w.Width; x++ {
|
||||
l.X, l.Y = x, y
|
||||
c, f, b := w.State.Cell(x, y)
|
||||
|
||||
fg, bg := int(f), int(b)
|
||||
if f == terminal.DefaultFG {
|
||||
fg = int(tcell.ColorDefault)
|
||||
}
|
||||
if b == terminal.DefaultBG {
|
||||
bg = int(tcell.ColorDefault)
|
||||
}
|
||||
st := tcell.StyleDefault.Foreground(config.GetColor256(int(fg))).Background(config.GetColor256(int(bg)))
|
||||
|
||||
if l.LessThan(w.Selection[1]) && l.GreaterEqual(w.Selection[0]) || l.LessThan(w.Selection[0]) && l.GreaterEqual(w.Selection[1]) {
|
||||
st = st.Reverse(true)
|
||||
}
|
||||
|
||||
screen.SetContent(w.X+x, w.Y+y, c, nil, st)
|
||||
}
|
||||
}
|
||||
if config.GetGlobalOption("statusline").(bool) {
|
||||
statusLineStyle := config.DefStyle.Reverse(true)
|
||||
if style, ok := config.Colorscheme["statusline"]; ok {
|
||||
statusLineStyle = style
|
||||
}
|
||||
|
||||
text := []byte(w.Name())
|
||||
textLen := utf8.RuneCount(text)
|
||||
for x := 0; x < w.Width; x++ {
|
||||
if x < textLen {
|
||||
r, size := utf8.DecodeRune(text)
|
||||
text = text[size:]
|
||||
screen.SetContent(w.X+x, w.Y+w.Height, r, nil, statusLineStyle)
|
||||
} else {
|
||||
screen.SetContent(w.X+x, w.Y+w.Height, ' ', nil, statusLineStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
if w.State.CursorVisible() && w.active {
|
||||
curx, cury := w.State.Cursor()
|
||||
screen.ShowCursor(curx+w.X, cury+w.Y)
|
||||
}
|
||||
}
|
||||
74
internal/display/uiwindow.go
Normal file
74
internal/display/uiwindow.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
"github.com/zyedidia/micro/internal/screen"
|
||||
"github.com/zyedidia/micro/internal/views"
|
||||
)
|
||||
|
||||
type UIWindow struct {
|
||||
root *views.Node
|
||||
}
|
||||
|
||||
func NewUIWindow(n *views.Node) *UIWindow {
|
||||
uw := new(UIWindow)
|
||||
uw.root = n
|
||||
return uw
|
||||
}
|
||||
|
||||
func (w *UIWindow) drawNode(n *views.Node) {
|
||||
cs := n.Children()
|
||||
dividerStyle := config.DefStyle
|
||||
if style, ok := config.Colorscheme["divider"]; ok {
|
||||
dividerStyle = style
|
||||
}
|
||||
|
||||
for i, c := range cs {
|
||||
if c.IsLeaf() && c.Kind == views.STVert {
|
||||
if i != len(cs)-1 {
|
||||
for h := 0; h < c.H; h++ {
|
||||
screen.SetContent(c.X+c.W, c.Y+h, '|', nil, dividerStyle.Reverse(true))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
w.drawNode(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *UIWindow) Display() {
|
||||
w.drawNode(w.root)
|
||||
}
|
||||
|
||||
func (w *UIWindow) GetMouseSplitID(vloc buffer.Loc) uint64 {
|
||||
var mouseLoc func(*views.Node) uint64
|
||||
mouseLoc = func(n *views.Node) uint64 {
|
||||
cs := n.Children()
|
||||
for i, c := range cs {
|
||||
if c.Kind == views.STVert {
|
||||
if i != len(cs)-1 {
|
||||
if vloc.X == c.X+c.W && vloc.Y >= c.Y && vloc.Y < c.Y+c.H {
|
||||
return c.ID()
|
||||
}
|
||||
}
|
||||
} else if c.Kind == views.STHoriz {
|
||||
if i != len(cs)-1 {
|
||||
if vloc.Y == c.Y+c.H-1 && vloc.X >= c.X && vloc.X < c.X+c.W {
|
||||
return c.ID()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, c := range cs {
|
||||
m := mouseLoc(c)
|
||||
if m != 0 {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return mouseLoc(w.root)
|
||||
}
|
||||
func (w *UIWindow) Resize(width, height int) {}
|
||||
func (w *UIWindow) SetActive(b bool) {}
|
||||
32
internal/display/window.go
Normal file
32
internal/display/window.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package display
|
||||
|
||||
import (
|
||||
"github.com/zyedidia/micro/internal/buffer"
|
||||
)
|
||||
|
||||
type View struct {
|
||||
X, Y int // X,Y location of the view
|
||||
Width, Height int // Width and height of the view
|
||||
|
||||
// Start line and start column of the view (vertical/horizontal scroll)
|
||||
// note that since the starting column of every line is different if the view
|
||||
// is scrolled, StartCol is a visual index (will be the same for every line)
|
||||
StartLine, StartCol int
|
||||
}
|
||||
|
||||
type Window interface {
|
||||
Display()
|
||||
Clear()
|
||||
Relocate() bool
|
||||
GetView() *View
|
||||
SetView(v *View)
|
||||
LocFromVisual(vloc buffer.Loc) buffer.Loc
|
||||
Resize(w, h int)
|
||||
SetActive(b bool)
|
||||
IsActive() bool
|
||||
}
|
||||
|
||||
type BWindow interface {
|
||||
Window
|
||||
SetBuffer(b *buffer.Buffer)
|
||||
}
|
||||
18
internal/info/gutter.go
Normal file
18
internal/info/gutter.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package info
|
||||
|
||||
// A GutterMessage is a message displayed on the side of the editor
|
||||
type GutterMessage struct {
|
||||
lineNum int
|
||||
msg string
|
||||
kind int
|
||||
}
|
||||
|
||||
// These are the different types of messages
|
||||
const (
|
||||
// GutterInfo represents a simple info message
|
||||
GutterInfo = iota
|
||||
// GutterWarning represents a compiler warning
|
||||
GutterWarning
|
||||
// GutterError represents a compiler error
|
||||
GutterError
|
||||
)
|
||||
80
internal/info/history.go
Normal file
80
internal/info/history.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package info
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/zyedidia/micro/internal/config"
|
||||
)
|
||||
|
||||
// LoadHistory attempts to load user history from configDir/buffers/history
|
||||
// into the history map
|
||||
// The savehistory option must be on
|
||||
func (i *InfoBuf) LoadHistory() {
|
||||
if config.GetGlobalOption("savehistory").(bool) {
|
||||
file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", "history"))
|
||||
defer file.Close()
|
||||
var decodedMap map[string][]string
|
||||
if err == nil {
|
||||
decoder := gob.NewDecoder(file)
|
||||
err = decoder.Decode(&decodedMap)
|
||||
|
||||
if err != nil {
|
||||
i.Error("Error loading history:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if decodedMap != nil {
|
||||
i.History = decodedMap
|
||||
} else {
|
||||
i.History = make(map[string][]string)
|
||||
}
|
||||
} else {
|
||||
i.History = make(map[string][]string)
|
||||
}
|
||||
}
|
||||
|
||||
// SaveHistory saves the user's command history to configDir/buffers/history
|
||||
// only if the savehistory option is on
|
||||
func (i *InfoBuf) SaveHistory() {
|
||||
if config.GetGlobalOption("savehistory").(bool) {
|
||||
// Don't save history past 100
|
||||
for k, v := range i.History {
|
||||
if len(v) > 100 {
|
||||
i.History[k] = v[len(i.History[k])-100:]
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.Create(filepath.Join(config.ConfigDir, "buffers", "history"))
|
||||
defer file.Close()
|
||||
if err == nil {
|
||||
encoder := gob.NewEncoder(file)
|
||||
|
||||
err = encoder.Encode(i.History)
|
||||
if err != nil {
|
||||
i.Error("Error saving history:", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpHistory fetches the previous item in the history
|
||||
func (i *InfoBuf) UpHistory(history []string) {
|
||||
if i.HistoryNum > 0 && i.HasPrompt && !i.HasYN {
|
||||
i.HistoryNum--
|
||||
i.Replace(i.Start(), i.End(), history[i.HistoryNum])
|
||||
i.Buffer.GetActiveCursor().GotoLoc(i.End())
|
||||
}
|
||||
}
|
||||
|
||||
// DownHistory fetches the next item in the history
|
||||
func (i *InfoBuf) DownHistory(history []string) {
|
||||
if i.HistoryNum < len(history)-1 && i.HasPrompt && !i.HasYN {
|
||||
i.HistoryNum++
|
||||
i.Replace(i.Start(), i.End(), history[i.HistoryNum])
|
||||
i.Buffer.GetActiveCursor().GotoLoc(i.End())
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user