Compare commits

...

340 Commits

Author SHA1 Message Date
Zachary Yedidia
ff6f28e366 Autocompletion fix for infobuffer 2019-12-25 17:05:11 -05:00
Zachary Yedidia
4951f155ea Support for more complex action chaining 2019-12-25 17:05:11 -05:00
Zachary Yedidia
94ff79e7b2 Lua prompt support and plugin improvements 2019-12-25 17:05:11 -05:00
Zachary Yedidia
3b306c1d3b Better softwrap 2019-12-25 17:05:11 -05:00
Zachary Yedidia
432f1f3363 Minor relocate improvement 2019-12-25 17:05:11 -05:00
Zachary Yedidia
93734f5668 Fix highlighting issue 2019-12-25 17:05:11 -05:00
Zachary Yedidia
b527e4fe42 Reoragnize slightly 2019-12-25 17:05:11 -05:00
Zachary Yedidia
3f22501b1a Improved save with sudo 2019-12-25 17:05:11 -05:00
Zachary Yedidia
fc706bc404 No backups for no name files 2019-12-25 17:05:11 -05:00
Zachary Yedidia
c4d5d7c195 Better backup behavior 2019-12-25 17:05:11 -05:00
Zachary Yedidia
a9bb1f35da Improve selection display 2019-12-25 17:05:11 -05:00
Zachary Yedidia
04e5acb1f8 Minor highlighting fixes 2019-12-25 17:05:11 -05:00
Zachary Yedidia
e42cf3663b Backup support 2019-12-25 17:05:11 -05:00
Zachary Yedidia
a86a6c464e Start implementing backup system 2019-12-25 17:05:11 -05:00
Zachary Yedidia
88b8fc713d Proper scrollbar location for hsplits 2019-12-25 17:05:11 -05:00
Zachary Yedidia
9127152d93 Fix goto issue 2019-12-25 17:05:11 -05:00
Zachary Yedidia
dde52132cf Update tcell version 2019-12-25 17:05:11 -05:00
Zachary Yedidia
ba594abfad Clearer status bar 2019-12-25 17:05:11 -05:00
Zachary Yedidia
5075c91fd4 Fix rebase issue 2019-12-25 17:05:11 -05:00
Zachary Yedidia
5e28ed4271 Add textfilter command 2019-12-25 17:05:11 -05:00
Zachary Yedidia
d29994ada9 Close file 2019-12-25 17:05:11 -05:00
Zachary Yedidia
7f32d31108 Fix plugin names 2019-12-25 17:05:11 -05:00
Zachary Yedidia
aa66435353 Better plugin docs 2019-12-25 17:05:11 -05:00
Zachary Yedidia
e79869978b Use plugin name defined in info and require it to be an identifier 2019-12-25 17:05:11 -05:00
Zachary Yedidia
b41fc10b8f Update some docs 2019-12-25 17:05:11 -05:00
Zachary Yedidia
5dfaaf8856 Update runtime 2019-12-25 17:05:11 -05:00
Zachary Yedidia
4dccfc095d Add visual scroll bar 2019-12-25 17:05:11 -05:00
Zachary Yedidia
bc3f845c0d Remove semver from rebase 2019-12-25 17:05:11 -05:00
Zachary Yedidia
6f6b263d10 Add some plugin functions 2019-12-25 17:05:11 -05:00
Zachary Yedidia
b68461cf72 Terminal plugin callback support 2019-12-25 17:05:11 -05:00
Zachary Yedidia
199d65017f Auto init settings if config doesn't exist 2019-12-25 17:05:11 -05:00
Zachary Yedidia
d2f8adb8ff Support multiactions 2019-12-25 17:05:11 -05:00
Zachary Yedidia
5b18edf865 Small improvement for replace command 2019-12-25 17:05:11 -05:00
Zachary Yedidia
ac3a5154c0 Update version tool to support rc versions 2019-12-25 17:05:11 -05:00
Zachary Yedidia
adaddba696 Add plugin info.json support 2019-12-25 17:05:11 -05:00
Zachary Yedidia
26c545267d Support column marking in linter 2019-12-25 17:05:11 -05:00
Zachary Yedidia
3d40e91690 Add log and plugin list command 2019-12-25 17:05:11 -05:00
Zachary Yedidia
7217911c3a Add macro and QuitAll support 2019-12-25 17:05:11 -05:00
Zachary Yedidia
24eb6fee25 Add buftype access for plugins 2019-12-25 17:05:11 -05:00
Zachary Yedidia
65cd6c4605 Fix minor matchbrace issue 2019-12-25 17:05:11 -05:00
Zachary Yedidia
d1e713ce08 Add better matchbrace 2019-12-25 17:05:11 -05:00
Zachary Yedidia
f39a916e5f Fix minor autosave race condition 2019-12-25 17:05:11 -05:00
Zachary Yedidia
c0293b5d0e Add autosave option 2019-12-25 17:05:11 -05:00
Zachary Yedidia
bc6dd990e5 Improve gutter messages 2019-12-25 17:05:11 -05:00
Zachary Yedidia
ccb5904591 Add mkparents option 2019-12-25 17:05:11 -05:00
Zachary Yedidia
9eed8bc247 Remove local settings 2019-12-25 17:05:11 -05:00
Zachary Yedidia
763e635fea Add literate plugin support 2019-12-25 17:05:11 -05:00
Zachary Yedidia
e18f6f832f Add goto command 2019-12-25 17:05:10 -05:00
Zachary Yedidia
fc4811c1ab Add comment plugin support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
be136a4648 Full extensible linter support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
4027081e0e Add linter plugin support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
e7e0272968 Jobs and gutter messages for plugins 2019-12-25 17:05:10 -05:00
Zachary Yedidia
e3ae38e54a Autoclose plugin support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
a47e1f0ca5 Allow any plugin to be enabled or disabled via settings 2019-12-25 17:05:10 -05:00
Zachary Yedidia
576036f251 Update ftoptions and statusline plugin configuration options 2019-12-25 17:05:10 -05:00
Zachary Yedidia
23a76e1381 Add indentchar option 2019-12-25 17:05:10 -05:00
Zachary Yedidia
55e33badd0 Add readonly option 2019-12-25 17:05:10 -05:00
Zachary Yedidia
5bd54747b3 Fix history for YN prompt 2019-12-25 17:05:10 -05:00
Zachary Yedidia
bf15f5c585 Support filetype option as command line option 2019-12-25 17:05:10 -05:00
Zachary Yedidia
809b95d290 Add reset command and statusline format string options 2019-12-25 17:05:10 -05:00
Zachary Yedidia
8d85cae4c0 Add autocomplete 2019-12-25 17:05:10 -05:00
Zachary Yedidia
a5cf06026a Fix infobar style 2019-12-25 17:05:10 -05:00
Zachary Yedidia
7cd5024e34 Small fixes 2019-12-25 17:05:10 -05:00
Zachary Yedidia
0f4f60c018 Update docs 2019-12-25 17:05:10 -05:00
Zachary Yedidia
aa305c2676 Implement buffer opening at a location 2019-12-25 17:05:10 -05:00
Zachary Yedidia
aa774164a7 Fix relocate bug 2019-12-25 17:05:10 -05:00
Zachary Yedidia
47a129b70f Unicode support improvement 2019-12-25 17:05:10 -05:00
Zachary Yedidia
c93d7a1b35 Add hidehelp support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
995e1dc704 Add tabmovement support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
adfeaf52ba Fix serialization 2019-12-25 17:05:10 -05:00
Zachary Yedidia
f5f4154d4c Fix some search bugs 2019-12-25 17:05:10 -05:00
Zachary Yedidia
74ee256260 Revert "Some plugin helpers"
This reverts commit 75f9d7d9122f5b475c4ff323cca7cc068ea4e411.
2019-12-25 17:05:10 -05:00
Zachary Yedidia
d45f8b4d23 Some plugin helpers 2019-12-25 17:05:10 -05:00
Zachary Yedidia
3335f377a9 Some plugin callbacks 2019-12-25 17:05:10 -05:00
Zachary Yedidia
5ab6c9795f Load plugins 2019-12-25 17:05:10 -05:00
Zachary Yedidia
15dff722b0 Remove plugin manager 2019-12-25 17:05:10 -05:00
Zachary Yedidia
a2b9acd153 Some plugin manager improvements 2019-12-25 17:05:10 -05:00
Zachary Yedidia
4497daaef1 Resolve versions in plugin manager 2019-12-25 17:05:10 -05:00
Zachary Yedidia
cf2d5dbfe2 update travis 2019-12-25 17:05:10 -05:00
Zachary Yedidia
739dd28652 Fix test dependencies and travis build 2019-12-25 17:05:10 -05:00
Zachary Yedidia
39446df749 update makefile 2019-12-25 17:05:10 -05:00
Zachary Yedidia
7cd83b4361 Fix tooling dependencies 2019-12-25 17:05:10 -05:00
Zachary Yedidia
0612af1590 Change project layout and use go.mod 2019-12-25 17:05:10 -05:00
Zachary Yedidia
c7f2c9c704 More plugin manager work 2019-12-25 17:05:10 -05:00
Zachary Yedidia
f4a3465a08 Start plugin support and plugin manager 2019-12-25 17:05:10 -05:00
Zachary Yedidia
453e96358a Fix option flags 2019-12-25 17:05:10 -05:00
Zachary Yedidia
b97ded9058 Fix view relocate bug 2019-12-25 17:05:10 -05:00
Zachary Yedidia
253790de99 Sort suggestions and cycle back 2019-12-25 17:05:10 -05:00
Zachary Yedidia
ef18fc572c Add more option support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
0e4faf108d Finish autocomplete 2019-12-25 17:05:10 -05:00
Zachary Yedidia
ad487807a5 Remove chardet dependency 2019-12-25 17:05:10 -05:00
Zachary Yedidia
ad50d7aa56 Add reopen cmd and other encodings support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
ef3f081347 Add colorcolumn 2019-12-25 17:05:10 -05:00
Zachary Yedidia
bc1d6b6f94 Add more infobar autocomplete 2019-12-25 17:05:10 -05:00
Zachary Yedidia
fc7058d47c Add infobar autocomplete 2019-12-25 17:05:10 -05:00
Zachary Yedidia
ab37e6ad6c Add support for binding command and command-edit 2019-12-25 17:05:10 -05:00
Zachary Yedidia
4bdf788091 Add replace all alias 2019-12-25 17:05:10 -05:00
Zachary Yedidia
8c687e8279 Support raw pane 2019-12-25 17:05:10 -05:00
Zachary Yedidia
9336e09532 Revert "Use byte slice for insert"
This reverts commit 0c844c2f5b.
2019-12-25 17:05:10 -05:00
Zachary Yedidia
069f7d20bc Add save and save as 2019-12-25 17:05:10 -05:00
Zachary Yedidia
212b0f8c71 Add keymenu 2019-12-25 17:05:10 -05:00
Zachary Yedidia
254b892a3b Fix multi cursor relocate 2019-12-25 17:05:10 -05:00
Zachary Yedidia
1a710272f8 Prompt trim fix 2019-12-25 17:05:10 -05:00
Zachary Yedidia
a3885bfb12 Add search and replace 2019-12-25 17:05:10 -05:00
Zachary Yedidia
df968db5a3 Proper help toggle 2019-12-25 17:05:10 -05:00
Zachary Yedidia
538f0117bc Fix yn callback bug 2019-12-25 17:05:10 -05:00
Zachary Yedidia
4a5b759f16 Fix fileformat 2019-12-25 17:05:10 -05:00
Zachary Yedidia
3380170af8 Add retab 2019-12-25 17:05:10 -05:00
Zachary Yedidia
467d384789 Add more actions 2019-12-25 17:05:10 -05:00
Zachary Yedidia
1563ab93dd Use byte slice for insert 2019-12-25 17:05:10 -05:00
Zachary Yedidia
812c7761dc Correct infobar and statusline options 2019-12-25 17:05:10 -05:00
Zachary Yedidia
055fff2b08 Fix redraw 2019-12-25 17:05:10 -05:00
Zachary Yedidia
5671e039b9 Fix multi buffer same file cursors 2019-12-25 17:05:10 -05:00
Zachary Yedidia
224cbe5093 Add help 2019-12-25 17:05:10 -05:00
Zachary Yedidia
eb49052a48 Add bind and unbind commands 2019-12-25 17:05:10 -05:00
Zachary Yedidia
5825353f64 Add some commands 2019-12-25 17:05:10 -05:00
Zachary Yedidia
8fa34f23d8 Handle same file open in multiple buffers 2019-12-25 17:05:10 -05:00
Zachary Yedidia
a5e7122b30 Add almost full option support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
6c1db53b65 Fix scroll problem 2019-12-25 17:05:10 -05:00
Zachary Yedidia
b9f7939018 Add term statusline 2019-12-25 17:05:10 -05:00
Zachary Yedidia
5701ed211a Fix empty splits and single terms 2019-12-25 17:05:10 -05:00
Zachary Yedidia
8858c03b3b Add raw event support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
2f7858ce25 Gutter message support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
94ab77e2e0 Fix mouse bug 2019-12-25 17:05:10 -05:00
Zachary Yedidia
fb3923f344 Open default shell if no term args 2019-12-25 17:05:10 -05:00
Zachary Yedidia
354c9efc8f Move bindings location in code 2019-12-25 17:05:10 -05:00
Zachary Yedidia
149b3ae89f Fix small tab problem 2019-12-25 17:05:10 -05:00
Zachary Yedidia
0f1483dc8c Almost done terminal emulator 2019-12-25 17:05:10 -05:00
Zachary Yedidia
4146730aaf Start terminal emulator 2019-12-25 17:05:10 -05:00
Zachary Yedidia
c479c9d91a Add shell command support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
0febfd2c80 Better tab mUI 2019-12-25 17:05:10 -05:00
Zachary Yedidia
eec4e535b4 Add tabbar and tab mouse support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
8aa05cf409 Begin tab implementation 2019-12-25 17:05:10 -05:00
Zachary Yedidia
fe773c00d2 Implement split resizing 2019-12-25 17:05:10 -05:00
Zachary Yedidia
f2cb7d2fc1 Implement unsplitting 2019-12-25 17:05:10 -05:00
Zachary Yedidia
4412b44b47 Add showkey 2019-12-25 17:05:10 -05:00
Zachary Yedidia
9cf283e312 Resizing work 2019-12-25 17:05:10 -05:00
Zachary Yedidia
305f4debff Split improvements 2019-12-25 17:05:10 -05:00
Zachary Yedidia
93aed1ab9f Fix some split bugs 2019-12-25 17:05:10 -05:00
Zachary Yedidia
778bfd5cd3 Merge cursors after any event 2019-12-25 17:05:10 -05:00
Zachary Yedidia
16e5f55323 YN callbacks and better multi cursor 2019-12-25 17:05:10 -05:00
Zachary Yedidia
1ac4a8e7d3 Split improvements 2019-12-25 17:05:10 -05:00
Zachary Yedidia
541daf212e Start working on splits 2019-12-25 17:05:10 -05:00
Zachary Yedidia
d4c410f3dc Infobar history 2019-12-25 17:05:10 -05:00
Zachary Yedidia
4b50599411 Complete multicursor support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
6cf09f9843 Find next and prev 2019-12-25 17:05:10 -05:00
Zachary Yedidia
37a4cbfd98 Implement searching 2019-12-25 17:05:10 -05:00
Zachary Yedidia
0f37c0b0bf Add multi cursor support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
80fe992957 Fix infobar prompt 2019-12-25 17:05:10 -05:00
Zachary Yedidia
e97005f05d Working horizontal scrolling 2019-12-25 17:05:10 -05:00
Zachary Yedidia
5335c60d6c Fix sub bug 2019-12-25 17:05:10 -05:00
Zachary Yedidia
b8b245f305 Add mouse support 2019-12-25 17:05:10 -05:00
Zachary Yedidia
3d2cc3298e Finish non global actions 2019-12-25 17:05:10 -05:00
Zachary Yedidia
a89ddea619 Fix error 2019-12-25 17:05:10 -05:00
Zachary Yedidia
6562e3b48d Start implementing commands 2019-12-25 17:05:10 -05:00
Zachary Yedidia
c01995c1b6 Reorganize info bar 2019-12-25 17:05:10 -05:00
Zachary Yedidia
78ce7a5f0f Minor infobar improvements 2019-12-25 17:05:10 -05:00
Zachary Yedidia
afe24698ea Infobar prompts 2019-12-25 17:05:10 -05:00
Zachary Yedidia
c50e0cb932 Add infobar 2019-12-25 17:05:10 -05:00
Zachary Yedidia
e9a4238a3f More actions and view relocation 2019-12-25 17:05:10 -05:00
Zachary Yedidia
02b71a514a Add some comments 2019-12-25 17:05:10 -05:00
Zachary Yedidia
9f066f2fbf Rehighlighting 2019-12-25 17:05:10 -05:00
Zachary Yedidia
12d727fb93 Add some more actions 2019-12-25 17:05:10 -05:00
Zachary Yedidia
31cf5a15ce Fix serialization 2019-12-25 17:05:10 -05:00
Zachary Yedidia
31fb3f2df2 More actions 2019-12-25 17:05:10 -05:00
Zachary Yedidia
7d87e6db99 More actions and window organization 2019-12-25 17:05:10 -05:00
Zachary Yedidia
06d596e780 Synchronize screen 2019-12-25 17:05:10 -05:00
Zachary Yedidia
d7b3f961b4 Action subpackage 2019-12-25 17:05:10 -05:00
Zachary Yedidia
c3e2085e3c Cursor improvements 2019-12-25 17:05:10 -05:00
Zachary Yedidia
dd619b3ff5 Reorganize file structure 2019-12-25 17:05:10 -05:00
Zachary Yedidia
dc68183fc1 Start refactor 2019-12-25 17:05:10 -05:00
Zachary Yedidia
d9735e5c3b Update readme 2019-12-25 16:17:31 -05:00
Zachary Yedidia
bd9307483d Merge pull request #1363 from andradei/patch-1
Separate keys with + sign for consistency
2019-12-19 10:23:48 -05:00
Zachary Yedidia
fd8fc3acfa Merge pull request #1410 from serge-v/textfilter
Add textfilter command
2019-12-19 10:15:49 -05:00
Zachary Yedidia
f932cfb7f1 Merge pull request #1409 from serge-v/syntax
Add mc, godoc, proto syntax files
2019-12-19 10:05:38 -05:00
Zachary Yedidia
8817d711b9 Merge pull request #1415 from dullbananas/master
Improve syntax files
2019-12-19 10:05:30 -05:00
Zachary Yedidia
1956a49f9b Merge pull request #1421 from j-mortara/master
Corrected tex comment start separator
2019-12-19 10:01:28 -05:00
Johann Mortara
5b869cb836 Corrected tex comment start separator 2019-11-29 12:05:54 +01:00
Dull Bananas
166e227c9f Merge branch 'master' into master 2019-11-15 18:46:13 -07:00
Dull Bananas
7d1dc1183c Improve JavaScript syntax 2019-11-15 18:42:17 -07:00
Dull Bananas
4662f0c500 Remove old code 2019-11-15 18:38:33 -07:00
Dull Bananas
78fd9fb225 Add jinja syntax 2019-11-15 18:37:41 -07:00
Serge Voilokov
1857aa4067 Add proto syntax file 2019-11-06 07:23:04 -05:00
Serge Voilokov
7a51490591 Add textfilter command 2019-11-05 23:27:35 -05:00
Serge Voilokov
1fc5b316ab Add mc, godoc syntax files 2019-11-05 22:57:36 -05:00
Isaac Andrade
e9337da43f Separate keys with + sign for consistency
Some textual changes (without changing formatting) were made to table header lines.
This is a tiny and almost inconsequential change to improve readability.
2019-07-31 12:48:51 -06:00
Zachary Yedidia
3a8898dadd Merge pull request #1340 from dullbananas/improve-help
Add detail to help
2019-07-25 19:58:50 -07:00
Zachary Yedidia
1c4e2eb09f Merge pull request #1356 from dullbananas/master
Add missing keywords for Python syntax
2019-07-25 19:58:08 -07:00
Patrick Weingärtner
c11af1c19c ( #1358 ) add vue single-file component syntax highlighting (#1359)
* add vue single-file component syntax highlighting

* remove unnecessary new line
2019-07-25 19:57:53 -07:00
Zachary Yedidia
4f35eed615 Merge pull request #1360 from Lisiadito/js
add TODO to javascript highlighting
2019-07-25 19:57:33 -07:00
Patrick Weingaertner
3a4bdb0db6 add TODO to JS highlighting 2019-07-25 23:22:15 +02:00
Antonino Siena
73093f9497 Added Ziglang syntax support (#1354)
* Added Ziglang syntax support

* Removed unused constant string markup

* Added 'fn' as keyword

* Added constant matching ending with numbers and proper hexadecimal matching

* Added missing types

* Added all keywords in alphabetical order
2019-07-06 01:51:12 -04:00
Zachary Yedidia
3644ef4a5a Merge pull request #1353 from jakobnissen/master
Updated Julia number and string syntax highlighting
2019-07-06 01:50:58 -04:00
Zachary Yedidia
8b4943fc26 Merge pull request #1352 from Akito13/master
Additional file extensions
2019-07-06 01:50:46 -04:00
Zachary Yedidia
a90b17c855 Merge pull request #1331 from corbuntus/master
Dlang character syntax highlighting
2019-07-06 01:50:28 -04:00
Zachary Yedidia
9a8b7ab757 Merge pull request #1310 from coolreader18/elm-syntax
Add Elm syntax file
2019-07-06 01:49:57 -04:00
Dull Bananas
9ec07b595b Add missing keywords for Python syntax 2019-07-03 16:53:53 -07:00
Jakob Nybo Nissen
9bfc35656c Updated Julia number and string syntax highlighting 2019-07-01 12:47:16 +02:00
Akito13
b2b933c6c1 Additional file extensions
* Added support for Nimscript files
2019-07-01 03:30:58 +02:00
Dull Bananas
beea8d42d5 Add detail 2019-05-29 21:17:56 -07:00
corbuntus
957b3aaea7 Fix zyedidia#1330: Dlang character syntax highlighting 2019-05-24 20:12:45 +02:00
coolreader18
7e34eabb0e Add Elm syntax file 2019-04-25 22:45:23 -05:00
Zachary Yedidia
2c219ba647 Merge pull request #1253 from ColinRioux/master
Fixes missing syntax highlighting for TCL
2019-01-04 22:58:35 -05:00
Colin Rioux
ca9b1d7b14 Fixes #1249 2018-12-21 12:50:33 -05:00
Zachary Yedidia
001498eee4 Update runtime 2018-12-10 14:33:21 -05:00
Zachary Yedidia
3c87d1cfb4 Merge pull request #1200 from Calinou/add-systemd-timer-section
Add [Timer] section to systemd highlighting
2018-12-10 14:32:06 -05:00
Zachary Yedidia
54183ec4d2 Merge pull request #1206 from kylebarron/material-colorscheme
Add Material colorscheme
2018-12-10 14:31:48 -05:00
Zachary Yedidia
6f3548e7ce Merge pull request #1223 from piaph/syntax_highlight_files_in_gentoo_portage_folders
Changed regex for Gentoo etc-portage to include detection of folders
2018-12-10 14:31:26 -05:00
Zachary Yedidia
2a0d78b86d Merge pull request #1201 from Calinou/use-more-ini-highlighting
Highlight .tscn, .tres and project.godot files using INI syntax
2018-12-10 14:31:05 -05:00
Zachary Yedidia
ba98e973c4 Merge pull request #1202 from luizbills/patch-1
add 'from' and 'of' keywords in javascript syntax file
2018-12-10 14:30:25 -05:00
Zachary Yedidia
3515f254c4 Merge pull request #1203 from luizbills/patch-2
detect '.mjs' as javascript file
2018-12-10 14:30:08 -05:00
Zachary Yedidia
2823058806 Merge pull request #1220 from yvendruscolo/patch-1
match .edn files
2018-12-10 14:29:53 -05:00
Zachary Yedidia
5c2fc92332 Merge pull request #1205 from kylebarron/python-syntax-fixes
Use symbol.operator and symbol.brackets scopes correctly in Python syntax file
2018-12-10 14:28:21 -05:00
Zachary Yedidia
64a6779482 Merge pull request #1207 from kylebarron/stata-syntax
Add Stata syntax file
2018-12-10 14:28:03 -05:00
Zachary Yedidia
49b6cf3673 Merge pull request #1233 from teresy/simplify-index
simplify cases of strings.Index with strings.Contains
2018-12-10 14:27:45 -05:00
Zachary Yedidia
37bd454679 Merge pull request #1234 from kylebarron/python-docstring
Python syntax: docstring should be string, not comment
2018-12-10 14:27:30 -05:00
Zachary Yedidia
f9e8d8b9a0 Merge pull request #1241 from Danmou/patch-1
Allow more ways to write booleans in YAML
2018-12-10 14:27:16 -05:00
Zachary Yedidia
e289d44034 Merge pull request #1242 from dwwmmn/dwwmmn-erl
Add syntax file for Erlang
2018-12-10 14:26:59 -05:00
Drew Malzahn
2fd85cb033 Add syntax file for Erlang
Syntax hilighting for Erlang. Comment definition taken from:

d953339a56/runtime/syntax/ocaml.yaml
2018-11-23 09:37:51 -05:00
Daniel Mouritzen
8bda0a6b45 Allow more ways to write booleans in YAML
See http://yaml.org/type/bool.html and http://yaml.org/spec/1.2/spec.html#id2805071
2018-11-22 11:00:23 +01:00
Kyle Barron
fd48a3841e Python syntax: docstring should be string, not comment 2018-11-04 12:36:39 -05:00
teresy
a69cc72c9d simplify cases of strings.Index with strings.Contains 2018-11-02 18:57:28 -04:00
Pia Philipsson
57c681eddf Changed filename detection for Gentoo etc-portage to include detection of folders 2018-10-19 09:47:51 +02:00
yvendruscolo
ec6943b1c9 match .edn files
there was no file/match for edn (Clojure's json) files, so that would solve it
2018-10-16 10:39:09 -03:00
Zachary Yedidia
e071a4f8e2 Better bounds checks for search
Fixes #1217
2018-10-14 17:58:44 -04:00
Kyle Barron
04420de96a Add Stata syntax file 2018-10-02 15:46:38 -04:00
Kyle Barron
bfc9d4a195 Add identifier.macro color 2018-10-02 15:45:40 -04:00
Kyle Barron
e3955882e4 Add Material colorscheme 2018-10-02 14:44:13 -04:00
Kyle Barron
e0ce419357 Use symbol.operator and symbol.brackets scopes correctly 2018-10-02 13:54:29 -04:00
Luiz Paulo "Bills
2d0ec82baa add 'of' statement 2018-09-29 23:23:42 -03:00
Luiz Paulo "Bills
a0a154d957 detect '.mjs' as javascript file
`.mjs` extension will be used as ECMAScript Modules
2018-09-29 14:43:10 -03:00
Luiz Paulo "Bills
fa05d63d11 add 'from' in javascript syntax file 2018-09-29 14:40:20 -03:00
Hugo Locurcio
249405355a Add [Timer] section to systemd highlighting 2018-09-29 12:15:50 +02:00
Hugo Locurcio
dab18e2fee Highlight .tscn, .tres and project.godot files using INI syntax
This also removes header detection for INI syntax, which could
occasionally cause other file types (such as systemd service files)
to be detected as INI.
2018-09-29 12:14:15 +02:00
Zachary Yedidia
de35d00ba7 Merge pull request #1194 from MJBrune/patch-1
Added an s to command(s)
2018-09-24 16:22:16 -04:00
Michael Brune
f68149489e Added an s to command(s)
Adding an S seems more intuitive here. The command you are being asked to run there completes to:
`help commands`
not `help command` as one might expect.
Although maybe help aliases might also be something to consider?
2018-09-24 13:12:01 -07:00
Zachary Yedidia
1013b03314 Merge 2018-09-21 23:18:58 -04:00
Zachary Yedidia
96284a1feb LoadAll should reload plugins too
Fixes #1189
2018-09-21 23:18:47 -04:00
Zachary Yedidia
d2b51a59d6 Merge pull request #1173 from sc0ttj/enable-auto-highlighting-for-ash-shell
Update sh.yaml to support Ash scripts
2018-09-03 16:57:50 -04:00
Scott Jarvis
0e56c0c816 Update sh.yaml
support Ash as well as Bash, Sh, Dash.
2018-09-02 11:52:26 +00:00
Zachary Yedidia
f40abc1a59 Fix infocmp parser
Ref #1167
2018-08-29 13:01:38 -04:00
Zachary Yedidia
0a6948c8ac Merge 2018-08-29 12:16:18 -04:00
Zachary Yedidia
9db7991a1d Handle hex codes in infocmp output 2018-08-29 12:16:11 -04:00
Zachary Yedidia
7339afcf73 Add tcelldb error check 2018-08-28 14:26:21 -04:00
Zachary Yedidia
9cbe2c62de Merge pull request #1166 from rexy712/master
Fix UpN to handle going from long line to short line
2018-08-25 19:35:00 -04:00
rexy712
6e9b8c1bd5 Fixed UpN Cursor functionality to properly handle moving from long line to shorter line 2018-08-25 14:49:58 -07:00
Zachary Yedidia
e11d9deb6e Merge pull request #1165 from ev-dev/master
Basic syntax highlighting for the GraphQL language based on the official specification
2018-08-25 17:39:13 -04:00
Zachary Yedidia
1d93433bfb Merge pull request #1148 from Calinou/improve-gdscript-syntax
Improve the GDScript syntax file
2018-08-25 17:38:56 -04:00
Zachary Yedidia
45643f397b Merge pull request #1147 from Calinou/fix-c-keyword-highlighting
Fix some keywords being mistakenly highlighted in C syntax
2018-08-25 17:38:29 -04:00
Visual-Knowledge
33d9b8f60b Basic syntax highlighting for Graphql based on the official specification 2018-08-24 03:25:40 -07:00
Zachary Yedidia
6140dabca8 Merge pull request #1160 from supbish/fix-sh-comment
Fix shell comments; fixes #1114
2018-08-20 21:03:00 -07:00
supbish
27db63433f Fix shell comments; fixes #1114 2018-08-20 16:22:07 -04:00
Zachary Yedidia
bcdab882bc Update runtime 2018-08-18 15:25:42 -07:00
Zachary Yedidia
32b8c51992 Merge pull request #1158 from supbish/lua-syntax
Lua syntax improvements; fixes #1155, fixes #1136
2018-08-18 15:25:03 -07:00
supbish
4be3e9122c Lua syntax improvements; fixes #1155, fixes #1136 2018-08-18 07:00:51 -04:00
Zachary Yedidia
d0f8bede41 Merge pull request #1157 from supbish/smart-paste-indent
Add "smartpaste" option; fixes #1156
2018-08-17 21:23:42 -07:00
supbish
905e984f29 Add "smartpaste" option; fixes #1156 2018-08-17 22:37:19 -04:00
Zachary Yedidia
44e417c2f4 Merge pull request #1154 from supbish/luatabs
Add GetTabs Lua function
2018-08-15 11:56:10 -04:00
supbish
e03fab8daa Add GetTabs Lua function 2018-08-15 11:18:27 -04:00
Camille
1ab493de59 Only show basename of file in tabs unless there are mutliple tabs with the same basename (fixes #1079) (#1081)
* Only show basename of file in tabs unless there are mutliple tabs with the same basename (fixes #1079)

* Small fix
2018-08-10 16:54:19 -04:00
Zachary Yedidia
f56621a4bd Bump version 2018-08-10 13:45:03 -04:00
Hugo Locurcio
497ca2c66b Improve the GDScript syntax file
More keywords are now recognized. Some leftover syntax definitions
from Python 3 that are not allowed in GDScript were also removed.
2018-08-07 15:16:23 +02:00
Hugo Locurcio
18ca06d9be Fix some keywords being mistakenly highlighted in C syntax 2018-08-07 14:44:53 +02:00
Zachary Yedidia
1856891622 Update nightly release script to not duplicate nightlies 2018-07-20 00:24:02 +00:00
djmnzp
8a250f7d95 Update ats syntax (#1141)
* Multiple changes
 - Fixed overlapping between the macros and some statements.
 - Added "t" and "abs" as types.
 - Removed "fun0", "fun1", "clo0", "clo1", ..., "prf" from types and added them to the special block as effects.
 - Added "lin", "lincloptr0" and "lincloptr1" as effects.
 - Added "do" and "static" as statements.
 - Added "tupz!" and "prerr!" to the special block.
 - Fixed some typos.

* Updated regex for exhaustive types

* Final touches

* Removed "t" from types

* Minor fix

* Improved support for floats and integers
Make it comply with https://github.com/Hibou57/PostiATS-Utilities/blob/master/doc/lexemes-guide.md

* Chars are now interpreted as strings
Less troubling when working with '"' inside chars or multiline strings

* Reverted strings and chars from multiline to one line
For some reason, having strings on the same line as other symbols breaks the highlighting on the latter

* Add "ldouble" type
2018-07-16 15:37:57 -04:00
Zachary Yedidia
7a013f666e Update runtime and auto-gofmt runtime in make 2018-07-02 12:22:32 -04:00
Zachary Yedidia
41a24e61d6 Merge pull request #1135 from whilei/gofmt-2018-Jun-17-00-39
gofmt
2018-07-02 12:22:05 -04:00
djmnzp
d953339a56 Added syntax highlighting for ATS (#1137)
* Added syntax highlighting for ATS

* Fixed "////" comment not working as intended
Added a hack to make it impossible to match the end of the comment

* Fixed typo, added '#' and '@' as symbols
2018-07-02 12:19:38 -04:00
ia
76e1d7a3a7 all: gofmt
Run standard gofmt command on project root.

- go version go1.10.3 darwin/amd64

Signed-off-by: ia <isaac.ardis@gmail.com>
2018-06-17 00:41:57 +02:00
Zachary Yedidia
91b65001c9 Fix php syntax file
Fixes #1109
2018-06-04 15:13:58 -04:00
Dimitar Borislavov Tasev
aa74b1233c Fix -startpos flag being ignored (#1129)
* Refactored cursor location login into a function. Fixed buffer overflow when line position is 1 more than file lines

* Fixed crash when -startpos has an invalid argument

* Adapted tests to new interface

* Fixed bug where -startpos with lines 0 and 1 would both be on the first line

* Changed Fatalf format back to digits

* Fixed issues with buffer cursor location. Added tests for new function

* ParseCursorLocation will now return an error when path doesnt contain line/col

* Fixed off-by-one line error

* Fixed tests to account for subtracting 1 from the line index
2018-06-04 12:27:27 -04:00
Zachary Yedidia
61baa73d70 Merge pull request #1125 from nabeelomer/master
F# Configuration
2018-06-03 17:13:22 -04:00
Dimitar Borislavov Tasev
efe343b37c Allows opening files using full path on Windows (#1126)
* Now can open Windows full-path from command line arg

Example that now works: micro.exe D:\myfile.txt

* Now correctly retrieves the path from the input path string. Except for single-letter filenames

* Fixed line/cols, need to make the code prettier

* Fixed path matching with regex by @Pariador

* Fixed not stripping the line/col args from file path

* Added tests for ParseCursorLocation
2018-06-03 17:13:03 -04:00
Nabeel Omer
cc8e9a7e06 F# Configuration 2018-05-29 20:02:58 +05:30
Sean Charles
d7f7d845b9 Elixir configuration (#1118)
* Elixir configuration

* added exunit support

* end added
2018-05-26 10:08:35 -04:00
Zachary Yedidia
8f0418c9a8 Merge pull request #1119 from mbesancon/patch-3
Update julia.yaml
2018-05-26 10:08:19 -04:00
Maxim
71af765b4e Code optimisation (#1117)
* Making sure output files are always closed, plus hash calculation optimisation.

* Parallel hash calculation.

* Minor changes.

* Removed unnecessary memory allocations while trimming trailing whitespace.

* Buffered write.
2018-05-26 10:07:53 -04:00
mbesancon
c0f279ffe8 Update julia.yaml
added struct to keywords
2018-05-25 12:04:12 -04:00
JT Olio
ae9bb763fb a few miscellaneous fixes and improvements (#1105)
* add binding for more primitive backspace

* support selecting page up and page down

* fix matchbraceleft for braces that start on x=0

* fix multiline copy-paste indenting

let's say you have two lines like

  <space><space>line1
  <space><space>line2

so you start from cursor x=0 and select both lines, then paste.
we don't want any leading whitespace in this case, because the
cursor is already at x=0 and the selection already includes
whitespace.
2018-05-12 21:31:57 -04:00
Zachary Yedidia
3c01947cb3 Fix ini comment highlighting
Fixes #1094
2018-05-12 21:29:02 -04:00
Zachary Yedidia
53e142fb88 Fix matchbraceleft option
Fixes #1101
2018-04-28 17:42:17 -04:00
Zachary Yedidia
2e64499f96 Fix possible crash in findkey
Fixes #1103
2018-04-28 17:16:22 -04:00
Zachary Yedidia
11cb702d7f Merge 2018-04-28 17:04:47 -04:00
Zachary Yedidia
7a2820cbc0 Add hidehelp option
Fixes #1080
2018-04-28 17:04:33 -04:00
Mark Weston
b181342ff1 Make ^X act like ^K when nothing is selected (#1092)
* Make ^X act like ^K when nothing is selected

^K is hard to reach with your left hand or requires to use both hands
Also with this you could remove ^K whatsoever and make room for a different command
This is how I configured nano by the way
Line duplication also becomes nearly instantaneous with a flash-quick ^X+^V+^V combo (nano doesn't have a dedicated shortcut)
Small block (5-10 lines) cuts/copies/duplicates can also be made this way

* Remove unnecessary lines

* Call CutLine the right way
2018-04-23 15:34:45 -04:00
Zachary Yedidia
f0e2f3cc96 Merge pull request #1085 from jtolds/themes
darcula: fix highlighted line and color column
2018-04-21 16:57:53 -04:00
Zachary Yedidia
6ef273accd Merge pull request #1084 from jtolds/master
home toggles between start of line and start of text
2018-04-07 20:03:57 -04:00
JT Olio
0eadf283a5 darcula: fix highlighted line and color column 2018-04-05 19:45:28 -06:00
JT Olio
f8a171379a home toggles between start of line and start of text
by default home sends the cursor to the beginning of the line.
if the cursor is at the beginning of the line already though, home
will send the cursor to the first non-whitespace rune. tapping home
will toggle between these two line starts.
2018-04-05 15:25:34 -06:00
Zachary Yedidia
1a62ede320 Build snap using up-to-date golang 2018-04-03 00:53:24 -04:00
Zachary Yedidia
1bb1da4765 Update snapcraft.yaml Go plugin 2018-04-02 21:16:19 -07:00
Zachary Yedidia
987d48038a Update snapcraft.yaml 2018-04-02 21:07:12 -07:00
Zachary Yedidia
abc04ec521 fix typo 2018-03-31 02:32:48 +00:00
Zachary Yedidia
b7706d775c Add docs for SpawnMultiCursorSelect 2018-03-30 16:42:28 -04:00
dwwmmn
ac0b89366b Implement SpawnMultiCursorSelect (#1046)
Add function to actions.go which adds a new cursor to the beginning of each line of a selection. Bind to Ctrl-M by default.
2018-03-30 16:40:45 -04:00
Zachary Yedidia
3293160dcb Fix ReplaceHome implementation 2018-03-30 16:21:39 -04:00
DanielPower
804943a1e8 Add support for ~username syntax (fix #1033) (#1035)
* Add support for ~username syntax (fix #1033)

* Fixed return string

Also removed non-descriptive variable name `foo`

* moved err declarations outside of if statement
2018-03-30 16:20:51 -04:00
Zachary Yedidia
89f50638d7 Merge 2018-03-30 15:59:45 -04:00
Zachary Yedidia
c606c51c8b Close fd properly in save
Fixes #1057
2018-03-30 15:59:26 -04:00
Zachary Yedidia
4bde88d126 Merge pull request #1076 from Velocet/patch-1
Create PowerShell.yaml - PowerShell Syntax Highlighting
2018-03-20 23:22:51 -04:00
Velocet
41bae11c1e Create PowerShell.yaml 2018-03-21 03:58:04 +01:00
Zachary Yedidia
f43a1b5ced Merge pull request #1054 from jtolds/master
allow optional brace matching with the closing brace to the left of the cursor
2018-03-19 00:32:26 -04:00
Zachary Yedidia
219f934656 Merge pull request #1067 from sum01/issue-1066
Fix #1066 php syntax
2018-03-19 00:32:07 -04:00
Zachary Yedidia
26da85dcb1 Fix test string formatting
Fixes #1068
2018-03-09 00:39:59 -05:00
Zachary Yedidia
2885b42c62 Update fastdirty hash during save
Fixes #1064
2018-03-08 15:07:14 -05:00
sum01
b12eca0a98 Fix #1066 php syntax 2018-03-08 11:28:38 -05:00
Zachary Yedidia
3e612d2597 Merge pull request #1045 from emilyaviva/master
Organize colorscheme setting documentation
2018-03-02 20:12:46 -05:00
Zachary Yedidia
ade5efef5d Merge pull request #1050 from mathieu-aubin/master
raster compression
2018-03-02 20:11:50 -05:00
Zachary Yedidia
cb45481526 Make tab views array public
Ref #1024
2018-03-02 19:50:33 -05:00
Zachary Yedidia
88d8b0b181 Count replacements in replaceall correctly
Fixes #1055
2018-03-02 19:32:23 -05:00
JT Olds
ea6a87d41a allow optionally brace matching with the closing brace to the left of the cursor
this behavior, while slightly less obvious, allows for observing what brace you
just closed. as you write closing braces, the brace you closed gets highlighted
2018-02-27 18:53:04 -07:00
Mathieu
1c2fd30cab raster compression 2018-02-23 19:30:28 +01:00
Emily Aviva Kapor-Mater
69ed07cc62 Move setting instructions to top; add some minor organization 2018-02-20 12:11:31 -08:00
Zachary Yedidia
6d2cbb6cce Use regexp replaceall
Fixes #1038
2018-02-19 17:04:09 -05:00
Zachary Yedidia
397c29443a Fix SaveAs Lua callback
Fixes #1029
2018-02-12 00:06:31 -05:00
Zachary Yedidia
5b26702d5e Merge pull request #1028 from filalex77/patch-1
Fix relative URL for terminfo
2018-02-09 11:07:14 -05:00
Oleksii Filonenko
b9e77eee6a Fix relative URL for terminfo 2018-02-09 17:36:12 +02:00
Zachary Yedidia
8e5fd674cc Merge 2018-02-08 14:18:04 -05:00
Zachary Yedidia
5038167650 Update clipboard 2018-02-08 14:17:58 -05:00
Zachary Yedidia
6787db9eb3 Merge pull request #1026 from mbesancon/patch-2
Update julia.yaml
2018-02-07 19:43:28 -05:00
mbesancon
75b9c8c1ec Update julia.yaml
added "import" keyword
2018-02-07 17:43:43 -05:00
Zachary Yedidia
a37c30b889 Fix resize when prompt is active
Fixes #1020
2018-02-04 22:58:20 -05:00
Zachary Yedidia
f17b42bcd2 Update licenses 2018-02-04 14:04:42 -05:00
Zachary Yedidia
7bfc90d080 Update license info 2018-02-04 11:33:03 -05:00
Zachary Yedidia
1d24609ed1 Add goconvey dependency to vendor
Ref #1
2018-02-03 22:33:32 -05:00
Zachary Yedidia
aa81cf5cf6 Support nano syntax for open at line
Ref #887
2018-02-02 16:53:08 -05:00
Zachary Yedidia
4790c39dfc Open at line syntax with filename:line:col
Ref #1010
Ref #887
Ref #836
2018-02-02 13:57:30 -05:00
Zachary Yedidia
35a9245c5d Use current view for every action
Fixes #1015
2018-02-02 12:33:13 -05:00
Zachary Yedidia
3e3cdfc5b5 Fix minor issue with autoscroll
Fixes #1012
2018-02-01 20:20:57 -05:00
Zachary Yedidia
f0e453b4f9 Improve ocaml syntax highlighting 2018-01-30 22:34:44 -05:00
Zachary Yedidia
3325b98063 Exit with error on screen initialization 2018-01-30 13:04:26 -05:00
Zachary Yedidia
4632c3594f Fix bad import path 2018-01-29 23:42:45 -05:00
Zachary Yedidia
96c7b1d07b Update to use new mkinfo from tcell
This update incorporates the new terminfo updates in tcell into micro
essentially merging zyedidia/mkinfo into micro. The zyedidia/mkinfo
program should no longer be necessary and micro should automatically
generate a tcell database on its own if it cannot find a terminal
entry. The tcell database will be located in `configDir/.tcelldb`.

Ref #20
Ref #922
2018-01-29 23:36:39 -05:00
Zachary Yedidia
f48116801b Improve man page 2018-01-29 20:36:18 -05:00
Zachary Yedidia
aaf098bb47 Update tex syntax file 2018-01-29 18:02:43 -05:00
Zachary Yedidia
6d4134a178 Optimization to lots of redraws on large files 2018-01-29 16:47:55 -05:00
Zachary Yedidia
015fcf5fec Minor optimizations 2018-01-29 16:02:15 -05:00
Zachary Yedidia
fddf1690e3 Large syntax highlighting memory optimization
Ref #634
2018-01-29 15:21:00 -05:00
Zachary Yedidia
0913a1aeb3 Fix syntax highlighting on empty buffer 2018-01-28 22:35:43 -05:00
Zachary Yedidia
a19a6d28a7 Small simplification 2018-01-28 15:15:23 -05:00
202 changed files with 19472 additions and 17507 deletions

8
.gitignore vendored
View File

@@ -7,3 +7,11 @@ tmp.sh
test/
.idea/
packages/
todo.txt
test.txt
log.txt
*.old
tools/build-version
tools/build-date
tools/info-plist
tools/bindata

66
.gitmodules vendored
View File

@@ -1,63 +1,3 @@
[submodule "cmd/micro/vendor/github.com/blang/semver"]
path = cmd/micro/vendor/github.com/blang/semver
url = https://github.com/blang/semver
[submodule "cmd/micro/vendor/github.com/dustin/go-humanize"]
path = cmd/micro/vendor/github.com/dustin/go-humanize
url = https://github.com/dustin/go-humanize
[submodule "cmd/micro/vendor/github.com/go-errors/errors"]
path = cmd/micro/vendor/github.com/go-errors/errors
url = https://github.com/go-errors/errors
[submodule "cmd/micro/vendor/github.com/mattn/go-isatty"]
path = cmd/micro/vendor/github.com/mattn/go-isatty
url = https://github.com/mattn/go-isatty
[submodule "cmd/micro/vendor/github.com/mattn/go-runewidth"]
path = cmd/micro/vendor/github.com/mattn/go-runewidth
url = https://github.com/mattn/go-runewidth
[submodule "cmd/micro/vendor/github.com/mitchellh/go-homedir"]
path = cmd/micro/vendor/github.com/mitchellh/go-homedir
url = https://github.com/mitchellh/go-homedir
[submodule "cmd/micro/vendor/github.com/sergi/go-diff"]
path = cmd/micro/vendor/github.com/sergi/go-diff
url = https://github.com/sergi/go-diff
[submodule "cmd/micro/vendor/github.com/yuin/gopher-lua"]
path = cmd/micro/vendor/github.com/yuin/gopher-lua
url = https://github.com/yuin/gopher-lua
[submodule "cmd/micro/vendor/golang.org/x/net"]
path = cmd/micro/vendor/golang.org/x/net
url = https://go.googlesource.com/net
[submodule "cmd/micro/vendor/github.com/zyedidia/clipboard"]
path = cmd/micro/vendor/github.com/zyedidia/clipboard
url = https://github.com/zyedidia/clipboard
[submodule "cmd/micro/vendor/github.com/zyedidia/glob"]
path = cmd/micro/vendor/github.com/zyedidia/glob
url = https://github.com/zyedidia/glob
[submodule "cmd/micro/vendor/github.com/zyedidia/tcell"]
path = cmd/micro/vendor/github.com/zyedidia/tcell
url = https://github.com/zyedidia/tcell
[submodule "cmd/micro/vendor/github.com/gdamore/encoding"]
path = cmd/micro/vendor/github.com/gdamore/encoding
url = https://github.com/gdamore/encoding
[submodule "cmd/micro/vendor/golang.org/x/text"]
path = cmd/micro/vendor/golang.org/x/text
url = https://go.googlesource.com/text
[submodule "cmd/micro/vendor/github.com/lucasb-eyer/go-colorful"]
path = cmd/micro/vendor/github.com/lucasb-eyer/go-colorful
url = https://github.com/lucasb-eyer/go-colorful
[submodule "cmd/micro/vendor/layeh.com/gopher-luar"]
path = cmd/micro/vendor/layeh.com/gopher-luar
url = https://github.com/layeh/gopher-luar
[submodule "cmd/micro/vendor/gopkg.in/yaml.v2"]
path = cmd/micro/vendor/gopkg.in/yaml.v2
url = https://gopkg.in/yaml.v2
[submodule "cmd/micro/vendor/github.com/zyedidia/poller"]
path = cmd/micro/vendor/github.com/zyedidia/poller
url = https://github.com/zyedidia/poller
[submodule "cmd/micro/vendor/github.com/flynn/json5"]
path = cmd/micro/vendor/github.com/flynn/json5
url = https://github.com/flynn/json5
[submodule "cmd/micro/vendor/github.com/zyedidia/terminal"]
path = cmd/micro/vendor/github.com/zyedidia/terminal
url = https://github.com/zyedidia/terminal
[submodule "cmd/micro/vendor/github.com/zyedidia/pty"]
path = cmd/micro/vendor/github.com/zyedidia/pty
url = https://github.com/zyedidia/pty
[submodule "tools/go-bindata"]
path = tools/go-bindata
url = https://github.com/zyedidia/go-bindata

View File

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

View File

@@ -58,7 +58,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
github.com/gdamore/encoding/LICENSE
================
@@ -395,7 +394,9 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
github.com/zyedidia/clipboard/LICENSE
github.com/atotto/clipboard/LICENSE
================
github.com/zyedidia/clipboard/LICENSE (fork)
================
Copyright (c) 2013 Ato Araki. All rights reserved.
@@ -427,7 +428,9 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
github.com/zyedidia/tcell/LICENSE
github.com/gdamore/tcell/LICENSE
================
github.com/zyedidia/tcell/LICENSE (fork)
================
@@ -665,39 +668,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
golang.org/x/sys/LICENSE
================
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
golang.org/x/text/LICENSE
================
@@ -1167,6 +1137,8 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
github.com/james4k/terminal/LICENSE
================
github.com/zyedidia/terminal/LICENSE (fork)
================
Copyright (C) 2013 James Gray
@@ -1190,6 +1162,8 @@ SOFTWARE.
github.com/kr/pty/License
================
github.com/zyedidia/pty/License (fork)
================
Copyright (c) 2011 Keith Rarick
@@ -1214,3 +1188,199 @@ OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
github.com/smartystreets/goconvey/LICENSE.md
================
Copyright (c) 2016 SmartyStreets, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
NOTE: Various optional and subordinate components carry their own licensing
requirements and restrictions. Use of those components is subject to the terms
and conditions outlined the respective license of each component.
github.com/smartystreets/assertions/LICENSE.md
================
Copyright (c) 2016 SmartyStreets, LLC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
NOTE: Various optional and subordinate components carry their own licensing
requirements and restrictions. Use of those components is subject to the terms
and conditions outlined the respective license of each component.
github.com/jtolds/gls/LICENSE
================
Copyright (c) 2013, Space Monkey, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
github.com/npat-efault/poller/LICENSE.txt
================
github.com/zyedidia/poller/LICENSE.txt (fork)
================
Copyright (c) 2014, Nick Patavalis (npat@efault.net)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
github.com/zyedidia/glob
================
Glob is licensed under the MIT "Expat" License:
Copyright (c) 2016: Zachary Yedidia.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
github.com/dustin/go-humanize/LICENSE
================
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
<http://www.opensource.org/licenses/mit-license.php>
gopkg.in/yaml.v2/LICENSE
================
Copyright 2011-2016 Canonical Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
github.com/lucasb-eyer/go-colorful/LICENSE
================
Copyright (c) 2013 Lucas Beyer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -9,41 +9,43 @@ ADDITIONAL_GO_LINKER_FLAGS := $(shell GOOS=$(shell go env GOHOSTOS) \
GOARCH=$(shell go env GOHOSTARCH) \
go run tools/info-plist.go "$(VERSION)")
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)' -X github.com/zyedidia/micro/internal/util.Debug=OFF
# Builds micro after checking dependencies but without updating the runtime
build: update
go build -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
build:
go build -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
build-dbg:
go build -ldflags "-s -w $(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 "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
go build -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
# Same as 'build' but installs to $GOBIN afterward
install: update
go install -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
install:
go install -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
# Same as 'build-all' but installs to $GOBIN afterward
install-all: runtime install
# Same as 'build-quick' but installs to $GOBIN afterward
install-quick:
go install -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)' $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
update:
git pull
git submodule update --init
go install -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
# Builds the runtime
runtime:
go get -u github.com/jteeuwen/go-bindata/...
$(GOBIN)/go-bindata -nometadata -o runtime.go runtime/...
mv runtime.go cmd/micro
git submodule update --init
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
test:
go test ./cmd/micro
go test ./internal/...
clean:
rm -f micro

View File

@@ -17,7 +17,7 @@ Here is a picture of micro editing its source code.
![Screenshot](./assets/micro-solarized.png)
To see more screenshots of micro, showcasing all of the default colorschemes, see [here](http://zbyedidia.webfactional.com/micro/screenshots.html).
You can also check out the website for Micro at https://micro-editor.github.io.
# Table of Contents

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -7,16 +7,13 @@
.\" See \usr\share\doc\micro\LICENSE for more information.
.TH micro 1 "2017-03-28"
.SH NAME
micro \- An intuitive and modern terminal text editor
.
micro \- A modern and intuitive terminal-based text editor
.SH SYNOPSIS
.B micro
.RB []
[
.I "filename \&..."
]
.RB [OPTIONS]
[FILE]\&...
.SH DESCRIPTION
( Copied from the README file. )
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.
@@ -25,19 +22,39 @@ As the name indicates, micro aims to be somewhat of a successor to the nano edit
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
.B \-v --version
Displays the current version of micro and the git commit hash.
.TP
.SH ENVIRONMENT
Micro's behaviour can be changed by setting environment variables, of which
there is currently only one:
.I MICRO_TRUE_COLOR
.PP
\-config-dir dir
.RS 4
Specify a custom location for the configuration directory
.RE
.PP
\-startpos 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
\-version
.RS 4
Show the version number and information
.RE
When MICRO_TRUE_COLOR is set to 1, micro will attempt to treat your terminal as
a true-color terminal and will be able to make full use of the true-color colorschemes
that are included with micro. If MICRO_TRUE_COLOR is not set or is set to 0, then
micro will only make use of 256 color features and will internally map true-color
colorschemes to the nearest colors available. For more information see micro's documentation.
.SH CONFIGURATION
Micro uses
\fI$XDG_CONFIG_HOME/micro\fR
for configuration by default. If it is not set, micro uses ~/.config/micro.
Two main configuration files are settings.json, containing the user's
settings, and bindings.json, containing the user's custom keybindings.
.SH ENVIRONMENT
Micro's behaviour can be changed by setting environment variables, of which there is currently only one:
\fIMICRO_TRUECOLOR\fR.
When MICRO_TRUECOLOR is set to 1, micro will attempt to treat your terminal as a true-color terminal and will be able to make full use of the true-color colorschemes that are included with micro. If MICRO_TRUECOLOR is not set or is set to 0, then micro will only make use of 256 color features and will internally map true-color colorschemes to the nearest colors available. For more information see micro's documentation.
.SH NOTICE
This manpage is intended only to serve as a quick guide to the invocation of

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +0,0 @@
// +build plan9 nacl windows
package main
func (v *View) Suspend(usePlugin bool) bool {
messenger.Error("Suspend is only supported on Posix")
return false
}

View File

@@ -1,249 +0,0 @@
package main
import (
"io/ioutil"
"os"
"strings"
)
var pluginCompletions []func(string) []string
// 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) {
var sep string = 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 = ReplaceHome(directories)
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 += sep
}
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], sep) + sep + suggestions[0]
} else {
chosen = suggestions[0]
}
} else {
if len(dirs) > 1 {
chosen = strings.Join(dirs[:len(dirs)-1], sep) + sep
}
}
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 _, file := range ListRuntimeFiles(RTHelp) {
topic := file.Name()
if strings.HasPrefix(topic, input) {
suggestions = append(suggestions, topic)
}
}
var chosen string
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
// ColorschemeComplete tab-completes names of colorschemes.
func ColorschemeComplete(input string) (string, []string) {
var suggestions []string
files := ListRuntimeFiles(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(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
}
// OptionValueComplete completes values for various options
func OptionValueComplete(inputOpt, input string) (string, []string) {
inputOpt = strings.TrimSpace(inputOpt)
var suggestions []string
localSettings := DefaultLocalSettings()
var optionVal interface{}
for k, option := range 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")
}
}
}
var chosen string
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, 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))
}
// PluginComplete autocompletes from plugin function
func PluginComplete(complete Completion, input string) (chosen string, suggestions []string) {
idx := int(-complete) - 1
if len(pluginCompletions) <= idx {
return "", nil
}
suggestions = pluginCompletions[idx](input)
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return
}
// PluginCmdComplete completes with possible choices for the `> plugin` command
func PluginCmdComplete(input string) (chosen string, suggestions []string) {
for _, cmd := range []string{"install", "remove", "search", "update", "list"} {
if strings.HasPrefix(cmd, input) {
suggestions = append(suggestions, cmd)
}
}
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
// PluginnameComplete completes with the names of loaded plugins
func PluginNameComplete(input string) (chosen string, suggestions []string) {
for _, pp := range GetAllPluginPackages() {
if strings.HasPrefix(pp.Name, input) {
suggestions = append(suggestions, pp.Name)
}
}
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}

View File

@@ -1,607 +0,0 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"strings"
"unicode"
"github.com/flynn/json5"
"github.com/zyedidia/tcell"
)
var bindingsStr map[string]string
var bindings map[Key][]func(*View, bool) bool
var mouseBindings map[Key][]func(*View, bool, *tcell.EventMouse) bool
var helpBinding string
var kmenuBinding string
var mouseBindingActions = map[string]func(*View, bool, *tcell.EventMouse) bool{
"MousePress": (*View).MousePress,
"MouseMultiCursor": (*View).MouseMultiCursor,
}
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,
"SelectLine": (*View).SelectLine,
"SelectToStartOfLine": (*View).SelectToStartOfLine,
"SelectToEndOfLine": (*View).SelectToEndOfLine,
"ParagraphPrevious": (*View).ParagraphPrevious,
"ParagraphNext": (*View).ParagraphNext,
"InsertNewline": (*View).InsertNewline,
"InsertSpace": (*View).InsertSpace,
"Backspace": (*View).Backspace,
"Delete": (*View).Delete,
"InsertTab": (*View).InsertTab,
"Save": (*View).Save,
"SaveAll": (*View).SaveAll,
"SaveAs": (*View).SaveAs,
"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,
"MoveLinesUp": (*View).MoveLinesUp,
"MoveLinesDown": (*View).MoveLinesDown,
"IndentSelection": (*View).IndentSelection,
"OutdentSelection": (*View).OutdentSelection,
"OutdentLine": (*View).OutdentLine,
"Paste": (*View).Paste,
"PastePrimary": (*View).PastePrimary,
"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,
"ToggleKeyMenu": (*View).ToggleKeyMenu,
"ToggleRuler": (*View).ToggleRuler,
"JumpLine": (*View).JumpLine,
"ClearStatus": (*View).ClearStatus,
"ShellMode": (*View).ShellMode,
"CommandMode": (*View).CommandMode,
"ToggleOverwriteMode": (*View).ToggleOverwriteMode,
"Escape": (*View).Escape,
"Quit": (*View).Quit,
"QuitAll": (*View).QuitAll,
"AddTab": (*View).AddTab,
"PreviousTab": (*View).PreviousTab,
"NextTab": (*View).NextTab,
"NextSplit": (*View).NextSplit,
"PreviousSplit": (*View).PreviousSplit,
"Unsplit": (*View).Unsplit,
"VSplit": (*View).VSplitBinding,
"HSplit": (*View).HSplitBinding,
"ToggleMacro": (*View).ToggleMacro,
"PlayMacro": (*View).PlayMacro,
"Suspend": (*View).Suspend,
"ScrollUp": (*View).ScrollUpAction,
"ScrollDown": (*View).ScrollDownAction,
"SpawnMultiCursor": (*View).SpawnMultiCursor,
"RemoveMultiCursor": (*View).RemoveMultiCursor,
"RemoveAllMultiCursors": (*View).RemoveAllMultiCursors,
"SkipMultiCursor": (*View).SkipMultiCursor,
"JumpToMatchingBrace": (*View).JumpToMatchingBrace,
// This was changed to InsertNewline but I don't want to break backwards compatibility
"InsertEnter": (*View).InsertNewline,
}
var bindingMouse = 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 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,
"CtrlPageUp": tcell.KeyCtrlPgUp,
"CtrlPageDown": tcell.KeyCtrlPgDn,
"Tab": tcell.KeyTab,
"Esc": tcell.KeyEsc,
"Escape": tcell.KeyEscape,
"Enter": tcell.KeyEnter,
"Backspace": 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
buttons tcell.ButtonMask
r rune
escape string
}
// InitBindings initializes the keybindings for micro
func InitBindings() {
bindings = make(map[Key][]func(*View, bool) bool)
bindingsStr = make(map[string]string)
mouseBindings = make(map[Key][]func(*View, bool, *tcell.EventMouse) 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 = json5.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 != "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"):
return Key{
keyCode: -1,
modifiers: modifiers,
buttons: -1,
r: 0,
escape: k,
}, true
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.
k = string(unicode.ToUpper(rune(k[0]))) + k[1:]
if code, ok := bindingKeys["Ctrl"+k]; ok {
// It is, we're done.
return Key{
keyCode: code,
modifiers: modifiers,
buttons: -1,
r: 0,
}, true
}
}
// See if we can find the key in bindingKeys
if code, ok := bindingKeys[k]; ok {
return Key{
keyCode: code,
modifiers: modifiers,
buttons: -1,
r: 0,
}, true
}
// See if we can find the key in bindingMouse
if code, ok := bindingMouse[k]; ok {
return Key{
modifiers: modifiers,
buttons: code,
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,
buttons: -1,
r: rune(k[0]),
}, true
}
// We don't know what happened.
return Key{buttons: -1}, 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
}
func findMouseAction(v string) func(*View, bool, *tcell.EventMouse) bool {
action, ok := mouseBindingActions[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 = LuaFunctionMouseBinding(v)
}
return action
}
// TryBindKey tries to bind a key by writing to configDir/bindings.json
// This function is unused for now
func TryBindKey(k, v string) {
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
}
conflict := -1
lines := strings.Split(string(input), "\n")
for i, l := range lines {
parts := strings.Split(l, ":")
if len(parts) >= 2 {
if strings.Contains(parts[0], k) {
conflict = i
TermMessage("Warning: Keybinding conflict:", k, " has been overwritten")
}
}
}
binding := fmt.Sprintf(" \"%s\": \"%s\",", k, v)
if conflict == -1 {
lines = append([]string{lines[0], binding}, lines[conflict:]...)
} else {
lines = append(append(lines[:conflict], binding), lines[conflict+1:]...)
}
txt := strings.Join(lines, "\n")
err = ioutil.WriteFile(filename, []byte(txt), 0644)
if err != nil {
TermMessage("Error")
}
}
}
// BindKey takes a key and an action and binds the two together
func BindKey(k, v string) {
key, ok := findKey(k)
if !ok {
TermMessage("Unknown keybinding: " + k)
return
}
if v == "ToggleHelp" {
helpBinding = k
}
if v == "ToggleKeyMenu" {
kmenuBinding = k
}
if helpBinding == k && v != "ToggleHelp" {
helpBinding = ""
}
if kmenuBinding == k && v != "ToggleKeyMenu" {
kmenuBinding = ""
}
actionNames := strings.Split(v, ",")
if actionNames[0] == "UnbindKey" {
delete(bindings, key)
delete(mouseBindings, key)
delete(bindingsStr, k)
if len(actionNames) == 1 {
return
}
actionNames = append(actionNames[:0], actionNames[1:]...)
}
actions := make([]func(*View, bool) bool, 0, len(actionNames))
mouseActions := make([]func(*View, bool, *tcell.EventMouse) bool, 0, len(actionNames))
for _, actionName := range actionNames {
if strings.HasPrefix(actionName, "Mouse") {
mouseActions = append(mouseActions, findMouseAction(actionName))
} else if strings.HasPrefix(actionName, "command:") {
cmd := strings.SplitN(actionName, ":", 2)[1]
actions = append(actions, CommandAction(cmd))
} else if strings.HasPrefix(actionName, "command-edit:") {
cmd := strings.SplitN(actionName, ":", 2)[1]
actions = append(actions, CommandEditAction(cmd))
} else {
actions = append(actions, findAction(actionName))
}
}
if len(actions) > 0 {
// Can't have a binding be both mouse and normal
delete(mouseBindings, key)
bindings[key] = actions
bindingsStr[k] = v
} else if len(mouseActions) > 0 {
// Can't have a binding be both mouse and normal
delete(bindings, key)
mouseBindings[key] = mouseActions
}
}
// 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": "StartOfLine",
"CtrlRight": "EndOfLine",
"CtrlShiftLeft": "SelectToStartOfLine",
"ShiftHome": "SelectToStartOfLine",
"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": "IndentSelection,InsertTab",
"Backtab": "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": "StartOfLine",
"End": "EndOfLine",
"CtrlHome": "CursorStart",
"CtrlEnd": "CursorEnd",
"PageUp": "CursorPageUp",
"PageDown": "CursorPageDown",
"CtrlPageUp": "PreviousTab",
"CtrlPageDown": "NextTab",
"CtrlG": "ToggleHelp",
"Alt-g": "ToggleKeyMenu",
"CtrlR": "ToggleRuler",
"CtrlL": "JumpLine",
"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": "StartOfLine",
"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-p": "RemoveMultiCursor",
"Alt-c": "RemoveAllMultiCursors",
"Alt-x": "SkipMultiCursor",
}
}

View File

@@ -1,709 +0,0 @@
package main
import (
"bytes"
"crypto/md5"
"encoding/gob"
"errors"
"io"
"io/ioutil"
"os"
"os/exec"
"os/signal"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/zyedidia/micro/cmd/micro/highlight"
)
var (
// 0 - no line type detected
// 1 - lf detected
// 2 - crlf detected
fileformat = 0
)
// 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
cursors []*Cursor // for multiple cursors
curCursor int // the current cursor
// Path to the file on disk
Path string
// Absolute path to the file on disk
AbsPath 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 is the number of lines in the buffer
NumLines int
syntaxDef *highlight.Def
highlighter *highlight.Highlighter
// Hash of the original buffer -- empty if fastdirty is on
origHash [16]byte
// 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
}
// NewBufferFromString creates a new buffer containing the given
// string
func NewBufferFromString(text, path string) *Buffer {
return NewBuffer(strings.NewReader(text), int64(len(text)), path)
}
// NewBuffer creates a new buffer from a given reader with a given path
func NewBuffer(reader io.Reader, size int64, path string) *Buffer {
if path != "" {
for _, tab := range tabs {
for _, view := range tab.views {
if view.Buf.Path == path {
return view.Buf
}
}
}
}
b := new(Buffer)
b.LineArray = NewLineArray(size, reader)
b.Settings = DefaultLocalSettings()
for k, v := range globalSettings {
if _, ok := b.Settings[k]; ok {
b.Settings[k] = v
}
}
if fileformat == 1 {
b.Settings["fileformat"] = "unix"
} else if fileformat == 2 {
b.Settings["fileformat"] = "dos"
}
absPath, _ := filepath.Abs(path)
b.Path = path
b.AbsPath = absPath
// The last time this file was modified
b.ModTime, _ = GetModTime(b.Path)
b.EventHandler = NewEventHandler(b)
b.Update()
b.UpdateRules()
if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
os.Mkdir(configDir+"/buffers/", os.ModePerm)
}
// Put the cursor at the first spot
cursorStartX := 0
cursorStartY := 0
// If -startpos LINE,COL was passed, use start position LINE,COL
if len(*flagStartPos) > 0 {
positions := strings.Split(*flagStartPos, ",")
if len(positions) == 2 {
lineNum, errPos1 := strconv.Atoi(positions[0])
colNum, errPos2 := strconv.Atoi(positions[1])
if errPos1 == nil && errPos2 == nil {
cursorStartX = colNum
cursorStartY = lineNum - 1
// Check to avoid line overflow
if cursorStartY > b.NumLines {
cursorStartY = b.NumLines - 1
} else if cursorStartY < 0 {
cursorStartY = 0
}
// Check to avoid column overflow
if cursorStartX > len(b.Line(cursorStartY)) {
cursorStartX = len(b.Line(cursorStartY))
} else if cursorStartX < 0 {
cursorStartX = 0
}
}
}
}
b.Cursor = Cursor{
Loc: Loc{
X: cursorStartX,
Y: cursorStartY,
},
buf: b,
}
InitLocalSettings(b)
if len(*flagStartPos) == 0 && (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
file, err := os.Open(configDir + "/buffers/" + EscapePath(b.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 modified by someone else in the meantime
if b.ModTime == buffer.ModTime {
b.EventHandler = buffer.EventHandler
b.EventHandler.buf = b
}
}
}
file.Close()
}
if !b.Settings["fastdirty"].(bool) {
if size > 50000 {
// If the file is larger than a megabyte fastdirty needs to be on
b.Settings["fastdirty"] = true
} else {
b.origHash = md5.Sum([]byte(b.String()))
}
}
b.cursors = []*Cursor{&b.Cursor}
return b
}
// GetName returns the name that should be displayed in the statusline
// for this buffer
func (b *Buffer) GetName() string {
if b.name == "" {
if b.Path == "" {
return "No name"
}
return b.Path
}
return b.name
}
// UpdateRules updates the syntax rules and filetype for this buffer
// This is called when the colorscheme changes
func (b *Buffer) UpdateRules() {
rehighlight := false
var files []*highlight.File
for _, f := range ListRuntimeFiles(RTSyntax) {
data, err := f.Data()
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
} else {
file, err := highlight.ParseFile(data)
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
ftdetect, err := highlight.ParseFtDetect(file)
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
ft := b.Settings["filetype"].(string)
if (ft == "Unknown" || ft == "") && !rehighlight {
if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
header := new(highlight.Header)
header.FileType = file.FileType
header.FtDetect = ftdetect
b.syntaxDef, err = highlight.ParseDef(file, header)
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
rehighlight = true
}
} else {
if file.FileType == ft && !rehighlight {
header := new(highlight.Header)
header.FileType = file.FileType
header.FtDetect = ftdetect
b.syntaxDef, err = highlight.ParseDef(file, header)
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
rehighlight = true
}
}
files = append(files, file)
}
}
if b.syntaxDef != nil {
highlight.ResolveIncludes(b.syntaxDef, files)
}
if b.highlighter == nil || rehighlight {
if b.syntaxDef != nil {
b.Settings["filetype"] = b.syntaxDef.FileType
b.highlighter = highlight.NewHighlighter(b.syntaxDef)
if b.Settings["syntax"].(bool) {
b.highlighter.HighlightStates(b)
}
}
}
}
// FileType returns the buffer's filetype
func (b *Buffer) FileType() string {
return b.Settings["filetype"].(string)
}
// IndentString returns a string representing one level of indentation
func (b *Buffer) IndentString() string {
if b.Settings["tabstospaces"].(bool) {
return Spaces(int(b.Settings["tabsize"].(float64)))
}
return "\t"
}
// 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)
}
// MergeCursors merges any cursors that are at the same position
// into one cursor
func (b *Buffer) MergeCursors() {
var cursors []*Cursor
for i := 0; i < len(b.cursors); i++ {
c1 := b.cursors[i]
if c1 != nil {
for j := 0; j < len(b.cursors); j++ {
c2 := b.cursors[j]
if c2 != nil && i != j && c1.Loc == c2.Loc {
b.cursors[j] = nil
}
}
cursors = append(cursors, c1)
}
}
b.cursors = cursors
for i := range b.cursors {
b.cursors[i].Num = i
}
if b.curCursor >= len(b.cursors) {
b.curCursor = len(b.cursors) - 1
}
}
// UpdateCursors updates all the cursors indicies
func (b *Buffer) UpdateCursors() {
for i, c := range b.cursors {
c.Num = i
}
}
// 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) {
file, err := os.Create(configDir + "/buffers/" + EscapePath(b.AbsPath))
if err == nil {
enc := gob.NewEncoder(file)
gob.Register(TextEvent{})
err = enc.Encode(SerializedBuffer{
b.EventHandler,
b.Cursor,
b.ModTime,
})
}
err = 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()
if b.Settings["rmtrailingws"].(bool) {
r, _ := regexp.Compile(`[ \t]+$`)
for lineNum, line := range b.Lines(0, b.NumLines) {
indices := r.FindStringIndex(line)
if indices == nil {
continue
}
startLoc := Loc{indices[0], lineNum}
b.deleteToEnd(startLoc)
}
b.Cursor.Relocate()
}
if b.Settings["eofnewline"].(bool) {
end := b.End()
if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
b.Insert(end, "\n")
}
}
defer func() {
b.ModTime, _ = GetModTime(filename)
}()
// Removes any tilde and replaces with the absolute path to home
var absFilename string = 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 yes, canceled := messenger.YesNoPrompt("Parent folders \"" + dirname + "\" do not exist. Create them? (y,n)"); yes && !canceled {
// 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 {
// If they canceled the creation of leading dirs
return errors.New("Save aborted")
}
}
}
f, err := os.OpenFile(absFilename, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return err
}
if err := f.Truncate(0); err != nil {
return err
}
useCrlf := b.Settings["fileformat"] == "dos"
for i, l := range b.lines {
if _, err := f.Write(l.data); err != nil {
return err
}
if i != len(b.lines)-1 {
if useCrlf {
if _, err := f.Write([]byte{'\r', '\n'}); err != nil {
return err
}
} else {
if _, err := f.Write([]byte{'\n'}); err != nil {
return err
}
}
}
}
b.Path = filename
b.IsModified = false
return b.Serialize()
}
// 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.Path = filename
// 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(globalSettings["sucmd"].(string), "tee", filename)
cmd.Stdin = bytes.NewBufferString(b.SaveString(b.Settings["fileformat"] == "dos"))
// 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()
// Start the screen back up
InitScreen()
if err == nil {
b.IsModified = false
b.ModTime, _ = GetModTime(filename)
b.Serialize()
}
return err
}
// Modified returns if this buffer has been modified since
// being opened
func (b *Buffer) Modified() bool {
if b.Settings["fastdirty"].(bool) {
return b.IsModified
}
return b.origHash != md5.Sum([]byte(b.String()))
}
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
}
func (b *Buffer) deleteToEnd(start Loc) {
b.IsModified = true
b.LineArray.DeleteToEnd(start)
b.Update()
}
// 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].data), b.NumLines - 1}
}
// RuneAt returns the rune at a given location in the buffer
func (b *Buffer) RuneAt(loc Loc) rune {
line := []rune(b.Line(loc.Y))
if len(line) > 0 {
return line[loc.X]
}
return '\n'
}
// Line returns a single line
func (b *Buffer) Line(n int) string {
if n >= len(b.lines) {
return ""
}
return string(b.lines[n].data)
}
// LinesNum returns the number of lines in the buffer
func (b *Buffer) LinesNum() int {
return len(b.lines)
}
// 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.data))
}
return slice
}
// Len gives the length of the buffer
func (b *Buffer) Len() int {
return Count(b.String())
}
// MoveLinesUp moves the range of lines up one row
func (b *Buffer) MoveLinesUp(start int, end int) {
// 0 < start < end <= len(b.lines)
if start < 1 || start >= end || end > len(b.lines) {
return // what to do? FIXME
}
if end == len(b.lines) {
b.Insert(
Loc{
utf8.RuneCount(b.lines[end-1].data),
end - 1,
},
"\n"+b.Line(start-1),
)
} else {
b.Insert(
Loc{0, end},
b.Line(start-1)+"\n",
)
}
b.Remove(
Loc{0, start - 1},
Loc{0, start},
)
}
// MoveLinesDown moves the range of lines down one row
func (b *Buffer) MoveLinesDown(start int, end int) {
// 0 <= start < end < len(b.lines)
// if end == len(b.lines), we can't do anything here because the
// last line is unaccessible, FIXME
if start < 0 || start >= end || end >= len(b.lines)-1 {
return // what to do? FIXME
}
b.Insert(
Loc{0, start},
b.Line(end)+"\n",
)
end++
b.Remove(
Loc{0, end},
Loc{0, end + 1},
)
}
// ClearMatches clears all of the syntax highlighting for this buffer
func (b *Buffer) ClearMatches() {
for i := range b.lines {
b.SetMatch(i, nil)
b.SetState(i, nil)
}
}
func (b *Buffer) clearCursors() {
for i := 1; i < len(b.cursors); i++ {
b.cursors[i] = nil
}
b.cursors = b.cursors[:1]
b.UpdateCursors()
b.Cursor.ResetSelection()
}
var bracePairs = [][2]rune{
[2]rune{'(', ')'},
[2]rune{'{', '}'},
[2]rune{'[', ']'},
}
// FindMatchingBrace returns the location in the buffer of the matching bracket
// It is given a brace type containing the open and closing character, (for example
// '{' and '}') as well as the location to match from
func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) Loc {
curLine := []rune(string(b.lines[start.Y].data))
startChar := curLine[start.X]
var i int
if startChar == braceType[0] {
for y := start.Y; y < b.NumLines; y++ {
l := []rune(string(b.lines[y].data))
xInit := 0
if y == start.Y {
xInit = start.X
}
for x := xInit; x < len(l); x++ {
r := l[x]
if r == braceType[0] {
i++
} else if r == braceType[1] {
i--
if i == 0 {
return Loc{x, y}
}
}
}
}
} else if startChar == braceType[1] {
for y := start.Y; y >= 0; y-- {
l := []rune(string(b.lines[y].data))
xInit := len(l) - 1
if y == start.Y {
xInit = start.X
}
for x := xInit; x >= 0; x-- {
r := l[x]
if r == braceType[0] {
i--
if i == 0 {
return Loc{x, y}
}
} else if r == braceType[1] {
i++
}
}
}
}
return start
}

View File

@@ -1,229 +0,0 @@
package main
import (
"github.com/mattn/go-runewidth"
"github.com/zyedidia/tcell"
)
func min(a, b int) int {
if a <= b {
return a
}
return b
}
func visualToCharPos(visualIndex int, lineN int, str string, buf *Buffer, tabsize int) (int, int, *tcell.Style) {
charPos := 0
var lineIdx int
var lastWidth int
var style *tcell.Style
var width int
var rw int
for i, c := range str {
// width := StringWidth(str[:i], tabsize)
if group, ok := buf.Match(lineN)[charPos]; ok {
s := GetColor(group.String())
style = &s
}
if width >= visualIndex {
return charPos, visualIndex - lastWidth, style
}
if i != 0 {
charPos++
lineIdx += rw
}
lastWidth = width
rw = 0
if c == '\t' {
rw = tabsize - (lineIdx % tabsize)
width += rw
} else {
rw = runewidth.RuneWidth(c)
width += rw
}
}
return -1, -1, style
}
type Char struct {
visualLoc Loc
realLoc Loc
char rune
// The actual character that is drawn
// This is only different from char if it's for example hidden character
drawChar rune
style tcell.Style
width int
}
type CellView struct {
lines [][]*Char
}
func (c *CellView) Draw(buf *Buffer, top, height, left, width int) {
if width <= 0 {
return
}
matchingBrace := Loc{-1, -1}
// bracePairs is defined in buffer.go
if buf.Settings["matchbrace"].(bool) {
for _, bp := range bracePairs {
r := buf.Cursor.RuneUnder(buf.Cursor.X)
if r == bp[0] || r == bp[1] {
matchingBrace = buf.FindMatchingBrace(bp, buf.Cursor.Loc)
}
}
}
tabsize := int(buf.Settings["tabsize"].(float64))
softwrap := buf.Settings["softwrap"].(bool)
indentrunes := []rune(buf.Settings["indentchar"].(string))
// if empty indentchar settings, use space
if indentrunes == nil || len(indentrunes) == 0 {
indentrunes = []rune(" ")
}
indentchar := indentrunes[0]
start := buf.Cursor.Y
if buf.Settings["syntax"].(bool) && buf.syntaxDef != nil {
if start > 0 && buf.lines[start-1].rehighlight {
buf.highlighter.ReHighlightLine(buf, start-1)
buf.lines[start-1].rehighlight = false
}
buf.highlighter.ReHighlightStates(buf, start)
buf.highlighter.HighlightMatches(buf, top, top+height)
}
c.lines = make([][]*Char, 0)
viewLine := 0
lineN := top
curStyle := defStyle
for viewLine < height {
if lineN >= len(buf.lines) {
break
}
lineStr := buf.Line(lineN)
line := []rune(lineStr)
colN, startOffset, startStyle := visualToCharPos(left, lineN, lineStr, buf, tabsize)
if colN < 0 {
colN = len(line)
}
viewCol := -startOffset
if startStyle != nil {
curStyle = *startStyle
}
// We'll either draw the length of the line, or the width of the screen
// whichever is smaller
lineLength := min(StringWidth(lineStr, tabsize), width)
c.lines = append(c.lines, make([]*Char, lineLength))
wrap := false
// We only need to wrap if the length of the line is greater than the width of the terminal screen
if softwrap && StringWidth(lineStr, tabsize) > width {
wrap = true
// We're going to draw the entire line now
lineLength = StringWidth(lineStr, tabsize)
}
for viewCol < lineLength {
if colN >= len(line) {
break
}
if group, ok := buf.Match(lineN)[colN]; ok {
curStyle = GetColor(group.String())
}
char := line[colN]
if viewCol >= 0 {
st := curStyle
if colN == matchingBrace.X && lineN == matchingBrace.Y && !buf.Cursor.HasSelection() {
st = curStyle.Reverse(true)
}
if viewCol < len(c.lines[viewLine]) {
c.lines[viewLine][viewCol] = &Char{Loc{viewCol, viewLine}, Loc{colN, lineN}, char, char, st, 1}
}
}
if char == '\t' {
charWidth := tabsize - (viewCol+left)%tabsize
if viewCol >= 0 {
c.lines[viewLine][viewCol].drawChar = indentchar
c.lines[viewLine][viewCol].width = charWidth
indentStyle := curStyle
ch := buf.Settings["indentchar"].(string)
if group, ok := colorscheme["indent-char"]; ok && !IsStrWhitespace(ch) && ch != "" {
indentStyle = group
}
c.lines[viewLine][viewCol].style = indentStyle
}
for i := 1; i < charWidth; i++ {
viewCol++
if viewCol >= 0 && viewCol < lineLength && viewCol < len(c.lines[viewLine]) {
c.lines[viewLine][viewCol] = &Char{Loc{viewCol, viewLine}, Loc{colN, lineN}, char, ' ', curStyle, 1}
}
}
viewCol++
} else if runewidth.RuneWidth(char) > 1 {
charWidth := runewidth.RuneWidth(char)
if viewCol >= 0 {
c.lines[viewLine][viewCol].width = charWidth
}
for i := 1; i < charWidth; i++ {
viewCol++
if viewCol >= 0 && viewCol < lineLength && viewCol < len(c.lines[viewLine]) {
c.lines[viewLine][viewCol] = &Char{Loc{viewCol, viewLine}, Loc{colN, lineN}, char, ' ', curStyle, 1}
}
}
viewCol++
} else {
viewCol++
}
colN++
if wrap && viewCol >= width {
viewLine++
// If we go too far soft wrapping we have to cut off
if viewLine >= height {
break
}
nextLine := line[colN:]
lineLength := min(StringWidth(string(nextLine), tabsize), width)
c.lines = append(c.lines, make([]*Char, lineLength))
viewCol = 0
}
}
if group, ok := buf.Match(lineN)[len(line)]; ok {
curStyle = GetColor(group.String())
}
// newline
viewLine++
lineN++
}
for i := top; i < top+height; i++ {
if i >= buf.NumLines {
break
}
buf.SetMatch(i, nil)
}
}

View File

@@ -1,738 +0,0 @@
package main
import (
"fmt"
"os"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
humanize "github.com/dustin/go-humanize"
"github.com/zyedidia/micro/cmd/micro/shellwords"
)
// A Command contains a action (a function to call) as well as information about how to autocomplete the command
type Command struct {
action func([]string)
completions []Completion
}
// A StrCommand is similar to a command but keeps the name of the action
type StrCommand struct {
action string
completions []Completion
}
var commands map[string]Command
var commandActions map[string]func([]string)
func init() {
commandActions = map[string]func([]string){
"Set": Set,
"SetLocal": SetLocal,
"Show": Show,
"ShowKey": ShowKey,
"Run": Run,
"Bind": Bind,
"Quit": Quit,
"Save": Save,
"Replace": Replace,
"ReplaceAll": ReplaceAll,
"VSplit": VSplit,
"HSplit": HSplit,
"Tab": NewTab,
"Help": Help,
"Eval": Eval,
"ToggleLog": ToggleLog,
"Plugin": PluginCmd,
"Reload": Reload,
"Cd": Cd,
"Pwd": Pwd,
"Open": Open,
"TabSwitch": TabSwitch,
"Term": Term,
"MemUsage": MemUsage,
"Retab": Retab,
"Raw": Raw,
}
}
// 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, OptionValueCompletion}},
"setlocal": {"SetLocal", []Completion{OptionCompletion, OptionValueCompletion}},
"show": {"Show", []Completion{OptionCompletion, NoCompletion}},
"showkey": {"ShowKey", []Completion{NoCompletion}},
"bind": {"Bind", []Completion{NoCompletion}},
"run": {"Run", []Completion{NoCompletion}},
"quit": {"Quit", []Completion{NoCompletion}},
"save": {"Save", []Completion{NoCompletion}},
"replace": {"Replace", []Completion{NoCompletion}},
"replaceall": {"ReplaceAll", []Completion{NoCompletion}},
"vsplit": {"VSplit", []Completion{FileCompletion, NoCompletion}},
"hsplit": {"HSplit", []Completion{FileCompletion, NoCompletion}},
"tab": {"Tab", []Completion{FileCompletion, NoCompletion}},
"help": {"Help", []Completion{HelpCompletion, NoCompletion}},
"eval": {"Eval", []Completion{NoCompletion}},
"log": {"ToggleLog", []Completion{NoCompletion}},
"plugin": {"Plugin", []Completion{PluginCmdCompletion, PluginNameCompletion}},
"reload": {"Reload", []Completion{NoCompletion}},
"cd": {"Cd", []Completion{FileCompletion}},
"pwd": {"Pwd", []Completion{NoCompletion}},
"open": {"Open", []Completion{FileCompletion}},
"tabswitch": {"TabSwitch", []Completion{NoCompletion}},
"term": {"Term", []Completion{NoCompletion}},
"memusage": {"MemUsage", []Completion{NoCompletion}},
"retab": {"Retab", []Completion{NoCompletion}},
"raw": {"Raw", []Completion{NoCompletion}},
}
}
// 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) func(*View, bool) bool {
return func(v *View, usePlugin bool) bool {
input, canceled := messenger.Prompt("> ", prompt, "Command", CommandCompletion)
if !canceled {
HandleCommand(input)
}
return false
}
}
// CommandAction returns a bindable function which executes the
// given command
func CommandAction(cmd string) func(*View, bool) bool {
return func(v *View, usePlugin bool) bool {
HandleCommand(cmd)
return false
}
}
// PluginCmd installs, removes, updates, lists, or searches for given plugins
func PluginCmd(args []string) {
if len(args) >= 1 {
switch args[0] {
case "install":
installedVersions := GetInstalledVersions(false)
for _, plugin := range args[1:] {
pp := GetAllPluginPackages().Get(plugin)
if pp == nil {
messenger.Error("Unknown plugin \"" + plugin + "\"")
} else if err := pp.IsInstallable(); err != nil {
messenger.Error("Error installing ", plugin, ": ", err)
} else {
for _, installed := range installedVersions {
if pp.Name == installed.pack.Name {
if pp.Versions[0].Version.Compare(installed.Version) == 1 {
messenger.Error(pp.Name, " is already installed but out-of-date: use 'plugin update ", pp.Name, "' to update")
} else {
messenger.Error(pp.Name, " is already installed")
}
}
}
pp.Install()
}
}
case "remove":
removed := ""
for _, plugin := range args[1:] {
// check if the plugin exists.
if _, ok := loadedPlugins[plugin]; ok {
UninstallPlugin(plugin)
removed += plugin + " "
continue
}
}
if !IsSpaces(removed) {
messenger.Message("Removed ", removed)
} else {
messenger.Error("The requested plugins do not exist")
}
case "update":
UpdatePlugins(args[1:])
case "list":
plugins := GetInstalledVersions(false)
messenger.AddLog("----------------")
messenger.AddLog("The following plugins are currently installed:\n")
for _, p := range plugins {
messenger.AddLog(fmt.Sprintf("%s (%s)", p.pack.Name, p.Version))
}
messenger.AddLog("----------------")
if len(plugins) > 0 {
if CurView().Type != vtLog {
ToggleLog([]string{})
}
}
case "search":
plugins := SearchPlugin(args[1:])
messenger.Message(len(plugins), " plugins found")
for _, p := range plugins {
messenger.AddLog("----------------")
messenger.AddLog(p.String())
}
messenger.AddLog("----------------")
if len(plugins) > 0 {
if CurView().Type != vtLog {
ToggleLog([]string{})
}
}
case "available":
packages := GetAllPluginPackages()
messenger.AddLog("Available Plugins:")
for _, pkg := range packages {
messenger.AddLog(pkg.Name)
}
if CurView().Type != vtLog {
ToggleLog([]string{})
}
}
} else {
messenger.Error("Not enough arguments")
}
}
// Retab changes all spaces to tabs or all tabs to spaces
// depending on the user's settings
func Retab(args []string) {
CurView().Retab(true)
}
// Raw opens a new raw view which displays the escape sequences micro
// is receiving in real-time
func Raw(args []string) {
buf := NewBufferFromString("", "Raw events")
view := NewView(buf)
view.Buf.Insert(view.Cursor.Loc, "Warning: Showing raw event escape codes\n")
view.Buf.Insert(view.Cursor.Loc, "Use CtrlQ to exit\n")
view.Type = vtRaw
tab := NewTabFromView(view)
tab.SetNum(len(tabs))
tabs = append(tabs, tab)
curTab = len(tabs) - 1
if len(tabs) == 2 {
for _, t := range tabs {
for _, v := range t.views {
v.ToggleTabbar()
}
}
}
}
// TabSwitch switches to a given tab either by name or by number
func TabSwitch(args []string) {
if len(args) > 0 {
num, err := strconv.Atoi(args[0])
if err != nil {
// Check for tab with this name
found := false
for _, t := range tabs {
v := t.views[t.CurView]
if v.Buf.GetName() == args[0] {
curTab = v.TabNum
found = true
}
}
if !found {
messenger.Error("Could not find tab: ", err)
}
} else {
num--
if num >= 0 && num < len(tabs) {
curTab = num
} else {
messenger.Error("Invalid tab index")
}
}
}
}
// Cd changes the current working directory
func Cd(args []string) {
if len(args) > 0 {
path := ReplaceHome(args[0])
err := os.Chdir(path)
if err != nil {
messenger.Error("Error with cd: ", err)
return
}
wd, _ := os.Getwd()
for _, tab := range tabs {
for _, view := range tab.views {
if len(view.Buf.name) == 0 {
continue
}
view.Buf.Path, _ = MakeRelative(view.Buf.AbsPath, wd)
if p, _ := filepath.Abs(view.Buf.Path); !strings.Contains(p, wd) {
view.Buf.Path = view.Buf.AbsPath
}
}
}
}
}
// MemUsage 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 MemUsage(args []string) {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
messenger.Message(fmt.Sprintf("Alloc: %v, Sys: %v, NumGC: %v", humanize.Bytes(mem.Alloc), humanize.Bytes(mem.Sys), mem.NumGC))
}
// Pwd prints the current working directory
func Pwd(args []string) {
wd, err := os.Getwd()
if err != nil {
messenger.Message(err.Error())
} else {
messenger.Message(wd)
}
}
// Open opens a new buffer with a given filename
func Open(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 := shellwords.Split(filename)
if err != nil {
messenger.Error("Error parsing args ", err)
return
}
filename = strings.Join(args, " ")
CurView().Open(filename)
} else {
messenger.Error("No filename")
}
}
// ToggleLog toggles the log view
func ToggleLog(args []string) {
buffer := messenger.getBuffer()
if CurView().Type != vtLog {
CurView().HSplit(buffer)
CurView().Type = vtLog
RedrawAll()
buffer.Cursor.Loc = buffer.Start()
CurView().Relocate()
buffer.Cursor.Loc = buffer.End()
CurView().Relocate()
} else {
CurView().Quit(true)
}
}
// Reload reloads all files (syntax files, colorschemes...)
func Reload(args []string) {
LoadAll()
}
// 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 FindRuntimeFile(RTHelp, helpPage) != nil {
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(NewBufferFromString("", ""))
} else {
filename := args[0]
filename = ReplaceHome(filename)
file, err := os.Open(filename)
fileInfo, _ := os.Stat(filename)
if err == nil && fileInfo.IsDir() {
messenger.Error(filename, " is a directory")
return
}
defer file.Close()
var buf *Buffer
if err != nil {
// File does not exist -- create an empty buffer with that name
buf = NewBufferFromString("", filename)
} else {
buf = NewBuffer(file, FSize(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(NewBufferFromString("", ""))
} else {
filename := args[0]
filename = ReplaceHome(filename)
file, err := os.Open(filename)
fileInfo, _ := os.Stat(filename)
if err == nil && fileInfo.IsDir() {
messenger.Error(filename, " is a directory")
return
}
defer file.Close()
var buf *Buffer
if err != nil {
// File does not exist -- create an empty buffer with that name
buf = NewBufferFromString("", filename)
} else {
buf = NewBuffer(file, FSize(file), filename)
}
CurView().HSplit(buf)
}
}
// Eval evaluates a lua expression
func Eval(args []string) {
if len(args) >= 1 {
err := L.DoString(args[0])
if err != nil {
messenger.Error(err)
}
} else {
messenger.Error("Not enough arguments")
}
}
// NewTab opens the given file in a new tab
func NewTab(args []string) {
if len(args) == 0 {
CurView().AddTab(true)
} else {
filename := args[0]
filename = ReplaceHome(filename)
file, err := os.Open(filename)
fileInfo, _ := os.Stat(filename)
if err == nil && fileInfo.IsDir() {
messenger.Error(filename, " is a directory")
return
}
defer file.Close()
var buf *Buffer
if err != nil {
buf = NewBufferFromString("", filename)
} else {
buf = NewBuffer(file, FSize(file), filename)
}
tab := NewTabFromView(NewView(buf))
tab.SetNum(len(tabs))
tabs = append(tabs, tab)
curTab = len(tabs) - 1
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 := args[0]
value := 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 := args[0]
value := 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)
}
// ShowKey displays the action that a key is bound to
func ShowKey(args []string) {
if len(args) < 1 {
messenger.Error("Please provide a key to show")
return
}
if action, ok := bindingsStr[args[0]]; ok {
messenger.Message(action)
} else {
messenger.Message(args[0], " has no binding")
}
}
// 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(shellwords.Join(args...), false, true)
}
// 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) {
if len(args) == 0 {
// Save the main view
CurView().Save(true)
} else {
CurView().Buf.SaveAs(args[0])
}
}
// Replace runs search and replace
func Replace(args []string) {
if len(args) < 2 || len(args) > 4 {
// We need to find both a search and replace expression
messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
return
}
all := false
noRegex := false
if len(args) > 2 {
for _, arg := range args[2:] {
switch arg {
case "-a":
all = true
case "-l":
noRegex = true
default:
messenger.Error("Invalid flag: " + arg)
return
}
}
}
search := string(args[0])
if noRegex {
search = regexp.QuoteMeta(search)
}
replace := string(args[1])
regex, err := regexp.Compile("(?m)" + search)
if err != nil {
// There was an error with the user's regex
messenger.Error(err.Error())
return
}
view := CurView()
found := 0
replaceAll := func() {
var deltas []Delta
deltaXOffset := Count(replace) - Count(search)
for i := 0; i < view.Buf.LinesNum(); i++ {
matches := regex.FindAllIndex(view.Buf.lines[i].data, -1)
str := string(view.Buf.lines[i].data)
if matches != nil {
xOffset := 0
for _, m := range matches {
from := Loc{runePos(m[0], str) + xOffset, i}
to := Loc{runePos(m[1], str) + xOffset, i}
xOffset += deltaXOffset
deltas = append(deltas, Delta{replace, from, to})
found++
}
}
}
view.Buf.MultipleReplace(deltas)
}
if all {
replaceAll()
} else {
for {
// The 'check' flag was used
Search(search, view, true)
if !view.Cursor.HasSelection() {
break
}
view.Relocate()
RedrawAll()
choice, canceled := messenger.LetterPrompt("Perform replacement? (y,n,a)", 'y', 'n', 'a')
if canceled {
if view.Cursor.HasSelection() {
view.Cursor.Loc = view.Cursor.CurSelection[0]
view.Cursor.ResetSelection()
}
messenger.Reset()
break
} else if choice == 'a' {
if view.Cursor.HasSelection() {
view.Cursor.Loc = view.Cursor.CurSelection[0]
view.Cursor.ResetSelection()
}
messenger.Reset()
replaceAll()
break
} else if choice == 'y' {
view.Cursor.DeleteSelection()
view.Buf.Insert(view.Cursor.Loc, replace)
view.Cursor.ResetSelection()
messenger.Reset()
found++
}
if view.Cursor.HasSelection() {
searchStart = view.Cursor.CurSelection[1]
} else {
searchStart = view.Cursor.Loc
}
}
}
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)
}
}
// ReplaceAll replaces search term all at once
func ReplaceAll(args []string) {
// aliased to Replace command
Replace(append(args, "-a"))
}
// Term opens a terminal in the current view
func Term(args []string) {
var err error
if len(args) == 0 {
err = CurView().StartTerminal([]string{os.Getenv("SHELL"), "-i"}, true, false, "")
} else {
err = CurView().StartTerminal(args, true, false, "")
}
if err != nil {
messenger.Error(err)
}
}
// HandleCommand handles input from the user
func HandleCommand(input string) {
args, err := shellwords.Split(input)
if err != nil {
messenger.Error("Error parsing args ", err)
return
}
inputCmd := args[0]
if _, ok := commands[inputCmd]; !ok {
messenger.Error("Unknown command ", inputCmd)
} else {
commands[inputCmd].action(args[1:])
}
}

31
cmd/micro/debug.go Normal file
View 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{})
}
}

View File

@@ -1,28 +0,0 @@
package main
import "github.com/zyedidia/micro/cmd/micro/highlight"
var syntaxFiles []*highlight.File
func LoadSyntaxFiles() {
InitColorscheme()
for _, f := range ListRuntimeFiles(RTSyntax) {
data, err := f.Data()
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
} else {
LoadSyntaxFile(data, f.Name())
}
}
}
func LoadSyntaxFile(text []byte, filename string) {
f, err := highlight.ParseFile(text)
if err != nil {
TermMessage("Syntax file error: " + filename + ": " + err.Error())
return
}
syntaxFiles = append(syntaxFiles, f)
}

129
cmd/micro/initlua.go Normal file
View File

@@ -0,0 +1,129 @@
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))
}
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))
return pkg
}
func luaImportMicroConfig() *lua.LTable {
pkg := ulua.L.NewTable()
ulua.L.SetField(pkg, "MakeCommand", luar.New(ulua.L, action.LuaMakeCommand))
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.PluginAddRuntimeFileFromMemory))
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, "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.RegisterCommonOption))
ulua.L.SetField(pkg, "RegisterGlobalOption", luar.New(ulua.L, config.RegisterGlobalOption))
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, "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))
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))
return pkg
}

View File

@@ -1,20 +0,0 @@
package main
// DisplayKeyMenu displays the nano-style key menu at the bottom of the screen
func DisplayKeyMenu() {
w, h := screen.Size()
bot := h - 3
display := []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"}
for y := 0; y < len(display); y++ {
for x := 0; x < w; x++ {
if x < len(display[y]) {
screen.SetContent(x, bot+y, rune(display[y][x]), nil, defStyle)
} else {
screen.SetContent(x, bot+y, ' ', nil, defStyle)
}
}
}
}

View File

@@ -1,664 +0,0 @@
package main
import (
"bufio"
"bytes"
"encoding/gob"
"fmt"
"os"
"strconv"
"github.com/mattn/go-runewidth"
"github.com/zyedidia/clipboard"
"github.com/zyedidia/micro/cmd/micro/shellwords"
"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()
screen = nil
}
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 {
log *Buffer
// 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
}
// AddLog sends a message to the log view
func (m *Messenger) AddLog(msg ...interface{}) {
logMessage := fmt.Sprint(msg...)
buffer := m.getBuffer()
buffer.insert(buffer.End(), []byte(logMessage+"\n"))
buffer.Cursor.Loc = buffer.End()
buffer.Cursor.Relocate()
}
func (m *Messenger) getBuffer() *Buffer {
if m.log == nil {
m.log = NewBufferFromString("", "")
m.log.name = "Log"
}
return m.log
}
// Message sends a message to the user
func (m *Messenger) Message(msg ...interface{}) {
displayMessage := fmt.Sprint(msg...)
// only display a new message if there isn't an active prompt
// this is to prevent overwriting an existing prompt to the user
if m.hasPrompt == false {
// if there is no active prompt then style and display the message as normal
m.message = displayMessage
m.style = defStyle
if _, ok := colorscheme["message"]; ok {
m.style = colorscheme["message"]
}
m.hasMessage = true
}
// add the message to the log regardless of active prompts
m.AddLog(displayMessage)
}
// Error sends an error message to the user
func (m *Messenger) Error(msg ...interface{}) {
buf := new(bytes.Buffer)
fmt.Fprint(buf, msg...)
// only display a new message if there isn't an active prompt
// this is to prevent overwriting an existing prompt to the user
if m.hasPrompt == false {
// if there is no active prompt then style and display the message as normal
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
}
// add the message to the log regardless of active prompts
m.AddLog(buf.String())
}
func (m *Messenger) PromptText(msg ...interface{}) {
displayMessage := fmt.Sprint(msg...)
// if there is no active prompt then style and display the message as normal
m.message = displayMessage
m.style = defStyle
if _, ok := colorscheme["message"]; ok {
m.style = colorscheme["message"]
}
m.hasMessage = true
// add the message to the log regardless of active prompts
m.AddLog(displayMessage)
}
// 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.hasPrompt = true
m.PromptText(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' || e.Rune() == 'Y' {
m.AddLog("\t--> y")
m.hasPrompt = false
return true, false
} else if e.Rune() == 'n' || e.Rune() == 'N' {
m.AddLog("\t--> n")
m.hasPrompt = false
return false, false
}
case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
m.AddLog("\t--> (cancel)")
m.Clear()
m.Reset()
m.hasPrompt = false
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.hasPrompt = true
m.PromptText(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.AddLog("\t--> " + string(r))
m.Clear()
m.Reset()
m.hasPrompt = false
return r, false
}
}
case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
m.AddLog("\t--> (cancel)")
m.Clear()
m.Reset()
m.hasPrompt = false
return ' ', true
}
}
}
}
// Completion represents a type of completion
type Completion int
const (
NoCompletion Completion = iota
FileCompletion
CommandCompletion
HelpCompletion
OptionCompletion
PluginCmdCompletion
PluginNameCompletion
OptionValueCompletion
)
// 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, placeholder, historyType string, completionTypes ...Completion) (string, bool) {
m.hasPrompt = true
m.PromptText(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 := placeholder, true
m.response = response
m.cursorx = Count(placeholder)
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.AddLog("\t--> (cancel)")
m.hasPrompt = false
case tcell.KeyEnter:
// User is done entering their response
m.AddLog("\t--> " + m.response)
m.hasPrompt = false
response, canceled = m.response, false
m.history[historyType][len(m.history[historyType])-1] = response
case tcell.KeyTab:
args, err := shellwords.Split(m.response)
if err != nil {
break
}
currentArg := ""
currentArgNum := 0
if len(args) > 0 {
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)
} else if completionType == OptionValueCompletion {
if currentArgNum-1 > 0 {
chosen, suggestions = OptionValueComplete(args[currentArgNum-1], currentArg)
}
} else if completionType == PluginCmdCompletion {
chosen, suggestions = PluginCmdComplete(currentArg)
} else if completionType == PluginNameCompletion {
chosen, suggestions = PluginNameComplete(currentArg)
} else if completionType < NoCompletion {
chosen, suggestions = PluginComplete(completionType, currentArg)
}
if len(suggestions) > 1 {
chosen = chosen + CommonSubstring(suggestions...)
}
if len(suggestions) != 0 && chosen != "" {
m.response = shellwords.Join(append(args[:len(args)-1], chosen)...)
m.cursorx = Count(m.response)
}
}
}
m.HandleEvent(event, m.history[historyType])
m.Clear()
for _, v := range tabs[curTab].views {
v.Display()
}
DisplayTabs()
m.Display()
if len(suggestions) > 1 {
m.DisplaySuggestions(suggestions)
}
screen.Show()
}
m.Clear()
m.Reset()
return response, canceled
}
// UpHistory fetches the previous item in the history
func (m *Messenger) UpHistory(history []string) {
if m.historyNum > 0 {
m.historyNum--
m.response = history[m.historyNum]
m.cursorx = Count(m.response)
}
}
// DownHistory fetches the next item in the history
func (m *Messenger) DownHistory(history []string) {
if m.historyNum < len(history)-1 {
m.historyNum++
m.response = history[m.historyNum]
m.cursorx = Count(m.response)
}
}
// CursorLeft moves the cursor one character left
func (m *Messenger) CursorLeft() {
if m.cursorx > 0 {
m.cursorx--
}
}
// CursorRight moves the cursor one character right
func (m *Messenger) CursorRight() {
if m.cursorx < Count(m.response) {
m.cursorx++
}
}
// Start moves the cursor to the start of the line
func (m *Messenger) Start() {
m.cursorx = 0
}
// End moves the cursor to the end of the line
func (m *Messenger) End() {
m.cursorx = Count(m.response)
}
// Backspace deletes one character
func (m *Messenger) Backspace() {
if m.cursorx > 0 {
m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
m.cursorx--
}
}
// Paste pastes the clipboard
func (m *Messenger) Paste() {
clip, _ := clipboard.ReadAll("clipboard")
m.response = Insert(m.response, m.cursorx, clip)
m.cursorx += Count(clip)
}
// WordLeft moves the cursor one word to the left
func (m *Messenger) WordLeft() {
response := []rune(m.response)
m.CursorLeft()
if m.cursorx <= 0 {
return
}
for IsWhitespace(response[m.cursorx]) {
if m.cursorx <= 0 {
return
}
m.CursorLeft()
}
m.CursorLeft()
for IsWordChar(string(response[m.cursorx])) {
if m.cursorx <= 0 {
return
}
m.CursorLeft()
}
m.CursorRight()
}
// WordRight moves the cursor one word to the right
func (m *Messenger) WordRight() {
response := []rune(m.response)
if m.cursorx >= len(response) {
return
}
for IsWhitespace(response[m.cursorx]) {
m.CursorRight()
if m.cursorx >= len(response) {
m.CursorRight()
return
}
}
m.CursorRight()
if m.cursorx >= len(response) {
return
}
for IsWordChar(string(response[m.cursorx])) {
m.CursorRight()
if m.cursorx >= len(response) {
return
}
}
}
// DeleteWordLeft deletes one word to the left
func (m *Messenger) DeleteWordLeft() {
m.WordLeft()
m.response = string([]rune(m.response)[:m.cursorx])
}
// 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.KeyCtrlA:
m.Start()
case tcell.KeyCtrlE:
m.End()
case tcell.KeyUp:
m.UpHistory(history)
case tcell.KeyDown:
m.DownHistory(history)
case tcell.KeyLeft:
if e.Modifiers() == tcell.ModCtrl {
m.Start()
} else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
m.WordLeft()
} else {
m.CursorLeft()
}
case tcell.KeyRight:
if e.Modifiers() == tcell.ModCtrl {
m.End()
} else if e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
m.WordRight()
} else {
m.CursorRight()
}
case tcell.KeyBackspace2, tcell.KeyBackspace:
if e.Modifiers() == tcell.ModCtrl || e.Modifiers() == tcell.ModAlt || e.Modifiers() == tcell.ModMeta {
m.DeleteWordLeft()
} else {
m.Backspace()
}
case tcell.KeyCtrlW:
m.DeleteWordLeft()
case tcell.KeyCtrlV:
m.Paste()
case tcell.KeyCtrlF:
m.WordRight()
case tcell.KeyCtrlB:
m.WordLeft()
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)
case *tcell.EventMouse:
x, y := e.Position()
x -= Count(m.message)
button := e.Buttons()
_, screenH := screen.Size()
if y == screenH-1 {
switch button {
case tcell.Button1:
m.cursorx = x
if m.cursorx < 0 {
m.cursorx = 0
} else if m.cursorx > Count(m.response) {
m.cursorx = Count(m.response)
}
}
}
}
}
// 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 {
if m.hasPrompt || globalSettings["infobar"].(bool) {
runes := []rune(m.message + m.response)
posx := 0
for x := 0; x < len(runes); x++ {
screen.SetContent(posx, h-1, runes[x], nil, m.style)
posx += runewidth.RuneWidth(runes[x])
}
}
}
if m.hasPrompt {
screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
screen.Show()
}
}
// LoadHistory attempts to load user history from configDir/buffers/history
// into the history map
// The savehistory option must be on
func (m *Messenger) LoadHistory() {
if GetGlobalOption("savehistory").(bool) {
file, err := os.Open(configDir + "/buffers/history")
var decodedMap map[string][]string
if err == nil {
decoder := gob.NewDecoder(file)
err = decoder.Decode(&decodedMap)
file.Close()
if err != nil {
m.Error("Error loading history:", err)
return
}
}
if decodedMap != nil {
m.history = decodedMap
} else {
m.history = make(map[string][]string)
}
} else {
m.history = make(map[string][]string)
}
}
// SaveHistory saves the user's command history to configDir/buffers/history
// only if the savehistory option is on
func (m *Messenger) SaveHistory() {
if GetGlobalOption("savehistory").(bool) {
// Don't save history past 100
for k, v := range m.history {
if len(v) > 100 {
m.history[k] = v[len(m.history[k])-100:]
}
}
file, err := os.Create(configDir + "/buffers/history")
if err == nil {
encoder := gob.NewEncoder(file)
err = encoder.Encode(m.history)
if err != nil {
m.Error("Error saving history:", err)
return
}
file.Close()
}
}
}
// 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
)

View File

@@ -5,68 +5,87 @@ import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"time"
"sort"
"github.com/go-errors/errors"
"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"
"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"
"layeh.com/gopher-luar"
)
const (
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
autosaveTime = 8 // Number of seconds to wait before autosaving
)
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 = "0.0.0-unknown"
// CommitHash is the commit this version was built on
CommitHash = "Unknown"
// CompileDate is the date this binary was compiled on
CompileDate = "Unknown"
// 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
autosave chan bool
updateterm chan bool
closeterm chan int
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")
optionFlags map[string]*string
)
func InitFlags() {
flag.Usage = func() {
fmt.Println("Usage: micro [OPTIONS] [FILE]...")
fmt.Println("-config-dir dir")
fmt.Println(" \tSpecify a custom location for the configuration directory")
fmt.Println("[FILE]:LINE:COL")
fmt.Println(" \tSpecify a line and column to start the cursor at when opening a buffer")
fmt.Println(" \tThis can also be done by opening file:LINE:COL")
fmt.Println("-options")
fmt.Println(" \tShow all option help")
fmt.Println("-version")
fmt.Println(" \tShow the version number and information")
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)
}
}
// LoadInput determines which files should be loaded into buffers
// based on the input stored in flag.Args()
func LoadInput() []*Buffer {
func LoadInput() []*buffer.Buffer {
// There are a number of ways micro should start given its input
// 1. If it is given a files in flag.Args(), it should open those
@@ -82,36 +101,19 @@ func LoadInput() []*Buffer {
var input []byte
var err error
args := flag.Args()
buffers := make([]*Buffer, 0, len(args))
buffers := make([]*buffer.Buffer, 0, len(args))
if len(args) > 0 {
// Option 1
// We go through each file and load it
for i := 0; i < len(args); i++ {
filename = args[i]
// Check that the file exists
var input *os.File
if _, e := os.Stat(filename); e == nil {
// If it exists we load it into a buffer
input, err = os.Open(filename)
stat, _ := input.Stat()
defer input.Close()
if err != nil {
TermMessage(err)
continue
}
if stat.IsDir() {
TermMessage("Cannot read", filename, "because it is a directory")
continue
}
buf, err := buffer.NewBufferFromFile(args[i], buffer.BTDefault)
if err != nil {
screen.TermMessage(err)
continue
}
// If the file didn't exist, input will be empty, and we'll open an empty buffer
if input != nil {
buffers = append(buffers, NewBuffer(input, FSize(input), filename))
} else {
buffers = append(buffers, NewBufferFromString("", filename))
}
buffers = append(buffers, buf)
}
} else if !isatty.IsTerminal(os.Stdin.Fd()) {
// Option 2
@@ -119,446 +121,141 @@ 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, NewBufferFromString(string(input), filename))
buffers = append(buffers, buffer.NewBufferFromString(string(input), filename, buffer.BTDefault))
} else {
// Option 3, just open an empty buffer
buffers = append(buffers, NewBufferFromString(string(input), filename))
buffers = append(buffers, buffer.NewBufferFromString(string(input), filename, buffer.BTDefault))
}
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 len(*flagConfigDir) > 0 {
if _, err := os.Stat(*flagConfigDir); os.IsNotExist(err) {
TermMessage("Error: " + *flagConfigDir + " does not exist. Defaulting to " + configDir + ".")
} else {
configDir = *flagConfigDir
return
}
}
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)
if err == tcell.ErrTermNotFound {
fmt.Println("Micro does not recognize your terminal:", oldTerm)
fmt.Println("Please go to https://github.com/zyedidia/mkinfo to read about how to fix this problem (it should be easy to fix).")
}
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)
}
if GetGlobalOption("mouse").(bool) {
screen.EnableMouse()
}
// screen.SetStyle(defStyle)
}
// RedrawAll redraws everything -- all the views and the messenger
func RedrawAll() {
messenger.Clear()
w, h := screen.Size()
for x := 0; x < w; x++ {
for y := 0; y < h; y++ {
screen.SetContent(x, y, ' ', nil, defStyle)
}
}
for _, v := range tabs[curTab].views {
v.Display()
}
DisplayTabs()
messenger.Display()
if globalSettings["keymenu"].(bool) {
DisplayKeyMenu()
}
screen.Show()
}
func LoadAll() {
// Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
InitConfigDir()
// Build a list of available Extensions (Syntax, Colorscheme etc.)
InitRuntimeFiles()
// Load the user's settings
InitGlobalSettings()
InitCommands()
InitBindings()
InitColorscheme()
for _, tab := range tabs {
for _, v := range tab.views {
v.Buf.UpdateRules()
}
}
}
// Command line flags
var flagVersion = flag.Bool("version", false, "Show the version number and information")
var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at when opening a buffer.")
var flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
var flagOptions = flag.Bool("options", false, "Show all option help")
func main() {
flag.Usage = func() {
fmt.Println("Usage: micro [OPTIONS] [FILE]...")
fmt.Println("-config-dir dir")
fmt.Println(" \tSpecify a custom location for the configuration directory")
fmt.Println("-startpos 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("-version")
fmt.Println(" \tShow the version number and information")
defer os.Exit(0)
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")
// runtime.SetCPUProfileRate(400)
// f, _ := os.Create("micro.prof")
// pprof.StartCPUProfile(f)
// defer pprof.StopCPUProfile()
var err error
InitLog()
InitFlags()
err = config.InitConfigDir(*flagConfigDir)
if err != nil {
screen.TermMessage(err)
}
optionFlags := make(map[string]*string)
for k, v := range DefaultGlobalSettings() {
optionFlags[k] = flag.String(k, "", fmt.Sprintf("The %s option. Default value: '%v'", k, v))
config.InitRuntimeFiles()
err = config.ReadSettings()
if err != nil {
screen.TermMessage(err)
}
config.InitGlobalSettings()
flag.Parse()
if *flagVersion {
// If -version was passed
fmt.Println("Version:", Version)
fmt.Println("Commit hash:", CommitHash)
fmt.Println("Compiled on", CompileDate)
os.Exit(0)
}
if *flagOptions {
// If -options was passed
for k, v := range DefaultGlobalSettings() {
fmt.Printf("-%s value\n", k)
fmt.Printf(" \tThe %s option. Default value: '%v'\n", k, v)
// 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
}
os.Exit(0)
}
// Start the Lua VM for running plugins
L = lua.NewState()
defer L.Close()
action.InitBindings()
action.InitCommands()
// Some encoding stuff in case the user isn't using UTF-8
encoding.Register()
tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
err = config.InitColorscheme()
if err != nil {
screen.TermMessage(err)
}
// Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
InitConfigDir()
err = config.LoadAllPlugins()
if err != nil {
screen.TermMessage(err)
}
err = config.RunPluginFn("init")
if err != nil {
screen.TermMessage(err)
}
// Build a list of available Extensions (Syntax, Colorscheme etc.)
InitRuntimeFiles()
screen.Init()
// Load the user's settings
InitGlobalSettings()
InitCommands()
InitBindings()
// Start the screen
InitScreen()
// This is just so if we have an error, we can exit cleanly and not completely
// 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.LoadHistory()
// Now we load the input
buffers := LoadInput()
if len(buffers) == 0 {
screen.Fini()
os.Exit(1)
}
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)
}
t.Resize()
}
}
for k, v := range optionFlags {
if *v != "" {
SetOption(k, *v)
}
}
// 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("ExecCommand", luar.New(L, ExecCommand))
L.SetGlobal("RunShellCommand", luar.New(L, RunShellCommand))
L.SetGlobal("RunBackgroundShell", luar.New(L, RunBackgroundShell))
L.SetGlobal("RunInteractiveShell", luar.New(L, RunInteractiveShell))
L.SetGlobal("TermEmuSupported", luar.New(L, TermEmuSupported))
L.SetGlobal("RunTermEmulator", luar.New(L, RunTermEmulator))
L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion))
L.SetGlobal("NewBuffer", luar.New(L, NewBufferFromString))
L.SetGlobal("RuneStr", luar.New(L, func(r rune) string {
return string(r)
}))
L.SetGlobal("Loc", luar.New(L, func(x, y int) Loc {
return Loc{x, y}
}))
L.SetGlobal("WorkingDirectory", luar.New(L, os.Getwd))
L.SetGlobal("JoinPaths", luar.New(L, filepath.Join))
L.SetGlobal("DirectoryName", luar.New(L, filepath.Dir))
L.SetGlobal("configDir", luar.New(L, configDir))
L.SetGlobal("Reload", luar.New(L, LoadAll))
L.SetGlobal("ByteOffset", luar.New(L, ByteOffset))
L.SetGlobal("ToCharPos", luar.New(L, ToCharPos))
// Used for asynchronous jobs
L.SetGlobal("JobStart", luar.New(L, JobStart))
L.SetGlobal("JobSpawn", luar.New(L, JobSpawn))
L.SetGlobal("JobSend", luar.New(L, JobSend))
L.SetGlobal("JobStop", luar.New(L, JobStop))
// Extension Files
L.SetGlobal("ReadRuntimeFile", luar.New(L, PluginReadRuntimeFile))
L.SetGlobal("ListRuntimeFiles", luar.New(L, PluginListRuntimeFiles))
L.SetGlobal("AddRuntimeFile", luar.New(L, PluginAddRuntimeFile))
L.SetGlobal("AddRuntimeFilesFromDirectory", luar.New(L, PluginAddRuntimeFilesFromDirectory))
L.SetGlobal("AddRuntimeFileFromMemory", luar.New(L, PluginAddRuntimeFileFromMemory))
// Access to Go stdlib
L.SetGlobal("import", luar.New(L, Import))
jobs = make(chan JobFunction, 100)
events = make(chan tcell.Event, 100)
autosave = make(chan bool)
updateterm = make(chan bool)
closeterm = make(chan int)
LoadPlugins()
for _, t := range tabs {
for _, v := range t.views {
GlobalPluginCall("onViewOpen", v)
GlobalPluginCall("onBufferOpen", v.Buf)
}
}
InitColorscheme()
messenger.style = defStyle
b := LoadInput()
action.InitTabs(b)
action.InitGlobals()
// Here is the event loop which runs in a separate thread
go func() {
events = make(chan tcell.Event)
for {
if screen != nil {
events <- screen.PollEvent()
}
}
}()
go func() {
for {
time.Sleep(autosaveTime * time.Second)
if globalSettings["autosave"].(bool) {
autosave <- true
screen.Lock()
e := screen.Screen.PollEvent()
screen.Unlock()
if e != nil {
events <- e
}
}
}()
for {
// Display everything
RedrawAll()
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()
var event tcell.Event
// Check for new events
select {
case f := <-jobs:
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...)
continue
case <-autosave:
if CurView().Buf.Path != "" {
CurView().Save(true)
f.Function(f.Output, f.Args...)
case <-config.Autosave:
for _, b := range buffer.OpenBuffers {
b.Save()
}
case <-updateterm:
continue
case vnum := <-closeterm:
tabs[curTab].views[vnum].CloseTerminal()
case <-shell.CloseTerms:
case event = <-events:
case <-screen.DrawChan:
}
for event != nil {
didAction := false
switch e := event.(type) {
case *tcell.EventResize:
for _, t := range tabs {
t.Resize()
}
case *tcell.EventMouse:
if !searching {
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 != "" && globalSettings["infobar"].(bool) {
// 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, "primary")
break
}
if CurView().mouseReleased {
// We loop through each view in the current tab and make sure the current view
// is 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
}
}
}
} else if e.Buttons() == tcell.WheelUp || e.Buttons() == tcell.WheelDown {
var view *View
x, y := e.Position()
for _, v := range tabs[curTab].views {
if x >= v.x && x < v.x+v.Width && y >= v.y && y < v.y+v.Height {
view = tabs[curTab].views[v.Num]
}
}
if view != nil {
view.HandleEvent(e)
didAction = true
}
}
}
}
if !didAction {
// 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) {
break
}
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)
}
}
select {
case event = <-events:
default:
event = nil
}
if action.InfoBar.HasPrompt {
action.InfoBar.HandleEvent(event)
} else {
action.Tabs.HandleEvent(event)
}
}
}

View File

@@ -1,184 +0,0 @@
package main
import (
"errors"
"io/ioutil"
"os"
"strings"
"github.com/yuin/gopher-lua"
"github.com/zyedidia/tcell"
"layeh.com/gopher-luar"
)
var loadedPlugins map[string]string
// 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
}
}
// LuaFunctionMouseBinding 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 mouse binding because this is used
// to bind mouse actions to lua functions
func LuaFunctionMouseBinding(function string) func(*View, bool, *tcell.EventMouse) bool {
return func(v *View, _ bool, e *tcell.EventMouse) bool {
_, err := Call(function, e)
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)
}
}
}
// LuaFunctionComplete returns a function which can be used for autocomplete in plugins
func LuaFunctionComplete(function string) func(string) []string {
return func(input string) (result []string) {
res, err := Call(function, input)
if err != nil {
TermMessage(err)
}
if tbl, ok := res.(*lua.LTable); !ok {
TermMessage(function, "should return a table of strings")
} else {
for i := 1; i <= tbl.Len(); i++ {
val := tbl.RawGetInt(i)
if v, ok := val.(lua.LString); !ok {
TermMessage(function, "should return a table of strings")
} else {
result = append(result, string(v))
}
}
}
return result
}
}
// LuaFunctionJob returns a function that will call the given lua function
// structured as a job call i.e. the job output and arguments are provided
// to the lua function
func LuaFunctionJob(function string) func(string, ...string) {
return func(output string, args ...string) {
_, err := Call(function, unpack(append([]string{output}, args...))...)
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
}
}
}
// luaPluginName convert a human-friendly plugin name into a valid lua variable name.
func luaPluginName(name string) string {
return strings.Replace(name, "-", "_", -1)
}
// LoadPlugins loads the pre-installed plugins and the plugins located in ~/.config/micro/plugins
func LoadPlugins() {
loadedPlugins = make(map[string]string)
for _, plugin := range ListRuntimeFiles(RTPlugin) {
pluginName := plugin.Name()
if _, ok := loadedPlugins[pluginName]; ok {
continue
}
data, err := plugin.Data()
if err != nil {
TermMessage("Error loading plugin: " + pluginName)
continue
}
pluginLuaName := luaPluginName(pluginName)
if err := LoadFile(pluginLuaName, pluginLuaName, string(data)); err != nil {
TermMessage(err)
continue
}
loadedPlugins[pluginName] = pluginLuaName
}
if _, err := os.Stat(configDir + "/init.lua"); err == nil {
data, _ := ioutil.ReadFile(configDir + "/init.lua")
if err := LoadFile("init", configDir+"init.lua", string(data)); err != nil {
TermMessage(err)
}
loadedPlugins["init"] = "init"
}
}
// GlobalCall makes a call to a function in every plugin that is currently
// loaded
func GlobalPluginCall(function string, args ...interface{}) {
for pl := range loadedPlugins {
_, err := Call(pl+"."+function, args...)
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
continue
}
}
}

View File

@@ -1,622 +0,0 @@
package main
import (
"archive/zip"
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/blang/semver"
"github.com/flynn/json5"
"github.com/yuin/gopher-lua"
)
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
}
// 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() PluginPackages {
return fetchAllSources(len(pc), func(i int) PluginPackages {
return pc[i].Fetch()
})
}
// Fetch retrieves all available PluginPackages from the given channel
func (pc PluginChannel) Fetch() PluginPackages {
// messenger.AddLog(fmt.Sprintf("Fetching channel: %q", string(pc)))
resp, err := http.Get(string(pc))
if err != nil {
TermMessage("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 {
TermMessage("Failed to decode channel data:\n", err)
return PluginPackages{}
}
return fetchAllSources(len(repositories), func(i int) PluginPackages {
return repositories[i].Fetch()
})
}
// Fetch retrieves all available PluginPackages from the given repository
func (pr PluginRepository) Fetch() PluginPackages {
// messenger.AddLog(fmt.Sprintf("Fetching repository: %q", string(pr)))
resp, err := http.Get(string(pr))
if err != nil {
TermMessage("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 {
TermMessage("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() PluginPackages {
if allPluginPackages == nil {
getOption := func(name string) []string {
data := GetOption(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()
}
return repos[i-1].Fetch()
})
}
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() error {
_, err := GetAllPluginPackages().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(texts []string) (plugins PluginPackages) {
plugins = make(PluginPackages, 0)
pluginLoop:
for _, pp := range GetAllPluginPackages() {
for _, text := range texts {
if !pp.Match(text) {
continue pluginLoop
}
}
if err := pp.IsInstallable(); err == nil {
plugins = append(plugins, pp)
}
}
return
}
func isUnknownCoreVersion() bool {
_, err := semver.ParseTolerant(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, Version))
}
for name, lpname := range loadedPlugins {
version := GetInstalledPluginVersion(lpname)
if pv := newStaticPluginVersion(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 := L.GetGlobal(name)
if plugin != lua.LNil {
version := 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() error {
messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", 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, "plugins", 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() {
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) {
messenger.AddLog(fmt.Sprint("Uninstalling %q", sel.pack.Name))
UninstallPlugin(sel.pack.Name)
} else {
shouldInstall = false
}
}
if shouldInstall {
if err := sel.DownloadAndInstall(); err != nil {
messenger.Error(err)
return
}
anyInstalled = true
}
}
}
if anyInstalled {
messenger.Message("One or more plugins installed. Please restart micro.")
} else {
messenger.AddLog("Nothing to install / update")
}
}
// UninstallPlugin deletes the plugin folder of the given plugin
func UninstallPlugin(name string) {
if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
messenger.Error(err)
return
}
delete(loadedPlugins, name)
}
// Install installs the plugin
func (pl PluginPackage) Install() {
selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
&PluginDependency{
Name: pl.Name,
Range: semver.Range(func(v semver.Version) bool { return true }),
}})
if err != nil {
TermMessage(err)
return
}
selected.install()
}
// UpdatePlugins updates the given plugins
func UpdatePlugins(plugins []string) {
// if no plugins are specified, update all installed plugins.
if len(plugins) == 0 {
for name := range loadedPlugins {
plugins = append(plugins, name)
}
}
messenger.AddLog("Checking for plugin updates")
microVersion := PluginVersions{
newStaticPluginVersion(CorePluginName, 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().Resolve(microVersion, updates)
if err != nil {
TermMessage(err)
return
}
selected.install()
}

View File

@@ -1,56 +0,0 @@
package main
import (
"testing"
"github.com/blang/semver"
"github.com/flynn/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 got %s", 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)
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,20 +0,0 @@
package main
// ScrollBar represents an optional scrollbar that can be used
type ScrollBar struct {
view *View
}
// Display shows the scrollbar
func (sb *ScrollBar) Display() {
style := defStyle.Reverse(true)
screen.SetContent(sb.view.x+sb.view.Width-1, sb.view.y+sb.pos(), ' ', nil, style)
}
func (sb *ScrollBar) pos() int {
numlines := sb.view.Buf.NumLines
h := sb.view.Height
filepercent := float32(sb.view.Topline) / float32(numlines)
return int(filepercent * float32(h))
}

View File

@@ -1,190 +0,0 @@
package main
import (
"regexp"
"strings"
"github.com/zyedidia/tcell"
)
var (
// What was the last search
lastSearch string
// Where should we start the search down from (or up from)
searchStart Loc
// Is there currently a search in progress
searching bool
// Stores the history for searching
searchHistory []string
)
// BeginSearch starts a search
func BeginSearch(searchStr string) {
searchHistory = append(searchHistory, "")
messenger.historyNum = len(searchHistory) - 1
searching = true
messenger.response = searchStr
messenger.cursorx = Count(searchStr)
messenger.Message("Find: ")
messenger.hasPrompt = true
}
// 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")
}
}
// ExitSearch exits the search mode, reset active search phrase, and clear status bar
func ExitSearch(v *View) {
lastSearch = ""
searching = false
messenger.hasPrompt = false
messenger.Clear()
messenger.Reset()
v.Cursor.ResetSelection()
}
// 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.KeyEscape:
// Exit the search mode
ExitSearch(v)
return
case tcell.KeyEnter:
// If the user has pressed Enter, they want this to be the lastSearch
lastSearch = messenger.response
EndSearch()
return
case tcell.KeyCtrlQ, tcell.KeyCtrlC:
// 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)
v.Relocate()
return
}
func searchDown(r *regexp.Regexp, v *View, start, end Loc) bool {
for i := start.Y; i <= end.Y; i++ {
var l []byte
var charPos int
if i == start.Y {
runes := []rune(string(v.Buf.lines[i].data))
l = []byte(string(runes[start.X:]))
charPos = start.X
if strings.Contains(r.String(), "^") && start.X != 0 {
continue
}
} else {
l = v.Buf.lines[i].data
}
match := r.FindIndex(l)
if match != nil {
v.Cursor.SetSelectionStart(Loc{charPos + runePos(match[0], string(l)), i})
v.Cursor.SetSelectionEnd(Loc{charPos + runePos(match[1], string(l)), i})
v.Cursor.OrigSelection[0] = v.Cursor.CurSelection[0]
v.Cursor.OrigSelection[1] = v.Cursor.CurSelection[1]
v.Cursor.Loc = v.Cursor.CurSelection[1]
return true
}
}
return false
}
func searchUp(r *regexp.Regexp, v *View, start, end Loc) bool {
for i := start.Y; i >= end.Y; i-- {
var l []byte
if i == start.Y {
runes := []rune(string(v.Buf.lines[i].data))
l = []byte(string(runes[:start.X]))
if strings.Contains(r.String(), "$") && start.X != Count(string(l)) {
continue
}
} else {
l = v.Buf.lines[i].data
}
match := r.FindIndex(l)
if match != nil {
v.Cursor.SetSelectionStart(Loc{runePos(match[0], string(l)), i})
v.Cursor.SetSelectionEnd(Loc{runePos(match[1], string(l)), i})
v.Cursor.OrigSelection[0] = v.Cursor.CurSelection[0]
v.Cursor.OrigSelection[1] = v.Cursor.CurSelection[1]
v.Cursor.Loc = v.Cursor.CurSelection[1]
return true
}
}
return false
}
// 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
}
r, err := regexp.Compile(searchStr)
if v.Buf.Settings["ignorecase"].(bool) {
r, err = regexp.Compile("(?i)" + searchStr)
}
if err != nil {
return
}
var found bool
if down {
found = searchDown(r, v, searchStart, v.Buf.End())
if !found {
found = searchDown(r, v, v.Buf.Start(), searchStart)
}
} else {
found = searchUp(r, v, searchStart, v.Buf.Start())
if !found {
found = searchUp(r, v, v.Buf.End(), searchStart)
}
}
if !found {
v.Cursor.ResetSelection()
}
}

View File

@@ -1,517 +0,0 @@
package main
import (
"crypto/md5"
"encoding/json"
"errors"
"io/ioutil"
"os"
"reflect"
"strconv"
"strings"
"github.com/flynn/json5"
"github.com/zyedidia/glob"
)
type optionValidator func(string, interface{}) error
// The options that the user can set
var globalSettings map[string]interface{}
var invalidSettings bool
// Options with validators
var optionValidators = map[string]optionValidator{
"tabsize": validatePositiveValue,
"scrollmargin": validateNonNegativeValue,
"scrollspeed": validateNonNegativeValue,
"colorscheme": validateColorscheme,
"colorcolumn": validateNonNegativeValue,
"fileformat": validateLineEnding,
}
// InitGlobalSettings initializes the options map and sets all options to their default values
func InitGlobalSettings() {
invalidSettings = false
defaults := DefaultGlobalSettings()
var parsed map[string]interface{}
filename := configDir + "/settings.json"
writeSettings := false
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if !strings.HasPrefix(string(input), "null") {
if err != nil {
TermMessage("Error reading settings.json file: " + err.Error())
invalidSettings = true
return
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
TermMessage("Error reading settings.json:", err.Error())
invalidSettings = true
}
} else {
writeSettings = true
}
}
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) || writeSettings {
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) {
invalidSettings = false
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())
invalidSettings = true
return
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
TermMessage("Error reading settings.json:", err.Error())
invalidSettings = true
}
}
for k, v := range parsed {
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
if strings.HasPrefix(k, "ft:") {
if buf.Settings["filetype"].(string) == k[3:] {
for k1, v1 := range v.(map[string]interface{}) {
buf.Settings[k1] = v1
}
}
} else {
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 {
if invalidSettings {
// Do not write the settings if there was an error when reading them
return nil
}
var err error
if _, e := os.Stat(configDir); e == nil {
parsed := make(map[string]interface{})
filename := configDir + "/settings.json"
for k, v := range globalSettings {
parsed[k] = v
}
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if string(input) != "null" {
if err != nil {
return err
}
err = json5.Unmarshal(input, &parsed)
if err != nil {
TermMessage("Error reading settings.json:", err.Error())
invalidSettings = true
}
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, append(txt, '\n'), 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,
"autosave": false,
"basename": false,
"colorcolumn": float64(0),
"colorscheme": "default",
"cursorline": true,
"eofnewline": false,
"fastdirty": true,
"fileformat": "unix",
"ignorecase": false,
"indentchar": " ",
"infobar": true,
"keepautoindent": false,
"keymenu": false,
"matchbrace": false,
"mouse": true,
"pluginchannels": []string{"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json"},
"pluginrepos": []string{},
"rmtrailingws": false,
"ruler": true,
"savecursor": false,
"savehistory": true,
"saveundo": false,
"scrollbar": false,
"scrollmargin": float64(3),
"scrollspeed": float64(2),
"softwrap": false,
"splitbottom": true,
"splitright": true,
"statusline": true,
"sucmd": "sudo",
"syntax": true,
"tabmovement": false,
"tabsize": float64(4),
"tabstospaces": false,
"termtitle": false,
"useprimary": true,
}
}
// 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,
"autosave": false,
"basename": false,
"colorcolumn": float64(0),
"cursorline": true,
"eofnewline": false,
"fastdirty": true,
"fileformat": "unix",
"filetype": "Unknown",
"ignorecase": false,
"indentchar": " ",
"keepautoindent": false,
"matchbrace": false,
"rmtrailingws": false,
"ruler": true,
"savecursor": false,
"saveundo": false,
"scrollbar": false,
"scrollmargin": float64(3),
"scrollspeed": float64(2),
"softwrap": false,
"splitbottom": true,
"splitright": true,
"statusline": true,
"syntax": true,
"tabmovement": false,
"tabsize": float64(4),
"tabstospaces": false,
"useprimary": true,
}
}
// 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
}
var nativeValue interface{}
kind := reflect.TypeOf(globalSettings[option]).Kind()
if kind == reflect.Bool {
b, err := ParseBool(value)
if err != nil {
return errors.New("Invalid value")
}
nativeValue = b
} else if kind == reflect.String {
nativeValue = value
} else if kind == reflect.Float64 {
i, err := strconv.Atoi(value)
if err != nil {
return errors.New("Invalid value")
}
nativeValue = float64(i)
} else {
return errors.New("Option has unsupported value type")
}
if err := optionIsValid(option, nativeValue); err != nil {
return err
}
globalSettings[option] = nativeValue
if option == "colorscheme" {
// LoadSyntaxFiles()
InitColorscheme()
for _, tab := range tabs {
for _, view := range tab.views {
view.Buf.UpdateRules()
}
}
}
if option == "infobar" || option == "keymenu" {
for _, tab := range tabs {
tab.Resize()
}
}
if option == "mouse" {
if !nativeValue.(bool) {
screen.DisableMouse()
} else {
screen.EnableMouse()
}
}
if len(tabs) != 0 {
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")
}
var nativeValue interface{}
kind := reflect.TypeOf(buf.Settings[option]).Kind()
if kind == reflect.Bool {
b, err := ParseBool(value)
if err != nil {
return errors.New("Invalid value")
}
nativeValue = b
} else if kind == reflect.String {
nativeValue = value
} else if kind == reflect.Float64 {
i, err := strconv.Atoi(value)
if err != nil {
return errors.New("Invalid value")
}
nativeValue = float64(i)
} else {
return errors.New("Option has unsupported value type")
}
if err := optionIsValid(option, nativeValue); err != nil {
return err
}
if option == "fastdirty" {
// If it is being turned off, we have to hash every open buffer
var empty [16]byte
for _, tab := range tabs {
for _, v := range tab.views {
if !nativeValue.(bool) {
if v.Buf.origHash == empty {
data, err := ioutil.ReadFile(v.Buf.AbsPath)
if err != nil {
data = []byte{}
}
v.Buf.origHash = md5.Sum(data)
}
} else {
v.Buf.IsModified = v.Buf.Modified()
}
}
}
}
buf.Settings[option] = nativeValue
if option == "statusline" {
view.ToggleStatusLine()
}
if option == "filetype" {
// LoadSyntaxFiles()
InitColorscheme()
buf.UpdateRules()
}
if option == "fileformat" {
buf.IsModified = true
}
if option == "syntax" {
if !nativeValue.(bool) {
buf.ClearMatches()
} else {
buf.highlighter.HighlightStates(buf)
}
}
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
}
}
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
}

View File

@@ -1,18 +0,0 @@
// +build linux darwin dragonfly openbsd_amd64 freebsd
package main
import (
"github.com/zyedidia/micro/cmd/micro/shellwords"
)
const TermEmuSupported = true
func RunTermEmulator(input string, wait bool, getOutput bool, callback string) error {
args, err := shellwords.Split(input)
if err != nil {
return err
}
err = CurView().StartTerminal(args, wait, getOutput, callback)
return err
}

View File

@@ -1,229 +0,0 @@
package shellwords
import (
"os"
"reflect"
"testing"
)
var testcases = []struct {
line string
expected []string
}{
{`var --bar=baz`, []string{`var`, `--bar=baz`}},
{`var --bar="baz"`, []string{`var`, `--bar=baz`}},
{`var "--bar=baz"`, []string{`var`, `--bar=baz`}},
{`var "--bar='baz'"`, []string{`var`, `--bar='baz'`}},
{"var --bar=`baz`", []string{`var`, "--bar=`baz`"}},
{`var "--bar=\"baz'"`, []string{`var`, `--bar="baz'`}},
{`var "--bar=\'baz\'"`, []string{`var`, `--bar='baz'`}},
{`var --bar='\'`, []string{`var`, `--bar=\`}},
{`var "--bar baz"`, []string{`var`, `--bar baz`}},
{`var --"bar baz"`, []string{`var`, `--bar baz`}},
{`var --"bar baz"`, []string{`var`, `--bar baz`}},
}
func TestSimple(t *testing.T) {
for _, testcase := range testcases {
args, err := Parse(testcase.line)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(args, testcase.expected) {
t.Fatalf("Expected %#v, but %#v:", testcase.expected, args)
}
}
}
func TestError(t *testing.T) {
_, err := Parse("foo '")
if err == nil {
t.Fatal("Should be an error")
}
_, err = Parse(`foo "`)
if err == nil {
t.Fatal("Should be an error")
}
_, err = Parse("foo `")
if err == nil {
t.Fatal("Should be an error")
}
}
func TestLastSpace(t *testing.T) {
args, err := Parse("foo bar\\ ")
if err != nil {
t.Fatal(err)
}
if len(args) != 2 {
t.Fatal("Should have two elements")
}
if args[0] != "foo" {
t.Fatal("1st element should be `foo`")
}
if args[1] != "bar " {
t.Fatal("1st element should be `bar `")
}
}
func TestBacktick(t *testing.T) {
goversion, err := shellRun("go version")
if err != nil {
t.Fatal(err)
}
parser := NewParser()
parser.ParseBacktick = true
args, err := parser.Parse("echo `go version`")
if err != nil {
t.Fatal(err)
}
expected := []string{"echo", goversion}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
args, err = parser.Parse(`echo $(echo foo)`)
if err != nil {
t.Fatal(err)
}
expected = []string{"echo", "foo"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
parser.ParseBacktick = false
args, err = parser.Parse(`echo $(echo "foo")`)
expected = []string{"echo", `$(echo "foo")`}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
args, err = parser.Parse("echo $(`echo1)")
if err != nil {
t.Fatal(err)
}
expected = []string{"echo", "$(`echo1)"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
}
func TestBacktickError(t *testing.T) {
parser := NewParser()
parser.ParseBacktick = true
_, err := parser.Parse("echo `go Version`")
if err == nil {
t.Fatal("Should be an error")
}
expected := "exit status 2:go: unknown subcommand \"Version\"\nRun 'go help' for usage.\n"
if expected != err.Error() {
t.Fatalf("Expected %q, but %q", expected, err.Error())
}
_, err = parser.Parse(`echo $(echo1)`)
if err == nil {
t.Fatal("Should be an error")
}
_, err = parser.Parse(`echo $(echo1`)
if err == nil {
t.Fatal("Should be an error")
}
_, err = parser.Parse(`echo $ (echo1`)
if err == nil {
t.Fatal("Should be an error")
}
_, err = parser.Parse(`echo (echo1`)
if err == nil {
t.Fatal("Should be an error")
}
_, err = parser.Parse(`echo )echo1`)
if err == nil {
t.Fatal("Should be an error")
}
}
func TestEnv(t *testing.T) {
os.Setenv("FOO", "bar")
parser := NewParser()
parser.ParseEnv = true
args, err := parser.Parse("echo $FOO")
if err != nil {
t.Fatal(err)
}
expected := []string{"echo", "bar"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
}
func TestNoEnv(t *testing.T) {
parser := NewParser()
parser.ParseEnv = true
args, err := parser.Parse("echo $BAR")
if err != nil {
t.Fatal(err)
}
expected := []string{"echo", ""}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
}
func TestDupEnv(t *testing.T) {
os.Setenv("FOO", "bar")
os.Setenv("FOO_BAR", "baz")
parser := NewParser()
parser.ParseEnv = true
args, err := parser.Parse("echo $$FOO$")
if err != nil {
t.Fatal(err)
}
expected := []string{"echo", "$bar$"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
args, err = parser.Parse("echo $${FOO_BAR}$")
if err != nil {
t.Fatal(err)
}
expected = []string{"echo", "$baz$"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
}
func TestHaveMore(t *testing.T) {
parser := NewParser()
parser.ParseEnv = true
line := "echo foo; seq 1 10"
args, err := parser.Parse(line)
if err != nil {
t.Fatalf(err.Error())
}
expected := []string{"echo", "foo"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
if parser.Position == 0 {
t.Fatalf("Commands should be remaining")
}
line = string([]rune(line)[parser.Position+1:])
args, err = parser.Parse(line)
if err != nil {
t.Fatalf(err.Error())
}
expected = []string{"seq", "1", "10"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %#v, but %#v:", expected, args)
}
if parser.Position > 0 {
t.Fatalf("Commands should not be remaining")
}
}

View File

@@ -1,317 +0,0 @@
package main
// SplitType specifies whether a split is horizontal or vertical
type SplitType bool
const (
// VerticalSplit type
VerticalSplit = false
// HorizontalSplit type
HorizontalSplit = true
)
// A Node on the split tree
type Node interface {
VSplit(buf *Buffer, splitIndex int)
HSplit(buf *Buffer, splitIndex int)
String() string
}
// A LeafNode is an actual split so it contains a view
type LeafNode struct {
view *View
parent *SplitTree
}
// NewLeafNode returns a new leaf node containing the given view
func NewLeafNode(v *View, parent *SplitTree) *LeafNode {
n := new(LeafNode)
n.view = v
n.view.splitNode = n
n.parent = parent
return n
}
// A SplitTree is a Node itself and it contains other nodes
type SplitTree struct {
kind SplitType
parent *SplitTree
children []Node
x int
y int
width int
height int
lockWidth bool
lockHeight bool
tabNum int
}
// VSplit creates a vertical split
func (l *LeafNode) VSplit(buf *Buffer, splitIndex int) {
if splitIndex < 0 {
splitIndex = 0
}
tab := tabs[l.parent.tabNum]
if l.parent.kind == VerticalSplit {
if splitIndex > len(l.parent.children) {
splitIndex = len(l.parent.children)
}
newView := NewView(buf)
newView.TabNum = l.parent.tabNum
l.parent.children = append(l.parent.children, nil)
copy(l.parent.children[splitIndex+1:], l.parent.children[splitIndex:])
l.parent.children[splitIndex] = NewLeafNode(newView, l.parent)
tab.views = append(tab.views, nil)
copy(tab.views[splitIndex+1:], tab.views[splitIndex:])
tab.views[splitIndex] = newView
tab.CurView = splitIndex
} else {
if splitIndex > 1 {
splitIndex = 1
}
s := new(SplitTree)
s.kind = VerticalSplit
s.parent = l.parent
s.tabNum = l.parent.tabNum
newView := NewView(buf)
newView.TabNum = l.parent.tabNum
if splitIndex == 1 {
s.children = []Node{l, NewLeafNode(newView, s)}
} else {
s.children = []Node{NewLeafNode(newView, s), l}
}
l.parent.children[search(l.parent.children, l)] = s
l.parent = s
tab.views = append(tab.views, nil)
copy(tab.views[splitIndex+1:], tab.views[splitIndex:])
tab.views[splitIndex] = newView
tab.CurView = splitIndex
}
tab.Resize()
}
// HSplit creates a horizontal split
func (l *LeafNode) HSplit(buf *Buffer, splitIndex int) {
if splitIndex < 0 {
splitIndex = 0
}
tab := tabs[l.parent.tabNum]
if l.parent.kind == HorizontalSplit {
if splitIndex > len(l.parent.children) {
splitIndex = len(l.parent.children)
}
newView := NewView(buf)
newView.TabNum = l.parent.tabNum
l.parent.children = append(l.parent.children, nil)
copy(l.parent.children[splitIndex+1:], l.parent.children[splitIndex:])
l.parent.children[splitIndex] = NewLeafNode(newView, l.parent)
tab.views = append(tab.views, nil)
copy(tab.views[splitIndex+1:], tab.views[splitIndex:])
tab.views[splitIndex] = newView
tab.CurView = splitIndex
} else {
if splitIndex > 1 {
splitIndex = 1
}
s := new(SplitTree)
s.kind = HorizontalSplit
s.tabNum = l.parent.tabNum
s.parent = l.parent
newView := NewView(buf)
newView.TabNum = l.parent.tabNum
newView.Num = len(tab.views)
if splitIndex == 1 {
s.children = []Node{l, NewLeafNode(newView, s)}
} else {
s.children = []Node{NewLeafNode(newView, s), l}
}
l.parent.children[search(l.parent.children, l)] = s
l.parent = s
tab.views = append(tab.views, nil)
copy(tab.views[splitIndex+1:], tab.views[splitIndex:])
tab.views[splitIndex] = newView
tab.CurView = splitIndex
}
tab.Resize()
}
// Delete deletes a split
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--
}
}
// Cleanup rearranges all the parents after a split has been deleted
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()
}
}
}
// ResizeSplits resizes all the splits correctly
func (s *SplitTree) ResizeSplits() {
lockedWidth := 0
lockedHeight := 0
lockedChildren := 0
for _, node := range s.children {
if n, ok := node.(*LeafNode); ok {
if s.kind == VerticalSplit {
if n.view.LockWidth {
lockedWidth += n.view.Width
lockedChildren++
}
} else {
if n.view.LockHeight {
lockedHeight += n.view.Height
lockedChildren++
}
}
} else if n, ok := node.(*SplitTree); ok {
if s.kind == VerticalSplit {
if n.lockWidth {
lockedWidth += n.width
lockedChildren++
}
} else {
if n.lockHeight {
lockedHeight += n.height
lockedChildren++
}
}
}
}
x, y := 0, 0
for _, node := range s.children {
if n, ok := node.(*LeafNode); ok {
if s.kind == VerticalSplit {
if !n.view.LockWidth {
n.view.Width = (s.width - lockedWidth) / (len(s.children) - lockedChildren)
}
n.view.Height = s.height
n.view.x = s.x + x
n.view.y = s.y
x += n.view.Width
} else {
if !n.view.LockHeight {
n.view.Height = (s.height - lockedHeight) / (len(s.children) - lockedChildren)
}
n.view.Width = s.width
n.view.y = s.y + y
n.view.x = s.x
y += n.view.Height
}
if n.view.Buf.Settings["statusline"].(bool) {
n.view.Height--
}
n.view.ToggleTabbar()
} else if n, ok := node.(*SplitTree); ok {
if s.kind == VerticalSplit {
if !n.lockWidth {
n.width = (s.width - lockedWidth) / (len(s.children) - lockedChildren)
}
n.height = s.height
n.x = s.x + x
n.y = s.y
x += n.width
} else {
if !n.lockHeight {
n.height = (s.height - lockedHeight) / (len(s.children) - lockedChildren)
}
n.width = s.width
n.y = s.y + y
n.x = s.x
y += n.height
}
n.ResizeSplits()
}
}
}
func (l *LeafNode) String() string {
return l.view.Buf.GetName()
}
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
}
// VSplit is here just to make SplitTree fit the Node interface
func (s *SplitTree) VSplit(buf *Buffer, splitIndex int) {}
// HSplit is here just to make SplitTree fit the Node interface
func (s *SplitTree) HSplit(buf *Buffer, splitIndex int) {}
func (s *SplitTree) String() string {
str := "["
for _, child := range s.children {
str += child.String() + ", "
}
return str + "]"
}

View File

@@ -1,96 +0,0 @@
package main
import (
"path"
"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() {
if messenger.hasPrompt && !GetGlobalOption("infobar").(bool) {
return
}
// We'll draw the line at the lowest line in the view
y := sline.view.Height + sline.view.y
file := sline.view.Buf.GetName()
if sline.view.Buf.Settings["basename"].(bool) {
file = path.Base(file)
}
// If the buffer is dirty (has been modified) write a little '+'
if sline.view.Buf.Modified() {
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()
file += " " + sline.view.Buf.Settings["fileformat"].(string)
rightText := ""
if len(kmenuBinding) > 0 {
if globalSettings["keymenu"].(bool) {
rightText += kmenuBinding + ": hide bindings"
} else {
rightText += kmenuBinding + ": show bindings"
}
}
if len(helpBinding) > 0 {
if len(kmenuBinding) > 0 {
rightText += ", "
}
if sline.view.Type == vtHelp {
rightText += helpBinding + ": close help"
} else {
rightText += helpBinding + ": open help"
}
}
rightText += " "
statusLineStyle := defStyle.Reverse(true)
if style, ok := colorscheme["statusline"]; ok {
statusLineStyle = style
}
// Maybe there is a unicode filename?
fileRunes := []rune(file)
if sline.view.Type == vtTerm {
fileRunes = []rune(sline.view.term.title)
rightText = ""
}
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)
}
}
}

View File

@@ -1,279 +0,0 @@
package main
import (
"sort"
"github.com/zyedidia/tcell"
)
var tabBarOffset int
// A Tab holds an array of views and a splitTree to determine how the
// views should be arranged
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
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
if globalSettings["infobar"].(bool) {
t.tree.height--
}
if globalSettings["keymenu"].(bool) {
t.tree.height -= 2
}
t.Resize()
return t
}
// SetNum sets all this tab's views to have the correct tab number
func (t *Tab) SetNum(num int) {
t.tree.tabNum = num
for _, v := range t.views {
v.TabNum = num
}
}
// Cleanup cleans up the tree (for example if views have closed)
func (t *Tab) Cleanup() {
t.tree.Cleanup()
}
// Resize handles a resize event from the terminal and resizes
// all child views correctly
func (t *Tab) Resize() {
w, h := screen.Size()
t.tree.width = w
t.tree.height = h
if globalSettings["infobar"].(bool) {
t.tree.height--
}
if globalSettings["keymenu"].(bool) {
t.tree.height -= 2
}
t.tree.ResizeSplits()
for i, v := range t.views {
v.Num = i
if v.Type == vtTerm {
v.term.Resize(v.Width, v.Height)
}
}
}
// 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 += " "
}
buf := t.views[t.CurView].Buf
str += buf.GetName()
if buf.Modified() {
str += " +"
}
if i == curTab {
str += "]"
} else {
str += " "
}
indicies[Count(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+tabBarOffset >= 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+tabBarOffset <= 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, indicies := 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()
tooWide := (w < len(fileRunes))
// if the entire tab-bar is longer than the screen is wide,
// then it should be truncated appropriately to keep the
// active tab visible on the UI.
if tooWide == true {
// first we have to work out where the selected tab is
// out of the total length of the tab bar. this is done
// by extracting the hit-areas from the indicies map
// that was constructed by `TabbarString()`
var keys []int
for offset := range indicies {
keys = append(keys, offset)
}
// sort them to be in ascending order so that values will
// correctly reflect the displayed ordering of the tabs
sort.Ints(keys)
// record the offset of each tab and the previous tab so
// we can find the position of the tab's hit-box.
previousTabOffset := 0
currentTabOffset := 0
for _, k := range keys {
tabIndex := indicies[k] - 1
if tabIndex == curTab {
currentTabOffset = k
break
}
// this is +2 because there are two padding spaces that aren't accounted
// for in the display. please note that this is for cosmetic purposes only.
previousTabOffset = k + 2
}
// get the width of the hitbox of the active tab, from there calculate the offsets
// to the left and right of it to approximately center it on the tab bar display.
centeringOffset := (w - (currentTabOffset - previousTabOffset))
leftBuffer := previousTabOffset - (centeringOffset / 2)
rightBuffer := currentTabOffset + (centeringOffset / 2)
// check to make sure we haven't overshot the bounds of the string,
// if we have, then take that remainder and put it on the left side
overshotRight := rightBuffer - len(fileRunes)
if overshotRight > 0 {
leftBuffer = leftBuffer + overshotRight
}
overshotLeft := leftBuffer - 0
if overshotLeft < 0 {
leftBuffer = 0
rightBuffer = leftBuffer + (w - 1)
} else {
rightBuffer = leftBuffer + (w - 2)
}
if rightBuffer > len(fileRunes)-1 {
rightBuffer = len(fileRunes) - 1
}
// construct a new buffer of text to put the
// newly formatted tab bar text into.
var displayText []rune
// if the left-side of the tab bar isn't at the start
// of the constructed tab bar text, then show that are
// more tabs to the left by displaying a "+"
if leftBuffer != 0 {
displayText = append(displayText, '+')
}
// copy the runes in from the original tab bar text string
// into the new display buffer
for x := leftBuffer; x < rightBuffer; x++ {
displayText = append(displayText, fileRunes[x])
}
// if there is more text to the right of the right-most
// column in the tab bar text, then indicate there are more
// tabs to the right by displaying a "+"
if rightBuffer < len(fileRunes)-1 {
displayText = append(displayText, '+')
}
// now store the offset from zero of the left-most text
// that is being displayed. This is to ensure that when
// clicking on the tab bar, the correct tab gets selected.
tabBarOffset = leftBuffer
// use the constructed buffer as the display buffer to print
// onscreen.
fileRunes = displayText
} else {
tabBarOffset = 0
}
// iterate over the width of the terminal display and for each column,
// write a character into the tab display area with the appropriate style.
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)
}
}
}

View File

@@ -1,228 +0,0 @@
package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"github.com/zyedidia/clipboard"
"github.com/zyedidia/tcell"
"github.com/zyedidia/terminal"
)
const (
VTIdle = iota // Waiting for a new command
VTRunning // Currently running a command
VTDone // Finished running a command
)
// A Terminal holds information for the terminal emulator
type Terminal struct {
state terminal.State
view *View
vtOld ViewType
term *terminal.VT
title string
status int
selection [2]Loc
wait bool
getOutput bool
output *bytes.Buffer
callback string
}
// HasSelection returns whether this terminal has a valid selection
func (t *Terminal) HasSelection() bool {
return t.selection[0] != t.selection[1]
}
// GetSelection returns the selected text
func (t *Terminal) GetSelection(width int) string {
start := t.selection[0]
end := t.selection[1]
if start.GreaterThan(end) {
start, end = end, start
}
var ret string
var l Loc
for y := start.Y; y <= end.Y; y++ {
for x := 0; x < width; x++ {
l.X, l.Y = x, y
if l.GreaterEqual(start) && l.LessThan(end) {
c, _, _ := t.state.Cell(x, y)
ret += string(c)
}
}
}
return ret
}
// Start begins a new command in this terminal with a given view
func (t *Terminal) Start(execCmd []string, view *View, getOutput bool) error {
if len(execCmd) <= 0 {
return nil
}
cmd := exec.Command(execCmd[0], execCmd[1:]...)
t.output = nil
if getOutput {
t.output = bytes.NewBuffer([]byte{})
}
term, _, err := terminal.Start(&t.state, cmd, t.output)
if err != nil {
return err
}
t.term = term
t.view = view
t.getOutput = getOutput
t.vtOld = view.Type
t.status = VTRunning
t.title = execCmd[0] + ":" + strconv.Itoa(cmd.Process.Pid)
go func() {
for {
err := term.Parse()
if err != nil {
fmt.Fprintln(os.Stderr, "[Press enter to close]")
break
}
updateterm <- true
}
closeterm <- view.Num
}()
return nil
}
// Resize informs the terminal of a resize event
func (t *Terminal) Resize(width, height int) {
t.term.Resize(width, height)
}
// 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 *Terminal) HandleEvent(event tcell.Event) {
if e, ok := event.(*tcell.EventKey); ok {
if t.status == VTDone {
switch e.Key() {
case tcell.KeyEscape, tcell.KeyCtrlQ, tcell.KeyEnter:
t.Close()
t.view.Type = vtDefault
default:
}
}
if e.Key() == tcell.KeyCtrlC && t.HasSelection() {
clipboard.WriteAll(t.GetSelection(t.view.Width), "clipboard")
messenger.Message("Copied selection to clipboard")
} else if t.status != VTDone {
t.WriteString(event.EscSeq())
}
} else if e, ok := event.(*tcell.EventMouse); !ok || t.state.Mode(terminal.ModeMouseMask) {
t.WriteString(event.EscSeq())
} else {
x, y := e.Position()
x -= t.view.x
y += t.view.y
if e.Buttons() == tcell.Button1 {
if !t.view.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.view.mouseReleased = false
} else if e.Buttons() == tcell.ButtonNone {
if !t.view.mouseReleased {
t.selection[1].X = x
t.selection[1].Y = y
}
t.view.mouseReleased = true
}
}
}
// Stop stops execution of the terminal and sets the status
// to VTDone
func (t *Terminal) Stop() {
t.term.File().Close()
t.term.Close()
if t.wait {
t.status = VTDone
} else {
t.Close()
t.view.Type = t.vtOld
}
}
// Close sets the status to VTIdle indicating that the terminal
// is ready for a new command to execute
func (t *Terminal) Close() {
t.status = VTIdle
// call the lua function that the user has given as a callback
if t.getOutput {
_, err := Call(t.callback, t.output.String())
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
}
}
}
// WriteString writes a given string to this terminal's pty
func (t *Terminal) WriteString(str string) {
t.term.File().WriteString(str)
}
// Display displays this terminal in a view
func (t *Terminal) Display() {
divider := 0
if t.view.x != 0 {
divider = 1
dividerStyle := defStyle
if style, ok := colorscheme["divider"]; ok {
dividerStyle = style
}
for i := 0; i < t.view.Height; i++ {
screen.SetContent(t.view.x, t.view.y+i, '|', nil, dividerStyle.Reverse(true))
}
}
t.state.Lock()
defer t.state.Unlock()
var l Loc
for y := 0; y < t.view.Height; y++ {
for x := 0; x < t.view.Width; x++ {
l.X, l.Y = x, y
c, f, b := t.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(GetColor256(int(fg))).Background(GetColor256(int(bg)))
if l.LessThan(t.selection[1]) && l.GreaterEqual(t.selection[0]) || l.LessThan(t.selection[0]) && l.GreaterEqual(t.selection[1]) {
st = st.Reverse(true)
}
screen.SetContent(t.view.x+x+divider, t.view.y+y, c, nil, st)
}
}
if t.state.CursorVisible() && tabs[curTab].CurView == t.view.Num {
curx, cury := t.state.Cursor()
screen.ShowCursor(curx+t.view.x+divider, cury+t.view.y)
}
}

View File

@@ -1,291 +0,0 @@
package main
import (
"os"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/mattn/go-runewidth"
homedir "github.com/mitchellh/go-homedir"
)
// 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 occurrences 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 {
return strings.Repeat(" ", n)
}
// 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
}
// FSize gets the size of a file
func FSize(f *os.File) int64 {
fi, _ := f.Stat()
// get the size
return fi.Size()
}
// 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 true
}
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'
}
// IsStrWhitespace returns true if the given string is all whitespace
func IsStrWhitespace(str string) bool {
for _, c := range str {
if !IsWhitespace(c) {
return false
}
}
return true
}
// 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:])
}
// MakeRelative will attempt to make a relative path between path and base
func MakeRelative(path, base string) (string, error) {
if len(path) > 0 {
rel, err := filepath.Rel(base, path)
if err != nil {
return path, err
}
return rel, nil
}
return path, nil
}
// 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)
lineIdx := 0
for _, ch := range str {
switch ch {
case '\t':
ts := tabsize - (lineIdx % tabsize)
sw += ts
lineIdx += ts
case '\n':
lineIdx = 0
default:
lineIdx++
}
}
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
lineIdx := 0
for _, ch := range str {
var w int
if ch == '\t' {
w = tabsize - (lineIdx % tabsize)
} else {
w = runewidth.RuneWidth(ch)
}
if w > 1 {
count += (w - 1)
}
if ch == '\n' {
lineIdx = 0
} else {
lineIdx += w
}
}
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
}
// CommonSubstring gets a common substring among the inputs
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
}
// FuncName returns the full name of a given function object
func FuncName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
// ShortFuncName returns the name only of a given function object
func ShortFuncName(i interface{}) string {
return strings.TrimPrefix(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name(), "main.(*View).")
}
// ReplaceHome takes a path as input and replaces ~ at the start of the path with the user's
// home directory. Does nothing if the path does not start with '~'.
func ReplaceHome(path string) string {
if !strings.HasPrefix(path, "~") {
return path
}
home, err := homedir.Dir()
if err != nil {
messenger.Error("Could not find home directory: ", err)
return path
}
return strings.Replace(path, "~", home, 1)
}

View File

@@ -1,105 +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("ß") == 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("\n") == true {
t.Errorf("IsWordChar(\n)) = true")
}
}
func TestStringWidth(t *testing.T) {
tabsize := 4
if w := StringWidth("1\t2", tabsize); w != 5 {
t.Error("StringWidth 1 Failed. Got", w)
}
if w := StringWidth("\t", tabsize); w != 4 {
t.Error("StringWidth 2 Failed. Got", w)
}
if w := StringWidth("1\t", tabsize); w != 4 {
t.Error("StringWidth 3 Failed. Got", w)
}
if w := StringWidth("\t\t", tabsize); w != 8 {
t.Error("StringWidth 4 Failed. Got", w)
}
if w := StringWidth("12\t2\t", tabsize); w != 8 {
t.Error("StringWidth 5 Failed. Got", w)
}
}
func TestWidthOfLargeRunes(t *testing.T) {
tabsize := 4
if w := WidthOfLargeRunes("1\t2", tabsize); w != 2 {
t.Error("WidthOfLargeRunes 1 Failed. Got", w)
}
if w := WidthOfLargeRunes("\t", tabsize); w != 3 {
t.Error("WidthOfLargeRunes 2 Failed. Got", w)
}
if w := WidthOfLargeRunes("1\t", tabsize); w != 2 {
t.Error("WidthOfLargeRunes 3 Failed. Got", w)
}
if w := WidthOfLargeRunes("\t\t", tabsize); w != 6 {
t.Error("WidthOfLargeRunes 4 Failed. Got", w)
}
if w := WidthOfLargeRunes("12\t2\t", tabsize); w != 3 {
t.Error("WidthOfLargeRunes 5 Failed. Got", w)
}
}

File diff suppressed because it is too large Load Diff

38
go.mod Normal file
View File

@@ -0,0 +1,38 @@
module github.com/zyedidia/micro
require (
github.com/blang/semver v3.5.1+incompatible
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.0
github.com/flynn/json5 v0.0.0-20160717195620-7620272ed633
github.com/gdamore/encoding v1.0.0 // indirect
github.com/go-errors/errors v1.0.1
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 // indirect
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 // indirect
github.com/mattn/go-isatty v0.0.4
github.com/mattn/go-runewidth v0.0.4
github.com/mitchellh/go-homedir v1.1.0
github.com/npat-efault/poller v2.0.0+incompatible // indirect
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d // indirect
github.com/sergi/go-diff v1.0.0
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
github.com/stretchr/testify v1.3.0
github.com/yuin/gopher-lua v0.0.0-20190125051437-7b9317363aa9
github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3
github.com/zyedidia/poller v2.0.0+incompatible // indirect
github.com/zyedidia/pty v1.1.2-0.20180126010845-30364665a244 // indirect
github.com/zyedidia/tcell v0.0.0-20191219170756-59b50b23fa9b
github.com/zyedidia/terminal v0.0.0-20180726154117-533c623e2415
golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9 // indirect
golang.org/x/text v0.3.0
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v2 v2.2.2
layeh.com/gopher-luar v1.0.4
)

79
go.sum Normal file
View File

@@ -0,0 +1,79 @@
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/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
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/flynn/json5 v0.0.0-20160717195620-7620272ed633 h1:xJMmr4GMYIbALX5edyoDIOQpc2bOQTeJiWMeCl9lX/8=
github.com/flynn/json5 v0.0.0-20160717195620-7620272ed633/go.mod h1:NJDK3/o7abx6PP54EOe0G0n0RLmhCo9xv61gUYpI0EY=
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/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
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/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08 h1:5MnxBC15uMxFv5FY/J/8vzyaBiArCOkMdFT9Jsw78iY=
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
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/npat-efault/poller v2.0.0+incompatible h1:jtTdXWKgN5kDK41ts8hoY1rvTEi0K08MTB8/bRO9MqE=
github.com/npat-efault/poller v2.0.0+incompatible/go.mod h1:lni01B89P8PtVpwlAhdhK1niN5rPkDGGpGGgBJzpSgo=
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-20180617131154-15f95af6e78d h1:1VUlQbCfkoSGv7qP7Y+ro3ap1P1pPZxgdGVqiTVy5C4=
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY=
github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/yuin/gopher-lua v0.0.0-20190125051437-7b9317363aa9 h1:Wy3fAQLBPP0JSWdq3kBnmbFgXDHcyhtPpd+8kENV7mU=
github.com/yuin/gopher-lua v0.0.0-20190125051437-7b9317363aa9/go.mod h1:fFiAh+CowNFr0NK5VASokuwKwkbacRmHsVA7Yb1Tqac=
github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d h1:Lhqt2eo+rgM8aswvM7nTtAMVm8ARPWzkE9n6eZDOccY=
github.com/zyedidia/clipboard v0.0.0-20180718195219-bd31d747117d/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/poller v2.0.0+incompatible h1:DMOvB0EXz2JTokqOKfxPWN/8xXFJbO+m4vNhMkOY7Lo=
github.com/zyedidia/poller v2.0.0+incompatible/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE=
github.com/zyedidia/pty v1.1.2-0.20180126010845-30364665a244 h1:DZ7mZvUV5+oXeXV1E1t6ZIXRihHYyqYVIOSA+RGo88A=
github.com/zyedidia/pty v1.1.2-0.20180126010845-30364665a244/go.mod h1:4y9l9yJZNxRa7GB/fB+mmDmGkG3CqmzLf4vUxGGotEA=
github.com/zyedidia/tcell v0.0.0-20190204041104-518c15c24302 h1:ruNSURcO81y+J+XnqrLLt+zxcdFtq8QNoZfWXSsybYQ=
github.com/zyedidia/tcell v0.0.0-20190204041104-518c15c24302/go.mod h1:yXgdp23+aW8OMENYVBvpKoeiBtjaVWJ9HhpPDu6LBfM=
github.com/zyedidia/tcell v0.0.0-20191219170756-59b50b23fa9b h1:cryFENlMxJJrkimVx/CUMFDCxC4vpmey2x3A3tAgTNM=
github.com/zyedidia/tcell v0.0.0-20191219170756-59b50b23fa9b/go.mod h1:yXgdp23+aW8OMENYVBvpKoeiBtjaVWJ9HhpPDu6LBfM=
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-20180903190138-2b024373dcd9 h1:lkiLiLBHGoH3XnqSLUIaBsilGMUjI+Uy2Xu2JLUtTas=
golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0 h1:FVCohIoYO7IJoDDVpV2pdq7SgrMH6wHnuTyrdrxJNoY=
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
layeh.com/gopher-luar v1.0.4 h1:BFgt94J/CXh4HkDcE2b7A7pBaVeQKEVfHEBRKL/K/Tc=
layeh.com/gopher-luar v1.0.4/go.mod h1:N3rev/ttQd8yVluXaYsa0M/eknzRYWe+pxZ35ZFmaaI=

1540
internal/action/actions.go Normal file

File diff suppressed because it is too large Load Diff

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

View File

@@ -1,37 +1,27 @@
// +build linux darwin dragonfly solaris openbsd netbsd freebsd
package main
package action
import "syscall"
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 (v *View) Suspend(usePlugin bool) bool {
if usePlugin && !PreActionCall("Suspend", v) {
return false
}
screenWasNil := screen == nil
if !screenWasNil {
screen.Fini()
screen = nil
}
func (*BufPane) Suspend() bool {
screenb := screen.TempFini()
// suspend the process
pid := syscall.Getpid()
err := syscall.Kill(pid, syscall.SIGSTOP)
if err != nil {
TermMessage(err)
screen.TermMessage(err)
}
if !screenWasNil {
InitScreen()
}
screen.TempStart(screenb)
if usePlugin {
return PostActionCall("Suspend", v)
}
return true
return false
}

493
internal/action/bindings.go Normal file
View File

@@ -0,0 +1,493 @@
package action
import (
"encoding/json"
"errors"
"io/ioutil"
"os"
"strings"
"unicode"
"github.com/flynn/json5"
"github.com/zyedidia/micro/internal/config"
"github.com/zyedidia/micro/internal/screen"
"github.com/zyedidia/tcell"
)
func InitBindings() {
config.Bindings = DefaultBindings()
var parsed map[string]string
defaults := DefaultBindings()
filename := config.ConfigDir + "/bindings.json"
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"):
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 := config.ConfigDir + "/bindings.json"
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 := config.ConfigDir + "/bindings.json"
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,
"CtrlPageUp": tcell.KeyCtrlPgUp,
"CtrlPageDown": tcell.KeyCtrlPgDn,
"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,
}
// 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": "StartOfLine",
"CtrlRight": "EndOfLine",
"CtrlShiftLeft": "SelectToStartOfLine",
"ShiftHome": "SelectToStartOfLine",
"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": "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": "StartOfLine",
"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": "StartOfLine",
"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",
"Alt-p": "RemoveMultiCursor",
"Alt-c": "RemoveAllMultiCursors",
"Alt-x": "SkipMultiCursor",
}
}

601
internal/action/bufpane.go Normal file
View File

@@ -0,0 +1,601 @@
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
}
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(action, "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:", action, "does not exist")
continue
}
names = append(names, "")
} else if f, ok := BufKeyActions[a]; ok {
afn = f
names = append(names, a)
} else {
screen.TermMessage("Error:", action, "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 {
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
// remember original location of a search in case the search is canceled
searchOrig buffer.Loc
}
func NewBufPane(buf *buffer.Buffer, win display.BWindow) *BufPane {
h := new(BufPane)
h.Buf = buf
h.BWindow = win
h.Cursor = h.Buf.GetActiveCursor()
h.mouseReleased = true
config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
return h
}
func NewBufPaneFromBuf(buf *buffer.Buffer) *BufPane {
w := display.NewBufWindow(0, 0, 0, 0, buf)
return NewBufPane(buf, w)
}
// 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)
v := new(display.View)
h.SetView(v)
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 {
return h.Buf.GetName()
}
// HandleEvent executes the tcell event properly
func (h *BufPane) HandleEvent(event tcell.Event) {
switch e := event.(type) {
case *tcell.EventRaw:
re := RawEvent{
esc: e.EscSeq(),
}
h.DoKeyEvent(re)
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:
switch e.Buttons() {
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})
// 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.Loc = mouseLoc
h.Cursor.SetSelectionEnd(h.Cursor.Loc)
h.Cursor.CopySelection("primary")
}
h.mouseReleased = true
}
}
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" {
h.Buf.HasSuggestions = false
}
_, isMulti := MultiActions[name]
if (!isMulti && cursor == 0) || isMulti {
if h.PluginCB("pre" + name) {
asuccess := action(h)
psuccess := h.PluginCB("on" + name)
if isMulti {
if recording_macro {
if name != "ToggleMacro" && name != "PlayMacro" {
curmacro = append(curmacro, action)
}
}
}
return asuccess && psuccess
}
}
return false
}
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.PluginCBRune("onRune", r)
}
}
func (h *BufPane) VSplitBuf(buf *buffer.Buffer) *BufPane {
e := NewBufPaneFromBuf(buf)
e.splitID = MainTab().GetNode(h.splitID).VSplit(h.Buf.Settings["splitright"].(bool))
MainTab().Panes = append(MainTab().Panes, e)
MainTab().Resize()
MainTab().SetActive(len(MainTab().Panes) - 1)
return e
}
func (h *BufPane) HSplitBuf(buf *buffer.Buffer) *BufPane {
e := NewBufPaneFromBuf(buf)
e.splitID = MainTab().GetNode(h.splitID).HSplit(h.Buf.Settings["splitbottom"].(bool))
MainTab().Panes = append(MainTab().Panes, e)
MainTab().Resize()
MainTab().SetActive(len(MainTab().Panes) - 1)
return e
}
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,
"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,
"OutdentLine": (*BufPane).OutdentLine,
"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,
"StartOfLine": (*BufPane).StartOfLine,
"EndOfLine": (*BufPane).EndOfLine,
"ToggleHelp": (*BufPane).ToggleHelp,
"ToggleKeyMenu": (*BufPane).ToggleKeyMenu,
"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,
"SpawnMultiCursorSelect": (*BufPane).SpawnMultiCursorSelect,
"RemoveMultiCursor": (*BufPane).RemoveMultiCursor,
"RemoveAllMultiCursors": (*BufPane).RemoveAllMultiCursors,
"SkipMultiCursor": (*BufPane).SkipMultiCursor,
"JumpToMatchingBrace": (*BufPane).JumpToMatchingBrace,
"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,
"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,
"Paste": true,
"PastePrimary": true,
"SelectPageUp": true,
"SelectPageDown": true,
"StartOfLine": true,
"EndOfLine": true,
"JumpToMatchingBrace": true,
}

967
internal/action/command.go Normal file
View File

@@ -0,0 +1,967 @@
package action
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"unicode/utf8"
luar "layeh.com/gopher-luar"
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/shell"
"github.com/zyedidia/micro/internal/util"
"github.com/zyedidia/micro/pkg/shellwords"
)
// 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": Command{(*BufPane).SetCmd, OptionValueComplete},
"reset": Command{(*BufPane).ResetCmd, OptionValueComplete},
"setlocal": Command{(*BufPane).SetLocalCmd, OptionValueComplete},
"show": Command{(*BufPane).ShowCmd, OptionComplete},
"showkey": Command{(*BufPane).ShowKeyCmd, nil},
"run": Command{(*BufPane).RunCmd, nil},
"bind": Command{(*BufPane).BindCmd, nil},
"unbind": Command{(*BufPane).UnbindCmd, nil},
"quit": Command{(*BufPane).QuitCmd, nil},
"goto": Command{(*BufPane).GotoCmd, nil},
"save": Command{(*BufPane).SaveCmd, nil},
"replace": Command{(*BufPane).ReplaceCmd, nil},
"replaceall": Command{(*BufPane).ReplaceAllCmd, nil},
"vsplit": Command{(*BufPane).VSplitCmd, buffer.FileComplete},
"hsplit": Command{(*BufPane).HSplitCmd, buffer.FileComplete},
"tab": Command{(*BufPane).NewTabCmd, buffer.FileComplete},
"help": Command{(*BufPane).HelpCmd, HelpComplete},
"eval": Command{(*BufPane).EvalCmd, nil},
"log": Command{(*BufPane).ToggleLogCmd, nil},
"plugin": Command{(*BufPane).PluginCmd, PluginComplete},
"reload": Command{(*BufPane).ReloadCmd, nil},
"reopen": Command{(*BufPane).ReopenCmd, nil},
"cd": Command{(*BufPane).CdCmd, buffer.FileComplete},
"pwd": Command{(*BufPane).PwdCmd, nil},
"open": Command{(*BufPane).OpenCmd, buffer.FileComplete},
"tabswitch": Command{(*BufPane).TabSwitchCmd, nil},
"term": Command{(*BufPane).TermCmd, nil},
"memusage": Command{(*BufPane).MemUsageCmd, nil},
"retab": Command{(*BufPane).RetabCmd, nil},
"raw": Command{(*BufPane).RawCmd, nil},
"textfilter": Command{(*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 LuaMakeCommand(name, function string, completer buffer.Completer) {
action := LuaFunctionCommand(function)
commands[name] = Command{action, completer}
}
// LuaFunctionCommand returns a normal function
// so that a command can be bound to a lua function
func LuaFunctionCommand(fn string) func(*BufPane, []string) {
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(bp *BufPane, args []string) {
luaArgs := []lua.LValue{luar.New(ulua.L, bp), luar.New(ulua.L, args)}
_, err := pl.Call(plFn, luaArgs...)
if err != nil {
screen.TermMessage(err)
}
}
}
// 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{"list", "info", "version"}
// PluginCmd installs, removes, updates, lists, or searches for given plugins
func (h *BufPane) PluginCmd(args []string) {
if len(args) <= 0 {
InfoBar.Error("Not enough arguments, see 'help commands'")
return
}
valid := true
switch args[0] {
case "list":
for _, pl := range config.Plugins {
var en string
if pl.IsEnabled() {
en = "enabled"
} else {
en = "disabled"
}
WriteLog(fmt.Sprintf("%s: %s", pl.Name, en))
if pl.Default {
WriteLog(" (default)\n")
} else {
WriteLog("\n")
}
}
WriteLog("Default plugins come pre-installed with micro.")
case "version":
if len(args) <= 1 {
InfoBar.Error("No plugin provided to give info for")
return
}
found := false
for _, pl := range config.Plugins {
if pl.Name == args[1] {
found = true
if pl.Info == nil {
InfoBar.Message("Sorry no version for", pl.Name)
return
}
WriteLog("Version: " + pl.Info.Vstr + "\n")
}
}
if !found {
InfoBar.Message(args[1], "is not installed")
}
case "info":
if len(args) <= 1 {
InfoBar.Error("No plugin provided to give info for")
return
}
found := false
for _, pl := range config.Plugins {
if pl.Name == args[1] {
found = true
if pl.Info == nil {
InfoBar.Message("Sorry no info for ", pl.Name)
return
}
var buffer bytes.Buffer
buffer.WriteString("Name: ")
buffer.WriteString(pl.Info.Name)
buffer.WriteString("\n")
buffer.WriteString("Description: ")
buffer.WriteString(pl.Info.Desc)
buffer.WriteString("\n")
buffer.WriteString("Website: ")
buffer.WriteString(pl.Info.Site)
buffer.WriteString("\n")
buffer.WriteString("Installation link: ")
buffer.WriteString(pl.Info.Install)
buffer.WriteString("\n")
buffer.WriteString("Version: ")
buffer.WriteString(pl.Info.Vstr)
buffer.WriteString("\n")
buffer.WriteString("Requirements:")
buffer.WriteString("\n")
for _, r := range pl.Info.Require {
buffer.WriteString(" - ")
buffer.WriteString(r)
buffer.WriteString("\n")
}
WriteLog(buffer.String())
}
}
if !found {
InfoBar.Message(args[1], "is not installed")
return
}
default:
InfoBar.Error("Not a valid plugin command")
return
}
if valid && h.Buf.Type != buffer.BTLog {
OpenLogBuf(h)
}
}
// 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())
Tabs.AddTab(tp)
Tabs.SetActive(len(Tabs.List) - 1)
}
// TextFilterCmd filters the selection through the command.
// Selection goes to the command input.
// On successfull 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 := shellwords.Split(filename)
if err != nil {
InfoBar.Error("Error parsing args ", err)
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 {
OpenLogBuf(h)
} 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) {
}
// 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 {
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 {
for _, pl := range config.Plugins {
if option == pl.Name {
if nativeValue.(bool) && !pl.Loaded {
pl.Load()
pl.Call("init")
} else if !nativeValue.(bool) && pl.Loaded {
pl.Call("deinit")
}
}
}
}
for _, b := range buffer.OpenBuffers {
b.SetOptionNative(option, nativeValue)
}
return config.WriteSettings(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 arguements")
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(shellwords.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()
if h.Cursor.HasSelection() {
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.LessThan(end)
}
searchLoc := start
searching := true
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])
InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
if !canceled && yes {
h.Buf.Replace(locs[0], locs[1], replaceStr)
searchLoc = locs[0]
searchLoc.X += utf8.RuneCount(replace)
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
}
if searching {
doReplacement()
}
})
}
doReplacement()
}
h.Buf.RelocateCursors()
if nreplaced > 1 {
InfoBar.Message("Replaced ", nreplaced, " occurrences of ", search)
} else if nreplaced == 1 {
InfoBar.Message("Replaced ", nreplaced, " occurrence of ", search)
} else {
InfoBar.Message("Nothing matched ", search)
}
}
// 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 := MainTab().Panes
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)
t.Start(args, false, true, "", nil)
id := h.ID()
if newtab {
h.AddTab()
i = 0
id = MainTab().Panes[0].ID()
} else {
MainTab().Panes[i].Close()
}
v := h.GetView()
MainTab().Panes[i] = NewTermPane(v.X, v.Y, v.Width, v.Height, t, id)
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 := shellwords.Split(input)
if err != nil {
InfoBar.Error("Error parsing args ", err)
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")
}
}

42
internal/action/events.go Normal file
View 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)
}

View File

@@ -0,0 +1,42 @@
package action
import "github.com/zyedidia/micro/internal/buffer"
var InfoBar *InfoPane
var LogBufPane *BufPane
func InitGlobals() {
InfoBar = NewInfoBar()
buffer.LogBuf = buffer.NewBufferFromString("", "Log", buffer.BTLog)
}
func GetInfoBar() *InfoPane {
return InfoBar
}
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)
}
}
}
func OpenLogBuf(h *BufPane) {
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)
}
}

View File

@@ -0,0 +1,303 @@
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
}
// OptionComplete autocompletes options
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
}
// // MakeCompletion registers a function from a plugin for autocomplete commands
// func MakeCompletion(function string) Completion {
// pluginCompletions = append(pluginCompletions, LuaFunctionComplete(function))
// return Completion(-len(pluginCompletions))
// }
//
// // PluginComplete autocompletes from plugin function
// func PluginComplete(complete Completion, input string) (chosen string, suggestions []string) {
// idx := int(-complete) - 1
//
// if len(pluginCompletions) <= idx {
// return "", nil
// }
// suggestions = pluginCompletions[idx](input)
//
// if len(suggestions) == 1 {
// chosen = suggestions[0]
// }
// return
// }
//
// // PluginCmdComplete completes with possible choices for the `> plugin` command
// func PluginCmdComplete(input string) (chosen string, suggestions []string) {
// for _, cmd := range []string{"install", "remove", "search", "update", "list"} {
// if strings.HasPrefix(cmd, input) {
// suggestions = append(suggestions, cmd)
// }
// }
//
// if len(suggestions) == 1 {
// chosen = suggestions[0]
// }
// return chosen, suggestions
// }
//
// // PluginnameComplete completes with the names of loaded plugins
// func PluginNameComplete(input string) (chosen string, suggestions []string) {
// for _, pp := range GetAllPluginPackages() {
// if strings.HasPrefix(pp.Name, input) {
// suggestions = append(suggestions, pp.Name)
// }
// }
//
// if len(suggestions) == 1 {
// chosen = suggestions[0]
// }
// return chosen, suggestions
// }

226
internal/action/infopane.go Normal file
View File

@@ -0,0 +1,226 @@
package action
import (
"bytes"
"strings"
"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) *InfoPane {
ip := new(InfoPane)
ip.InfoBuf = ib
ip.BufPane = NewBufPane(ib.Buffer, w)
return ip
}
func NewInfoBar() *InfoPane {
ib := info.NewBuffer()
w := display.NewInfoWindow(ib)
return NewInfoPane(ib, w)
}
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)
}
}
case *tcell.EventMouse:
h.BufPane.HandleEvent(event)
}
}
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.Contains(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",
"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 overriden
// by the infohandler
var InfoOverrides = map[string]InfoKeyAction{
"CursorUp": (*InfoPane).CursorUp,
"CursorDown": (*InfoPane).CursorDown,
"InsertNewline": (*InfoPane).InsertNewline,
"Autocomplete": (*InfoPane).Autocomplete,
"OutdentLine": (*InfoPane).CycleBack,
"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 len(args) == 1 {
b.Autocomplete(CommandComplete)
} else {
if action, ok := commands[cmd]; ok {
if action.completer != nil {
b.Autocomplete(action.completer)
}
}
}
}
// CycleBack cycles back in the autocomplete suggestion list
func (h *InfoPane) CycleBack() {
if h.Buf.HasSuggestions {
h.Buf.CycleAutocomplete(false)
}
}
// 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)
}

14
internal/action/pane.go Normal file
View File

@@ -0,0 +1,14 @@
package action
import (
"github.com/zyedidia/micro/internal/display"
)
type Pane interface {
Handler
display.Window
ID() uint64
SetID(i uint64)
Name() string
Close()
}

View 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) *RawPane {
rh := new(RawPane)
rh.BufPane = NewBufPane(b, win)
return rh
}
func NewRawPane() *RawPane {
b := buffer.NewBufferFromString("", "", buffer.BTRaw)
w := display.NewBufWindow(0, 0, 0, 0, b)
return NewRawPaneFromWin(b, w)
}
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()
}

282
internal/action/tab.go Normal file
View File

@@ -0,0 +1,282 @@
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()
}
}
// 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:
ind := t.LocFromVisual(buffer.Loc{mx, my})
if ind != -1 {
t.SetActive(ind)
}
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
}
// 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)
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.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:
resizeID := t.GetMouseSplitID(buffer.Loc{mx, my})
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 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
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() Pane {
return t.Panes[t.active]
}

View File

@@ -0,0 +1,30 @@
// +build linux darwin dragonfly openbsd_amd64 freebsd
package action
import (
"github.com/zyedidia/micro/internal/shell"
"github.com/zyedidia/micro/pkg/shellwords"
)
const TermEmuSupported = true
func RunTermEmulator(h *BufPane, input string, wait bool, getOutput bool, callback string, userargs []interface{}) error {
args, err := shellwords.Split(input)
if err != nil {
return err
}
t := new(shell.Terminal)
t.Start(args, getOutput, wait, callback, userargs)
id := h.ID()
h.AddTab()
id = MainTab().Panes[0].ID()
v := h.GetView()
MainTab().Panes[0] = NewTermPane(v.X, v.Y, v.Width, v.Height, t, id)
MainTab().SetActive(0)
return nil
}

View File

@@ -1,11 +1,11 @@
// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd_amd64
package main
package action
import "errors"
const TermEmuSupported = false
func RunTermEmulator(input string, wait bool, getOutput bool) error {
func RunTermEmulator(input string, wait bool, getOutput bool, callback string, userargs []interface{}) error {
return errors.New("Unsupported operating system")
}

120
internal/action/termpane.go Normal file
View File

@@ -0,0 +1,120 @@
package action
import (
"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
}
func NewTermPane(x, y, w, h int, t *shell.Terminal, id uint64) *TermPane {
th := new(TermPane)
th.Terminal = t
th.id = id
th.mouseReleased = true
th.Window = display.NewTermWindow(x, y, w, h, t)
return th
}
func (t *TermPane) ID() uint64 {
return t.id
}
func (t *TermPane) SetID(i uint64) {
t.id = i
}
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 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")
}

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

118
internal/buffer/backup.go Normal file
View File

@@ -0,0 +1,118 @@
package buffer
import (
"fmt"
"io"
"log"
"os"
"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.
* '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 == "" {
return nil
}
if checkTime {
sub := time.Now().Sub(b.lastbackup)
if sub < time.Duration(backup_time)*time.Millisecond {
log.Println("Backup event but not enough time has passed", sub)
return nil
}
}
b.lastbackup = time.Now()
backupdir := config.ConfigDir + "/backups/"
if _, err := os.Stat(backupdir); os.IsNotExist(err) {
os.Mkdir(backupdir, os.ModePerm)
log.Println("Creating backup dir")
}
name := backupdir + util.EscapePath(b.AbsPath)
log.Println("Backing up to", name)
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
})
return err
}
// RemoveBackup removes any backup file associated with this buffer
func (b *Buffer) RemoveBackup() {
if !b.Settings["backup"].(bool) || b.Path == "" {
return
}
f := 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 {
backupfile := 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"))
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
}

798
internal/buffer/buffer.go Normal file
View File

@@ -0,0 +1,798 @@
package buffer
import (
"bytes"
"crypto/md5"
"errors"
"io"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"unicode/utf8"
luar "layeh.com/gopher-luar"
"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"
"github.com/zyedidia/micro/pkg/highlight"
"golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
)
const backup_time = 8000
var (
OpenBuffers []*Buffer
LogBuf *Buffer
)
// The BufType defines what kind of buffer this is
type BufType struct {
Kind int
Readonly bool // The buffer cannot be edited
Scratch bool // The buffer cannot be saved
Syntax bool // Syntax highlighting is enabled
}
var (
BTDefault = BufType{0, false, false, true}
BTHelp = BufType{1, true, true, true}
BTLog = BufType{2, true, true, false}
BTScratch = BufType{3, false, true, false}
BTRaw = BufType{4, false, true, false}
BTInfo = BufType{5, false, true, false}
ErrFileTooLarge = errors.New("File is too large to hash")
)
type SharedBuffer struct {
*LineArray
// Stores the last modification time of the file the buffer is pointing to
ModTime time.Time
// Type of the buffer (e.g. help, raw, scratch etc..)
Type BufType
isModified bool
// Whether or not suggestions can be autocompleted must be shared because
// it changes based on how the buffer has changed
HasSuggestions bool
}
func (b *SharedBuffer) insert(pos Loc, value []byte) {
b.isModified = true
b.HasSuggestions = false
b.LineArray.insert(pos, value)
}
func (b *SharedBuffer) remove(start, end Loc) []byte {
b.isModified = true
b.HasSuggestions = false
return b.LineArray.remove(start, end)
}
// Buffer stores the main information about a currently open file including
// the actual text (in a LineArray), the undo/redo stack (in an EventHandler)
// all the cursors, the syntax highlighting info, the settings for the buffer
// and some misc info about modification time and path location.
// The syntax highlighting info must be stored with the buffer because the syntax
// highlighter attaches information to each line of the buffer for optimization
// purposes so it doesn't have to rehighlight everything on every update.
type Buffer struct {
*EventHandler
*SharedBuffer
cursors []*Cursor
curCursor int
StartCursor Loc
// Path to the file on disk
Path string
// Absolute path to the file on disk
AbsPath string
// Name of the buffer on the status line
name string
SyntaxDef *highlight.Def
Highlighter *highlight.Highlighter
// Hash of the original buffer -- empty if fastdirty is on
origHash [md5.Size]byte
// Settings customized by the user
Settings map[string]interface{}
Suggestions []string
Completions []string
CurSuggestion int
Messages []*Message
// counts the number of edits
// resets every backup_time edits
lastbackup time.Time
}
// NewBufferFromFile opens a new buffer using the given path
// It will also automatically handle `~`, and line/column with filename:l:c
// It will return an empty buffer if the path does not exist
// and an error if the file is a directory
func NewBufferFromFile(path string, btype BufType) (*Buffer, error) {
var err error
filename, cursorPos := GetPathAndCursorPosition(path)
filename, err = ReplaceHome(filename)
if err != nil {
return nil, err
}
file, err := os.Open(filename)
fileInfo, _ := os.Stat(filename)
if err == nil && fileInfo.IsDir() {
return nil, errors.New(filename + " is a directory")
}
defer file.Close()
cursorLoc, cursorerr := ParseCursorLocation(cursorPos)
if cursorerr != nil {
cursorLoc = Loc{-1, -1}
}
var buf *Buffer
if err != nil {
// File does not exist -- create an empty buffer with that name
buf = NewBufferFromString("", filename, btype)
} else {
buf = NewBuffer(file, FSize(file), filename, cursorLoc, btype)
}
return buf, nil
}
// NewBufferFromString creates a new buffer containing the given string
func NewBufferFromString(text, path string, btype BufType) *Buffer {
return NewBuffer(strings.NewReader(text), int64(len(text)), path, Loc{-1, -1}, btype)
}
// NewBuffer creates a new buffer from a given reader with a given path
// Ensure that ReadSettings and InitGlobalSettings have been called before creating
// a new buffer
// Places the cursor at startcursor. If startcursor is -1, -1 places the
// cursor at an autodetected location (based on savecursor or :LINE:COL)
func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufType) *Buffer {
absPath, _ := filepath.Abs(path)
b := new(Buffer)
b.Settings = config.DefaultCommonSettings()
for k, v := range config.GlobalSettings {
if _, ok := b.Settings[k]; ok {
b.Settings[k] = v
}
}
config.InitLocalSettings(b.Settings, path)
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
if err != nil {
enc = unicode.UTF8
b.Settings["encoding"] = "utf-8"
}
reader := transform.NewReader(r, enc.NewDecoder())
found := false
if len(path) > 0 {
for _, buf := range OpenBuffers {
if buf.AbsPath == absPath && buf.Type != BTInfo {
found = true
b.SharedBuffer = buf.SharedBuffer
b.EventHandler = buf.EventHandler
}
}
}
b.Path = path
b.AbsPath = absPath
if !found {
b.SharedBuffer = new(SharedBuffer)
b.Type = btype
hasBackup := b.ApplyBackup(size)
if !hasBackup {
b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
}
b.EventHandler = NewEventHandler(b.SharedBuffer, b.cursors)
}
if b.Settings["readonly"].(bool) {
b.Type.Readonly = true
}
// The last time this file was modified
b.ModTime, _ = GetModTime(b.Path)
switch b.Endings {
case FFUnix:
b.Settings["fileformat"] = "unix"
case FFDos:
b.Settings["fileformat"] = "dos"
}
b.UpdateRules()
config.InitLocalSettings(b.Settings, b.Path)
if _, err := os.Stat(config.ConfigDir + "/buffers/"); os.IsNotExist(err) {
os.Mkdir(config.ConfigDir+"/buffers/", os.ModePerm)
}
if startcursor.X != -1 && startcursor.Y != -1 {
b.StartCursor = startcursor
} else {
if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
err := b.Unserialize()
if err != nil {
screen.TermMessage(err)
}
}
}
b.AddCursor(NewCursor(b, b.StartCursor))
b.GetActiveCursor().Relocate()
if !b.Settings["fastdirty"].(bool) {
if size > LargeFileThreshold {
// If the file is larger than LargeFileThreshold fastdirty needs to be on
b.Settings["fastdirty"] = true
} else {
calcHash(b, &b.origHash)
}
}
err = config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b))
if err != nil {
screen.TermMessage(err)
}
OpenBuffers = append(OpenBuffers, b)
return b
}
// Close removes this buffer from the list of open buffers
func (b *Buffer) Close() {
for i, buf := range OpenBuffers {
if b == buf {
b.Fini()
copy(OpenBuffers[i:], OpenBuffers[i+1:])
OpenBuffers[len(OpenBuffers)-1] = nil
OpenBuffers = OpenBuffers[:len(OpenBuffers)-1]
return
}
}
}
// Fini should be called when a buffer is closed and performs
// some cleanup
func (b *Buffer) Fini() {
if !b.Modified() {
b.Serialize()
}
b.RemoveBackup()
}
// GetName returns the name that should be displayed in the statusline
// for this buffer
func (b *Buffer) GetName() string {
if b.name == "" {
if b.Path == "" {
return "No name"
}
return b.Path
}
return b.name
}
//SetName changes the name for this buffer
func (b *Buffer) SetName(s string) {
b.name = s
}
func (b *Buffer) Insert(start Loc, text string) {
if !b.Type.Readonly {
b.EventHandler.cursors = b.cursors
b.EventHandler.active = b.curCursor
b.EventHandler.Insert(start, text)
go b.Backup(true)
}
}
func (b *Buffer) Remove(start, end Loc) {
if !b.Type.Readonly {
b.EventHandler.cursors = b.cursors
b.EventHandler.active = b.curCursor
b.EventHandler.Remove(start, end)
go b.Backup(true)
}
}
// FileType returns the buffer's filetype
func (b *Buffer) FileType() string {
return b.Settings["filetype"].(string)
}
// ReOpen reloads the current buffer from disk
func (b *Buffer) ReOpen() error {
file, err := os.Open(b.Path)
if err != nil {
return err
}
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
if err != nil {
return err
}
reader := transform.NewReader(file, enc.NewDecoder())
data, err := ioutil.ReadAll(reader)
txt := string(data)
if err != nil {
return err
}
b.EventHandler.ApplyDiff(txt)
b.ModTime, err = GetModTime(b.Path)
b.isModified = false
b.RelocateCursors()
return err
}
func (b *Buffer) RelocateCursors() {
for _, c := range b.cursors {
c.Relocate()
}
}
// RuneAt returns the rune at a given location in the buffer
func (b *Buffer) RuneAt(loc Loc) rune {
line := b.LineBytes(loc.Y)
if len(line) > 0 {
i := 0
for len(line) > 0 {
r, size := utf8.DecodeRune(line)
line = line[size:]
i++
if i == loc.X {
return r
}
}
}
return '\n'
}
// Modified returns if this buffer has been modified since
// being opened
func (b *Buffer) Modified() bool {
if b.Type.Scratch {
return false
}
if b.Settings["fastdirty"].(bool) {
return b.isModified
}
var buff [md5.Size]byte
calcHash(b, &buff)
return buff != b.origHash
}
// calcHash calculates md5 hash of all lines in the buffer
func calcHash(b *Buffer, out *[md5.Size]byte) error {
h := md5.New()
size := 0
if len(b.lines) > 0 {
n, e := h.Write(b.lines[0].data)
if e != nil {
return e
}
size += n
for _, l := range b.lines[1:] {
n, e = h.Write([]byte{'\n'})
if e != nil {
return e
}
size += n
n, e = h.Write(l.data)
if e != nil {
return e
}
size += n
}
}
if size > LargeFileThreshold {
return ErrFileTooLarge
}
h.Sum((*out)[:0])
return nil
}
// UpdateRules updates the syntax rules and filetype for this buffer
// This is called when the colorscheme changes
func (b *Buffer) UpdateRules() {
if !b.Type.Syntax {
return
}
rehighlight := false
var files []*highlight.File
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
data, err := f.Data()
if err != nil {
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
} else {
file, err := highlight.ParseFile(data)
if err != nil {
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
ftdetect, err := highlight.ParseFtDetect(file)
if err != nil {
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
ft := b.Settings["filetype"].(string)
if (ft == "unknown" || ft == "") && !rehighlight {
if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
header := new(highlight.Header)
header.FileType = file.FileType
header.FtDetect = ftdetect
b.SyntaxDef, err = highlight.ParseDef(file, header)
if err != nil {
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
rehighlight = true
}
} else {
if file.FileType == ft && !rehighlight {
header := new(highlight.Header)
header.FileType = file.FileType
header.FtDetect = ftdetect
b.SyntaxDef, err = highlight.ParseDef(file, header)
if err != nil {
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
rehighlight = true
}
}
files = append(files, file)
}
}
if b.SyntaxDef != nil {
highlight.ResolveIncludes(b.SyntaxDef, files)
}
if b.Highlighter == nil || rehighlight {
if b.SyntaxDef != nil {
b.Settings["filetype"] = b.SyntaxDef.FileType
b.Highlighter = highlight.NewHighlighter(b.SyntaxDef)
if b.Settings["syntax"].(bool) {
b.Highlighter.HighlightStates(b)
}
}
}
}
// ClearMatches clears all of the syntax highlighting for the buffer
func (b *Buffer) ClearMatches() {
for i := range b.lines {
b.SetMatch(i, nil)
b.SetState(i, nil)
}
}
// IndentString returns this buffer's indent method (a tabstop or n spaces
// depending on the settings)
func (b *Buffer) IndentString(tabsize int) string {
if b.Settings["tabstospaces"].(bool) {
return Spaces(tabsize)
}
return "\t"
}
// SetCursors resets this buffer's cursors to a new list
func (b *Buffer) SetCursors(c []*Cursor) {
b.cursors = c
b.EventHandler.cursors = b.cursors
b.EventHandler.active = b.curCursor
}
// AddCursor adds a new cursor to the list
func (b *Buffer) AddCursor(c *Cursor) {
b.cursors = append(b.cursors, c)
b.EventHandler.cursors = b.cursors
b.EventHandler.active = b.curCursor
b.UpdateCursors()
}
// SetCurCursor sets the current cursor
func (b *Buffer) SetCurCursor(n int) {
b.curCursor = n
}
// GetActiveCursor returns the main cursor in this buffer
func (b *Buffer) GetActiveCursor() *Cursor {
return b.cursors[b.curCursor]
}
// GetCursor returns the nth cursor
func (b *Buffer) GetCursor(n int) *Cursor {
return b.cursors[n]
}
// GetCursors returns the list of cursors in this buffer
func (b *Buffer) GetCursors() []*Cursor {
return b.cursors
}
// NumCursors returns the number of cursors
func (b *Buffer) NumCursors() int {
return len(b.cursors)
}
// MergeCursors merges any cursors that are at the same position
// into one cursor
func (b *Buffer) MergeCursors() {
var cursors []*Cursor
for i := 0; i < len(b.cursors); i++ {
c1 := b.cursors[i]
if c1 != nil {
for j := 0; j < len(b.cursors); j++ {
c2 := b.cursors[j]
if c2 != nil && i != j && c1.Loc == c2.Loc {
b.cursors[j] = nil
}
}
cursors = append(cursors, c1)
}
}
b.cursors = cursors
for i := range b.cursors {
b.cursors[i].Num = i
}
if b.curCursor >= len(b.cursors) {
b.curCursor = len(b.cursors) - 1
}
b.EventHandler.cursors = b.cursors
b.EventHandler.active = b.curCursor
}
// UpdateCursors updates all the cursors indicies
func (b *Buffer) UpdateCursors() {
b.EventHandler.cursors = b.cursors
b.EventHandler.active = b.curCursor
for i, c := range b.cursors {
c.Num = i
}
}
func (b *Buffer) RemoveCursor(i int) {
copy(b.cursors[i:], b.cursors[i+1:])
b.cursors[len(b.cursors)-1] = nil
b.cursors = b.cursors[:len(b.cursors)-1]
b.curCursor = Clamp(b.curCursor, 0, len(b.cursors)-1)
b.UpdateCursors()
}
// ClearCursors removes all extra cursors
func (b *Buffer) ClearCursors() {
for i := 1; i < len(b.cursors); i++ {
b.cursors[i] = nil
}
b.cursors = b.cursors[:1]
b.UpdateCursors()
b.curCursor = 0
b.GetActiveCursor().ResetSelection()
}
// MoveLinesUp moves the range of lines up one row
func (b *Buffer) MoveLinesUp(start int, end int) {
if start < 1 || start >= end || end > len(b.lines) {
return
}
l := string(b.LineBytes(start - 1))
if end == len(b.lines) {
b.Insert(
Loc{
utf8.RuneCount(b.lines[end-1].data),
end - 1,
},
"\n"+l,
)
} else {
b.Insert(
Loc{0, end},
l+"\n",
)
}
b.Remove(
Loc{0, start - 1},
Loc{0, start},
)
}
// MoveLinesDown moves the range of lines down one row
func (b *Buffer) MoveLinesDown(start int, end int) {
if start < 0 || start >= end || end >= len(b.lines)-1 {
return
}
l := string(b.LineBytes(end))
b.Insert(
Loc{0, start},
l+"\n",
)
end++
b.Remove(
Loc{0, end},
Loc{0, end + 1},
)
}
var BracePairs = [][2]rune{
{'(', ')'},
{'{', '}'},
{'[', ']'},
}
// FindMatchingBrace returns the location in the buffer of the matching bracket
// It is given a brace type containing the open and closing character, (for example
// '{' and '}') as well as the location to match from
// TODO: maybe can be more efficient with utf8 package
// returns the location of the matching brace
// if the boolean returned is true then the original matching brace is one character left
// of the starting location
func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool) {
curLine := []rune(string(b.LineBytes(start.Y)))
startChar := ' '
if start.X >= 0 && start.X < len(curLine) {
startChar = curLine[start.X]
}
leftChar := ' '
if start.X-1 >= 0 && start.X-1 < len(curLine) {
leftChar = curLine[start.X-1]
}
var i int
if startChar == braceType[0] || leftChar == braceType[0] {
for y := start.Y; y < b.LinesNum(); y++ {
l := []rune(string(b.LineBytes(y)))
xInit := 0
if y == start.Y {
if startChar == braceType[0] {
xInit = start.X
} else {
xInit = start.X - 1
}
}
for x := xInit; x < len(l); x++ {
r := l[x]
if r == braceType[0] {
i++
} else if r == braceType[1] {
i--
if i == 0 {
if startChar == braceType[0] {
return Loc{x, y}, false
}
return Loc{x, y}, true
}
}
}
}
} else if startChar == braceType[1] || leftChar == braceType[1] {
for y := start.Y; y >= 0; y-- {
l := []rune(string(b.lines[y].data))
xInit := len(l) - 1
if y == start.Y {
if leftChar == braceType[1] {
xInit = start.X - 1
} else {
xInit = start.X
}
}
for x := xInit; x >= 0; x-- {
r := l[x]
if r == braceType[0] {
i--
if i == 0 {
if leftChar == braceType[1] {
return Loc{x, y}, true
}
return Loc{x, y}, false
}
} else if r == braceType[1] {
i++
}
}
}
}
return start, true
}
// Retab changes all tabs to spaces or vice versa
func (b *Buffer) Retab() {
toSpaces := b.Settings["tabstospaces"].(bool)
tabsize := IntOpt(b.Settings["tabsize"])
dirty := false
for i := 0; i < b.LinesNum(); i++ {
l := b.LineBytes(i)
ws := GetLeadingWhitespace(l)
if len(ws) != 0 {
if toSpaces {
ws = bytes.Replace(ws, []byte{'\t'}, bytes.Repeat([]byte{' '}, tabsize), -1)
} else {
ws = bytes.Replace(ws, bytes.Repeat([]byte{' '}, tabsize), []byte{'\t'}, -1)
}
}
l = bytes.TrimLeft(l, " \t")
b.lines[i].data = append(ws, l...)
dirty = true
}
b.isModified = dirty
}
// ParseCursorLocation turns a cursor location like 10:5 (LINE:COL)
// into a loc
func ParseCursorLocation(cursorPositions []string) (Loc, error) {
startpos := Loc{0, 0}
var err error
// if no positions are available exit early
if cursorPositions == nil {
return startpos, errors.New("No cursor positions were provided.")
}
startpos.Y, err = strconv.Atoi(cursorPositions[0])
startpos.Y -= 1
if err == nil {
if len(cursorPositions) > 1 {
startpos.X, err = strconv.Atoi(cursorPositions[1])
if startpos.X > 0 {
startpos.X -= 1
}
}
}
return startpos, err
}
func (b *Buffer) Line(i int) string {
return string(b.LineBytes(i))
}
func WriteLog(s string) {
LogBuf.EventHandler.Insert(LogBuf.End(), s)
}

View File

@@ -1,15 +1,23 @@
package main
package buffer
import (
"unicode/utf8"
"github.com/zyedidia/clipboard"
"github.com/zyedidia/micro/internal/util"
)
// 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.
// 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
@@ -28,6 +36,23 @@ type Cursor struct {
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) {
@@ -39,6 +64,54 @@ func (c *Cursor) Goto(b Cursor) {
// 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()
}
}
// 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()
}
@@ -47,7 +120,7 @@ func (c *Cursor) GotoLoc(l Loc) {
func (c *Cursor) CopySelection(target string) {
if c.HasSelection() {
if target != "primary" || c.buf.Settings["useprimary"].(bool) {
clipboard.WriteAll(c.GetSelection(), target)
clipboard.WriteAll(string(c.GetSelection()), target)
}
}
}
@@ -86,15 +159,30 @@ func (c *Cursor) DeleteSelection() {
}
}
// 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() string {
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 ""
return []byte{}
}
// SelectLine selects the current line
@@ -102,7 +190,7 @@ func (c *Cursor) SelectLine() {
c.Start()
c.SetSelectionStart(c.Loc)
c.End()
if c.buf.NumLines-1 > c.Y {
if len(c.buf.lines)-1 > c.Y {
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
} else {
c.SetSelectionEnd(c.Loc)
@@ -129,148 +217,20 @@ func (c *Cursor) AddLineToSelection() {
}
}
// 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.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 && IsWordChar(string(c.RuneUnder(backward-1))) {
backward--
}
c.SetSelectionStart(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.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 && IsWordChar(string(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
for forward < Count(c.buf.Line(c.Y))-1 && IsWordChar(string(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 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
c.LastVisualX = 0
} else if proposedY >= c.buf.NumLines {
proposedY = c.buf.NumLines - 1
} else if proposedY >= len(c.buf.lines) {
proposedY = len(c.buf.lines) - 1
}
runes := []rune(c.buf.Line(c.Y))
c.X = c.GetCharPosInLine(proposedY, c.LastVisualX)
bytes := c.buf.LineBytes(proposedY)
c.X = c.GetCharPosInLine(bytes, c.LastVisualX)
if c.X > len(runes) || (amount < 0 && proposedY == c.Y) {
c.X = len(runes)
if c.X > utf8.RuneCount(bytes) || (amount < 0 && proposedY == c.Y) {
c.X = utf8.RuneCount(bytes)
}
c.Y = proposedY
@@ -303,7 +263,7 @@ func (c *Cursor) Left() {
c.Up()
c.End()
}
c.LastVisualX = c.GetVisualX()
c.StoreVisualX()
}
// Right moves the cursor right one cell (if possible) or
@@ -312,62 +272,13 @@ func (c *Cursor) Right() {
if c.Loc == c.buf.End() {
return
}
if c.X < Count(c.buf.Line(c.Y)) {
if c.X < utf8.RuneCount(c.buf.LineBytes(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))
if c.X > len(runes) {
c.X = len(runes) - 1
}
if c.X < 0 {
c.X = 0
}
return StringWidth(string(runes[:c.X]), tabSize)
}
// StoreVisualX stores the current visual x value in the cursor
func (c *Cursor) StoreVisualX() {
c.LastVisualX = c.GetVisualX()
c.StoreVisualX()
}
// Relocate makes sure that the cursor is inside the bounds
@@ -376,13 +287,154 @@ func (c *Cursor) StoreVisualX() {
func (c *Cursor) Relocate() {
if c.Y < 0 {
c.Y = 0
} else if c.Y >= c.buf.NumLines {
c.Y = c.buf.NumLines - 1
} 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 > Count(c.buf.Line(c.Y)) {
c.X = Count(c.buf.Line(c.Y))
} 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()
}

View File

@@ -1,11 +1,10 @@
package main
package buffer
import (
"strings"
"time"
"unicode/utf8"
dmp "github.com/sergi/go-diff/diffmatchpatch"
"github.com/yuin/gopher-lua"
)
const (
@@ -17,6 +16,8 @@ const (
TextEventRemove = -1
// TextEventReplace represents a replace event
TextEventReplace = 0
undoThreshold = 500 // 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
@@ -30,16 +31,16 @@ type TextEvent struct {
// A Delta is a change to the buffer
type Delta struct {
Text string
Text []byte
Start Loc
End Loc
}
// ExecuteTextEvent runs a text event
func ExecuteTextEvent(t *TextEvent, buf *Buffer) {
func ExecuteTextEvent(t *TextEvent, buf *SharedBuffer) {
if t.EventType == TextEventInsert {
for _, d := range t.Deltas {
buf.insert(d.Start, []byte(d.Text))
buf.insert(d.Start, d.Text)
}
} else if t.EventType == TextEventRemove {
for i, d := range t.Deltas {
@@ -48,9 +49,9 @@ func ExecuteTextEvent(t *TextEvent, buf *Buffer) {
} 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, []byte(d.Text))
buf.insert(d.Start, d.Text)
t.Deltas[i].Start = d.Start
t.Deltas[i].End = Loc{d.Start.X + Count(d.Text), d.Start.Y}
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]
@@ -59,24 +60,27 @@ func ExecuteTextEvent(t *TextEvent, buf *Buffer) {
}
// UndoTextEvent undoes a text event
func UndoTextEvent(t *TextEvent, buf *Buffer) {
func UndoTextEvent(t *TextEvent, buf *SharedBuffer) {
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
buf *SharedBuffer
cursors []*Cursor
active int
UndoStack *TEStack
RedoStack *TEStack
}
// NewEventHandler returns a new EventHandler
func NewEventHandler(buf *Buffer) *EventHandler {
func NewEventHandler(buf *SharedBuffer, cursors []*Cursor) *EventHandler {
eh := new(EventHandler)
eh.UndoStack = new(Stack)
eh.RedoStack = new(Stack)
eh.UndoStack = new(TEStack)
eh.RedoStack = new(TEStack)
eh.buf = buf
eh.cursors = cursors
return eh
}
@@ -86,38 +90,39 @@ func NewEventHandler(buf *Buffer) *EventHandler {
// through insert and delete events
func (eh *EventHandler) ApplyDiff(new string) {
differ := dmp.New()
diff := differ.DiffMain(eh.buf.String(), new, false)
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.Move(Count(d.Text), eh.buf))
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.Move(Count(d.Text), eh.buf)
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, text string) {
func (eh *EventHandler) Insert(start Loc, textStr string) {
text := []byte(textStr)
e := &TextEvent{
C: *eh.buf.cursors[eh.buf.curCursor],
C: *eh.cursors[eh.active],
EventType: TextEventInsert,
Deltas: []Delta{{text, start, Loc{0, 0}}},
Time: time.Now(),
}
eh.Execute(e)
e.Deltas[0].End = start.Move(Count(text), eh.buf)
e.Deltas[0].End = start.MoveLA(utf8.RuneCount(text), eh.buf.LineArray)
end := e.Deltas[0].End
for _, c := range eh.buf.cursors {
for _, c := range eh.cursors {
move := func(loc Loc) Loc {
if start.Y != end.Y && loc.GreaterThan(start) {
loc.Y += end.Y - start.Y
} else if loc.Y == start.Y && loc.GreaterEqual(start) {
loc = loc.Move(Count(text), eh.buf)
loc = loc.MoveLA(utf8.RuneCount(text), eh.buf.LineArray)
}
return loc
}
@@ -133,19 +138,19 @@ func (eh *EventHandler) Insert(start Loc, text string) {
// Remove creates a remove text event and executes it
func (eh *EventHandler) Remove(start, end Loc) {
e := &TextEvent{
C: *eh.buf.cursors[eh.buf.curCursor],
C: *eh.cursors[eh.active],
EventType: TextEventRemove,
Deltas: []Delta{{"", start, end}},
Deltas: []Delta{{[]byte{}, start, end}},
Time: time.Now(),
}
eh.Execute(e)
for _, c := range eh.buf.cursors {
for _, c := range eh.cursors {
move := func(loc Loc) Loc {
if start.Y != end.Y && loc.GreaterThan(end) {
loc.Y -= end.Y - start.Y
} else if loc.Y == end.Y && loc.GreaterEqual(end) {
loc = loc.Move(-Diff(start, end, eh.buf), eh.buf)
loc = loc.MoveLA(-DiffLA(start, end, eh.buf.LineArray), eh.buf.LineArray)
}
return loc
}
@@ -161,7 +166,7 @@ func (eh *EventHandler) Remove(start, end Loc) {
// MultipleReplace creates an multiple insertions executes them
func (eh *EventHandler) MultipleReplace(deltas []Delta) {
e := &TextEvent{
C: *eh.buf.cursors[eh.buf.curCursor],
C: *eh.cursors[eh.active],
EventType: TextEventReplace,
Deltas: deltas,
Time: time.Now(),
@@ -178,19 +183,20 @@ func (eh *EventHandler) Replace(start, end Loc, replace string) {
// 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.RedoStack = new(TEStack)
}
eh.UndoStack.Push(t)
for pl := range loadedPlugins {
ret, err := Call(pl+".onBeforeTextEvent", t)
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
}
if val, ok := ret.(lua.LBool); ok && val == lua.LFalse {
return
}
}
// TODO: Call plugins on text events
// for pl := range loadedPlugins {
// ret, err := Call(pl+".onBeforeTextEvent", t)
// if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
// screen.TermMessage(err)
// }
// if val, ok := ret.(lua.LBool); ok && val == lua.LFalse {
// return
// }
// }
ExecuteTextEvent(t, eh.buf)
}
@@ -236,9 +242,9 @@ func (eh *EventHandler) UndoOneEvent() {
// Set the cursor in the right place
teCursor := t.C
if teCursor.Num >= 0 && teCursor.Num < len(eh.buf.cursors) {
t.C = *eh.buf.cursors[teCursor.Num]
eh.buf.cursors[teCursor.Num].Goto(teCursor)
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
}
@@ -283,9 +289,9 @@ func (eh *EventHandler) RedoOneEvent() {
UndoTextEvent(t, eh.buf)
teCursor := t.C
if teCursor.Num >= 0 && teCursor.Num < len(eh.buf.cursors) {
t.C = *eh.buf.cursors[teCursor.Num]
eh.buf.cursors[teCursor.Num].Goto(teCursor)
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
}

View File

@@ -1,13 +1,14 @@
package main
package buffer
import (
"bufio"
"io"
"unicode/utf8"
"github.com/zyedidia/micro/cmd/micro/highlight"
"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
@@ -39,10 +40,21 @@ type Line struct {
rehighlight bool
}
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
lines []Line
Endings FileFormat
initsize uint64
}
// Append efficiently appends lines together
@@ -52,7 +64,6 @@ func Append(slice []Line, data ...Line) []Line {
l := len(slice)
if l+len(data) > cap(slice) { // reallocate
newSlice := make([]Line, (l+len(data))+10000)
// The copy function is predeclared and works for any slice type.
copy(newSlice, slice)
slice = newSlice
}
@@ -64,10 +75,11 @@ func Append(slice []Line, data ...Line) []Line {
}
// NewLineArray returns a new line array from an array of bytes
func NewLineArray(size int64, reader io.Reader) *LineArray {
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
@@ -75,40 +87,49 @@ func NewLineArray(size int64, reader io.Reader) *LineArray {
n := 0
for {
data, err := br.ReadBytes('\n')
if len(data) > 1 && data[len(data)-2] == '\r' {
data = append(data[:len(data)-2], '\n')
if fileformat == 0 {
fileformat = 2
// 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
}
} else if len(data) > 0 {
if fileformat == 0 {
fileformat = 1
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)
// The copy function is predeclared and works for any slice type.
copy(newSlice, la.lines)
la.lines = newSlice
loaded = -1
}
// Counter for the number of bytes in the first 1000 lines
if loaded >= 0 {
loaded += len(data)
loaded += dlen
}
if err != nil {
if err == io.EOF {
la.lines = Append(la.lines, Line{data[:], nil, nil, false})
// la.lines = Append(la.lines, Line{data[:len(data)]})
}
// Last line was read
break
} else {
// la.lines = Append(la.lines, Line{data[:len(data)-1]})
la.lines = Append(la.lines, Line{data[:len(data)-1], nil, nil, false})
la.lines = Append(la.lines, Line{data[:dlen-1], nil, nil, false})
}
n++
}
@@ -116,49 +137,35 @@ func NewLineArray(size int64, reader io.Reader) *LineArray {
return la
}
// Returns the String representation of the LineArray
func (la *LineArray) String() string {
str := ""
for i, l := range la.lines {
str += string(l.data)
if i != len(la.lines)-1 {
str += "\n"
}
}
return str
}
// SaveString returns the string that should be written to disk when
// Bytes returns the string that should be written to disk when
// the line array is saved
// It is the same as string but uses crlf or lf line endings depending
func (la *LineArray) SaveString(useCrlf bool) string {
str := ""
func (la *LineArray) Bytes() []byte {
str := make([]byte, 0, la.initsize+1000) // initsize should provide a good estimate
for i, l := range la.lines {
str += string(l.data)
str = append(str, l.data...)
if i != len(la.lines)-1 {
if useCrlf {
str += "\r"
if la.Endings == FFDos {
str = append(str, '\r')
}
str += "\n"
str = append(str, '\n')
}
}
return str
}
// NewlineBelow adds a newline below the given line number
func (la *LineArray) NewlineBelow(y int) {
la.lines = append(la.lines, Line{[]byte(" "), nil, nil, false})
// newlineBelow adds a newline below the given line number
func (la *LineArray) newlineBelow(y int) {
la.lines = append(la.lines, Line{[]byte{' '}, nil, nil, false})
copy(la.lines[y+2:], la.lines[y+1:])
la.lines[y+1] = Line{[]byte(""), la.lines[y].state, nil, false}
la.lines[y+1] = Line{[]byte{}, la.lines[y].state, nil, false}
}
// inserts a byte array at a given location
// 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
// x, y := pos.x, pos.y
for i := 0; i < len(value); i++ {
if value[i] == '\n' {
la.Split(Loc{x, y})
la.split(Loc{x, y})
x = 0
y++
continue
@@ -168,33 +175,33 @@ func (la *LineArray) insert(pos Loc, value []byte) {
}
}
// inserts a byte at a given location
// 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) {
// 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)
la.deleteLine(b)
}
// Split splits a line at a given position
func (la *LineArray) Split(pos Loc) {
la.NewlineBelow(pos.Y)
// 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})
la.deleteToEnd(Loc{pos.X, pos.Y})
}
// removes from start to end
func (la *LineArray) remove(start, end Loc) string {
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)
@@ -202,51 +209,80 @@ func (la *LineArray) remove(start, end Loc) string {
la.lines[start.Y].data = append(la.lines[start.Y].data[:startX], la.lines[start.Y].data[endX:]...)
} else {
for i := start.Y + 1; i <= end.Y-1; i++ {
la.DeleteLine(start.Y + 1)
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)
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) {
// 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) {
// 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) {
// 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) {
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) string {
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 {
return string(la.lines[start.Y].data[startX:endX])
src := la.lines[start.Y].data[startX:endX]
dest := make([]byte, len(src))
copy(dest, src)
return dest
}
var str string
str += string(la.lines[start.Y].data[startX:]) + "\n"
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 += string(la.lines[i].data) + "\n"
str = append(str, la.lines[i].data...)
str = append(str, '\n')
}
str += string(la.lines[end.Y].data[:endX])
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 {
return la.lines[lineN].state
@@ -266,3 +302,11 @@ func (la *LineArray) SetMatch(lineN int, m highlight.LineMatch) {
func (la *LineArray) Match(lineN int) highlight.LineMatch {
return la.lines[lineN].match
}
func (la *LineArray) Rehighlight(lineN int) bool {
return la.lines[lineN].rehighlight
}
func (la *LineArray) SetRehighlight(lineN int, on bool) {
la.lines[lineN].rehighlight = on
}

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

View File

@@ -1,61 +1,58 @@
package main
package buffer
// FromCharPos converts from a character position to an x, y position
func FromCharPos(loc int, buf *Buffer) Loc {
charNum := 0
x, y := 0, 0
import (
"unicode/utf8"
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
}
// 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 >= buf.NumLines || pos.X < 0 || pos.X > Count(buf.Line(pos.Y)) {
return false
}
return true
}
// 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
}
"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 Diff(a, b Loc, buf *Buffer) int {
func DiffLA(a, b Loc, buf *LineArray) int {
if a.Y == b.Y {
if a.X > b.X {
return a.X - b.X
@@ -71,69 +68,19 @@ func Diff(a, b Loc, buf *Buffer) int {
loc := 0
for i := a.Y + 1; i < b.Y; i++ {
// + 1 for the newline
loc += Count(buf.Line(i)) + 1
loc += utf8.RuneCount(buf.LineBytes(i)) + 1
}
loc += Count(buf.Line(a.Y)) - a.X + b.X + 1
loc += utf8.RuneCount(buf.LineBytes(a.Y)) - a.X + b.X + 1
return loc
}
// 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 {
func (l Loc) right(buf *LineArray) Loc {
if l == buf.End() {
return Loc{l.X + 1, l.Y}
}
var res Loc
if l.X < Count(buf.Line(l.Y)) {
if l.X < utf8.RuneCount(buf.LineBytes(l.Y)) {
res = Loc{l.X + 1, l.Y}
} else {
res = Loc{0, l.Y + 1}
@@ -142,7 +89,7 @@ func (l Loc) right(buf *Buffer) Loc {
}
// This moves the given location one character to the left
func (l Loc) left(buf *Buffer) Loc {
func (l Loc) left(buf *LineArray) Loc {
if l == buf.Start() {
return Loc{l.X - 1, l.Y}
}
@@ -150,22 +97,41 @@ func (l Loc) left(buf *Buffer) Loc {
if l.X > 0 {
res = Loc{l.X - 1, l.Y}
} else {
res = Loc{Count(buf.Line(l.Y - 1)), l.Y - 1}
res = Loc{utf8.RuneCount(buf.LineBytes(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 {
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 < Abs(n); i++ {
for i := 0; i < util.Abs(n); i++ {
l = l.left(buf)
}
return l
}
func (l Loc) Diff(a, b Loc, buf *Buffer) int {
return DiffLA(a, b, buf.LineArray)
}
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
}

View File

@@ -0,0 +1,77 @@
package buffer
import (
"github.com/zyedidia/micro/internal/config"
"github.com/zyedidia/tcell"
)
type MsgType int
const (
MTInfo = iota
MTWarning
MTError
)
type Message struct {
Msg string
Start, End Loc
Kind MsgType
Owner string
}
func NewMessage(owner string, msg string, start, end Loc, kind MsgType) *Message {
return &Message{
Msg: msg,
Start: start,
End: end,
Kind: kind,
Owner: owner,
}
}
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)
}

228
internal/buffer/save.go Normal file
View File

@@ -0,0 +1,228 @@
package buffer
import (
"bytes"
"errors"
"io"
"os"
"os/exec"
"os/signal"
"path/filepath"
"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) (err error) {
var file *os.File
if file, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
return
}
defer func() {
if e := file.Close(); e != nil && err == nil {
err = e
}
}()
w := transform.NewWriter(file, enc.NewEncoder())
// w := bufio.NewWriter(file)
if err = fn(w); err != nil {
return
}
// err = w.Flush()
return
}
// overwriteFileAsRoot executes dd as root and then calls the supplied function
// with dd's standard input as an io.Writer object. Dd opens the given file for writing,
// truncating it if it exists, and writes what it receives on its standard input to the file.
func overwriteFileAsRoot(name string, enc encoding.Encoding, fn func(io.Writer) error) (err error) {
cmd := exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "status=none", "bs=4K", "of="+name)
var stdin io.WriteCloser
screenb := screen.TempFini()
// 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()
}
}()
if stdin, err = cmd.StdinPipe(); err != nil {
return
}
if err = cmd.Start(); err != nil {
return
}
e := fn(stdin)
if err = stdin.Close(); err != nil {
return
}
if err = cmd.Wait(); err != nil {
return
}
screen.TempStart(screenb)
return e
}
// 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")
}
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 - 1, end.Y}) != '\n' {
b.Insert(end, "\n")
}
}
// Update the last time this file was updated after saving
defer func() {
b.ModTime, _ = GetModTime(filename)
err = b.Serialize()
}()
// Removes any tilde and replaces with the absolute path to home
absFilename, _ := 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 withSudo {
err = overwriteFileAsRoot(absFilename, enc, fwriter)
} else {
err = overwriteFile(absFilename, enc, fwriter)
}
if 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
}

170
internal/buffer/search.go Normal file
View File

@@ -0,0 +1,170 @@
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
}
found := false
var l [2]Loc
if down {
l, found = b.findDown(r, from, end)
if !found {
l, found = b.findDown(r, start, from)
}
} else {
l, found = b.findUp(r, from, start)
if !found {
l, found = b.findUp(r, end, from)
}
}
return l, found, nil
}
// ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area
// and returns the number of replacements made
func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte) int {
if start.GreaterThan(end) {
start, end = end, start
}
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 {
found++
return replace
})
from := Loc{charpos, i}
to := Loc{charpos + utf8.RuneCount(l), i}
deltas = append(deltas, Delta{newText, from, to})
}
b.MultipleReplace(deltas)
return found
}

View File

@@ -0,0 +1,74 @@
package buffer
import (
"encoding/gob"
"errors"
"io"
"os"
"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 := config.ConfigDir + "/buffers/" + 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
})
}
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(config.ConfigDir + "/buffers/" + 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 store the information for the 'saveundo' and 'savecursor' options) if this problem persists.")
}
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
}

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

View File

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

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

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

View File

@@ -1,7 +1,7 @@
package main
package config
import (
"fmt"
"errors"
"regexp"
"strconv"
"strings"
@@ -9,15 +9,15 @@ 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
// GetColor takes in a syntax group and returns the colorscheme's style for that group
func GetColor(color string) tcell.Style {
st := defStyle
st := DefStyle
if color == "" {
return st
}
@@ -29,11 +29,11 @@ func GetColor(color string) tcell.Style {
curGroup += "."
}
curGroup += g
if style, ok := colorscheme[curGroup]; ok {
if style, ok := Colorscheme[curGroup]; ok {
st = style
}
}
} else if style, ok := colorscheme[color]; ok {
} else if style, ok := Colorscheme[color]; ok {
st = style
} else {
st = StringToStyle(color)
@@ -48,47 +48,46 @@ func ColorschemeExists(colorschemeName string) bool {
}
// InitColorscheme picks and initializes the colorscheme when micro starts
func InitColorscheme() {
colorscheme = make(Colorscheme)
defStyle = tcell.StyleDefault.
Foreground(tcell.ColorDefault).
Background(tcell.ColorDefault)
if screen != nil {
// screen.SetStyle(defStyle)
}
func InitColorscheme() error {
Colorscheme = make(map[string]tcell.Style)
DefStyle = tcell.StyleDefault
LoadDefaultColorscheme()
return LoadDefaultColorscheme()
}
// LoadDefaultColorscheme loads the default colorscheme from $(configDir)/colorschemes
func LoadDefaultColorscheme() {
LoadColorscheme(globalSettings["colorscheme"].(string))
// 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 string) {
func LoadColorscheme(colorschemeName string) error {
file := FindRuntimeFile(RTColorscheme, colorschemeName)
if file == nil {
TermMessage(colorschemeName, "is not a valid colorscheme")
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 {
if data, err := file.Data(); err != nil {
TermMessage("Error loading colorscheme:", err)
} else {
colorscheme = ParseColorscheme(string(data))
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) == "" ||
@@ -106,17 +105,14 @@ func ParseColorscheme(text string) Colorscheme {
c[link] = style
if link == "default" {
defStyle = style
}
if screen != nil {
// screen.SetStyle(defStyle)
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
@@ -141,17 +137,17 @@ func StringToStyle(str string) tcell.Style {
var fgColor, bgColor tcell.Color
if fg == "" {
fgColor, _, _ = defStyle.Decompose()
fgColor, _, _ = DefStyle.Decompose()
} else {
fgColor = StringToColor(fg)
}
if bg == "" {
_, bgColor, _ = defStyle.Decompose()
_, bgColor, _ = DefStyle.Decompose()
} else {
bgColor = StringToColor(bg)
}
style := defStyle.Foreground(fgColor).Background(bgColor)
style := DefStyle.Foreground(fgColor).Background(bgColor)
if strings.Contains(str, "bold") {
style = style.Bold(true)
}

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

54
internal/config/config.go Normal file
View File

@@ -0,0 +1,54 @@
package config
import (
"errors"
"os"
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
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")
}
xdgHome = home + "/.config"
}
ConfigDir = xdgHome + "/micro"
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
}
}
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 {
return errors.New("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 {
return errors.New("Error creating configuration directory: " + err.Error())
}
}
return e
}

View 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

131
internal/config/plugin.go Normal file
View File

@@ -0,0 +1,131 @@
package config
import (
"errors"
lua "github.com/yuin/gopher-lua"
ulua "github.com/zyedidia/micro/internal/lua"
)
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 {
reterr = errors.New(p.Name + "." + fn + " should return a boolean")
} else {
retbool = retbool && bool(v)
}
}
return retbool, reterr
}
type Plugin struct {
Name string // name of plugin
Info *PluginInfo // json file containing info
Srcs []RuntimeFile // lua files
Loaded bool
Default bool // pre-installed plugin
}
func (p *Plugin) IsEnabled() bool {
if v, ok := GlobalSettings[p.Name]; ok {
return v.(bool) && p.Loaded
}
return true
}
var Plugins []*Plugin
func (p *Plugin) Load() error {
for _, f := range p.Srcs {
if v, ok := GlobalSettings[p.Name]; ok && !v.(bool) {
return nil
}
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
}
func (p *Plugin) Call(fn string, args ...lua.LValue) (lua.LValue, error) {
plug := ulua.L.GetGlobal(p.Name)
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
}
func FindPlugin(name string) *Plugin {
var pl *Plugin
for _, p := range Plugins {
if p.Name == name {
pl = p
break
}
}
return pl
}

View File

@@ -0,0 +1,66 @@
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")
ErrMissingInstall = errors.New("Missing or empty install field")
ErrMissingVstr = errors.New("Missing or empty versions field")
ErrMissingRequire = errors.New("Missing or empty require 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"`
Install string `json:"install"`
Vstr string `json:"version"`
Require []string `json:"require"`
}
// 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
}
// if len(info.Name) == 0 {
// return nil, ErrMissingName
// } else if len(info.Desc) == 0 {
// return nil, ErrMissingDesc
// } else if len(info.Site) == 0 {
// return nil, ErrMissingSite
// } else if len(info.Install) == 0 {
// return nil, ErrMissingInstall
// } else if len(info.Vstr) == 0 {
// return nil, ErrMissingVstr
// } else if len(info.Require) == 0 {
// return nil, ErrMissingRequire
// }
return &info, nil
}

View File

@@ -1,19 +1,25 @@
package main
package config
import (
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"regexp"
"strings"
)
const (
RTColorscheme = "colorscheme"
RTSyntax = "syntax"
RTHelp = "help"
RTPlugin = "plugin"
RTColorscheme = 0
RTSyntax = 1
RTHelp = 2
RTPlugin = 3
NumTypes = 4 // How many filetypes are there
)
type RTFiletype byte
// 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
@@ -23,7 +29,7 @@ type RuntimeFile interface {
}
// allFiles contains all available files, mapped by filetype
var allFiles map[string][]RuntimeFile
var allFiles [NumTypes][]RuntimeFile
// some file on filesystem
type realFile string
@@ -73,16 +79,13 @@ func (nf namedFile) Name() string {
}
// AddRuntimeFile registers a file for the given filetype
func AddRuntimeFile(fileType string, file RuntimeFile) {
if allFiles == nil {
allFiles = make(map[string][]RuntimeFile)
}
func AddRuntimeFile(fileType RTFiletype, file RuntimeFile) {
allFiles[fileType] = append(allFiles[fileType], file)
}
// AddRuntimeFilesFromDirectory registers each file from the given directory for
// the filetype which matches the file-pattern
func AddRuntimeFilesFromDirectory(fileType, directory, pattern string) {
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 {
@@ -94,7 +97,7 @@ func AddRuntimeFilesFromDirectory(fileType, directory, pattern string) {
// AddRuntimeFilesFromAssets registers each file from the given asset-directory for
// the filetype which matches the file-pattern
func AddRuntimeFilesFromAssets(fileType, directory, pattern string) {
func AddRuntimeFilesFromAssets(fileType RTFiletype, directory, pattern string) {
files, err := AssetDir(directory)
if err != nil {
return
@@ -108,7 +111,7 @@ func AddRuntimeFilesFromAssets(fileType, directory, pattern string) {
// FindRuntimeFile finds a runtime file of the given filetype and name
// will return nil if no file was found
func FindRuntimeFile(fileType, name string) RuntimeFile {
func FindRuntimeFile(fileType RTFiletype, name string) RuntimeFile {
for _, f := range ListRuntimeFiles(fileType) {
if f.Name() == name {
return f
@@ -118,17 +121,14 @@ func FindRuntimeFile(fileType, name string) RuntimeFile {
}
// ListRuntimeFiles lists all known runtime files for the given filetype
func ListRuntimeFiles(fileType string) []RuntimeFile {
if files, ok := allFiles[fileType]; ok {
return files
}
return []RuntimeFile{}
func ListRuntimeFiles(fileType RTFiletype) []RuntimeFile {
return allFiles[fileType]
}
// InitRuntimeFiles initializes all assets file and the config directory
func InitRuntimeFiles() {
add := func(fileType, dir, pattern string) {
AddRuntimeFilesFromDirectory(fileType, filepath.Join(configDir, dir), pattern)
add := func(fileType RTFiletype, dir, pattern string) {
AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern)
AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern)
}
@@ -136,31 +136,77 @@ func InitRuntimeFiles() {
add(RTSyntax, "syntax", "*.yaml")
add(RTHelp, "help", "*.md")
// Search configDir for plugin-scripts
files, _ := ioutil.ReadDir(filepath.Join(configDir, "plugins"))
for _, f := range files {
realpath, _ := filepath.EvalSymlinks(filepath.Join(configDir, "plugins", f.Name()))
realpathStat, _ := os.Stat(realpath)
if realpathStat.IsDir() {
scriptPath := filepath.Join(configDir, "plugins", f.Name(), f.Name()+".lua")
if _, err := os.Stat(scriptPath); err == nil {
AddRuntimeFile(RTPlugin, realFile(scriptPath))
initlua := filepath.Join(ConfigDir, "init.lua")
if _, err := os.Stat(initlua); !os.IsNotExist(err) {
p := new(Plugin)
p.Name = "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()
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 f.Name() == "info.json" {
data, err := ioutil.ReadFile(filepath.Join(plugdir, d.Name(), "info.json"))
if err != nil {
continue
}
p.Info, _ = NewPluginInfo(data)
p.Name = p.Info.Name
}
}
if !isID(p.Name) {
log.Println("Invalid plugin name", p.Name)
continue
}
Plugins = append(Plugins, p)
}
}
if files, err := AssetDir("runtime/plugins"); err == nil {
for _, f := range files {
scriptPath := path.Join("runtime/plugins", f, f+".lua")
if _, err := AssetInfo(scriptPath); err == nil {
AddRuntimeFile(RTPlugin, assetFile(scriptPath))
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.Default = true
for _, f := range srcs {
if strings.HasSuffix(f, ".lua") {
p.Srcs = append(p.Srcs, assetFile(filepath.Join(plugdir, d, f)))
} else if f == "info.json" {
data, err := Asset(filepath.Join(plugdir, d, "info.json"))
if err != nil {
continue
}
p.Info, _ = NewPluginInfo(data)
p.Name = p.Info.Name
}
}
if !isID(p.Name) {
log.Println("Invalid plugin name", p.Name)
continue
}
Plugins = append(Plugins, p)
}
}
}
}
// PluginReadRuntimeFile allows plugin scripts to read the content of a runtime file
func PluginReadRuntimeFile(fileType, name string) string {
func PluginReadRuntimeFile(fileType RTFiletype, name string) string {
if file := FindRuntimeFile(fileType, name); file != nil {
if data, err := file.Data(); err == nil {
return string(data)
@@ -170,7 +216,7 @@ func PluginReadRuntimeFile(fileType, name string) string {
}
// PluginListRuntimeFiles allows plugins to lists all runtime files of the given type
func PluginListRuntimeFiles(fileType string) []string {
func PluginListRuntimeFiles(fileType RTFiletype) []string {
files := ListRuntimeFiles(fileType)
result := make([]string, len(files))
for i, f := range files {
@@ -180,8 +226,8 @@ func PluginListRuntimeFiles(fileType string) []string {
}
// PluginAddRuntimeFile adds a file to the runtime files for a plugin
func PluginAddRuntimeFile(plugin, filetype, filePath string) {
fullpath := filepath.Join(configDir, "plugins", plugin, filePath)
func PluginAddRuntimeFile(plugin string, filetype RTFiletype, filePath string) {
fullpath := filepath.Join(ConfigDir, "plug", plugin, filePath)
if _, err := os.Stat(fullpath); err == nil {
AddRuntimeFile(filetype, realFile(fullpath))
} else {
@@ -191,8 +237,8 @@ func PluginAddRuntimeFile(plugin, filetype, filePath string) {
}
// PluginAddRuntimeFilesFromDirectory adds files from a directory to the runtime files for a plugin
func PluginAddRuntimeFilesFromDirectory(plugin, filetype, directory, pattern string) {
fullpath := filepath.Join(configDir, "plugins", plugin, directory)
func PluginAddRuntimeFilesFromDirectory(plugin string, filetype RTFiletype, directory, pattern string) {
fullpath := filepath.Join(ConfigDir, "plug", plugin, directory)
if _, err := os.Stat(fullpath); err == nil {
AddRuntimeFilesFromDirectory(filetype, fullpath, pattern)
} else {
@@ -202,6 +248,6 @@ func PluginAddRuntimeFilesFromDirectory(plugin, filetype, directory, pattern str
}
// PluginAddRuntimeFileFromMemory adds a file to the runtime files for a plugin from a given string
func PluginAddRuntimeFileFromMemory(plugin, filetype, filename, data string) {
func PluginAddRuntimeFileFromMemory(plugin string, filetype RTFiletype, filename, data string) {
AddRuntimeFile(filetype, memoryFile{filename, []byte(data)})
}

Some files were not shown because too many files have changed in this diff Show More