Compare commits

...

232 Commits

Author SHA1 Message Date
Zachary Yedidia
c04a4ba604 Minor update 2016-10-24 08:03:00 -04:00
Zachary Yedidia
63ccbc1ebd Add eofnewline option
Closes #429

Enable with '> set eofnewline on'
2016-10-23 18:37:29 -04:00
Zachary Yedidia
ee553b7830 Add reload command
Closes #427
2016-10-21 11:51:36 -04:00
Zachary Yedidia
97fc52093f Add website to readme 2016-10-21 10:57:37 -04:00
Zachary Yedidia
49397039e0 Update runtime 2016-10-19 10:34:09 -04:00
Zachary Yedidia
efe1ab5db6 Merge pull request #425 from adrianvoica/master
Updated TypeScript with all the reserved words and new types
2016-10-19 07:26:49 -04:00
Zachary Yedidia
daeffdc81b Merge pull request #423 from ulrichSchreiner/master
add additional Dockerfile keywords
2016-10-19 07:26:36 -04:00
Zachary Yedidia
30083c4d0f Merge pull request #424 from ulrichSchreiner/yaml-highlighter
highlight yaml dicts as types
2016-10-19 07:26:23 -04:00
Adrian Voica
56e616d5bf Updated TypeScript with all the reserved words and new types 2016-10-19 11:54:49 +03:00
Ulrich Schreiner
112da0b8c6 highlight yaml dicts as types 2016-10-19 09:35:03 +02:00
Ulrich Schreiner
163a3993bd add additional Dockerfile keywords 2016-10-19 06:34:50 +02:00
Zachary Yedidia
1b9bb31dd6 Cleanup and add more comments 2016-10-18 11:12:28 -04:00
Zachary Yedidia
8db3b22411 Merge 2016-10-18 08:58:31 -04:00
Zachary Yedidia
4aae5ca451 Fix dockerfile syntax file
Fixes #421
2016-10-18 08:58:09 -04:00
Zachary Yedidia
d3a3b7a8cd Merge pull request #417 from jncraton/outdent-line
Added OutdentLine action
2016-10-16 09:55:24 -04:00
Jon Craton
cc9342df9d Added OutdentLine action 2016-10-15 12:47:15 -04:00
Jon Craton
fe0dce0960 Added IndentString method on Buffer (#415)
* Added IndentString function to retrun the string used for indentation (n-spaces or a tab) based on buffer settings

* Combined redundant  statements

* Removed duplicate leading whitespace check

* Better IndentString description

* Fixed remainder logic that I broke
2016-10-15 10:09:20 -04:00
Zachary Yedidia
766f836952 Merge pull request #416 from jncraton/duplicate-selection
DuplicateLine duplicates current selection if there is text selected
2016-10-15 10:09:08 -04:00
Jon Craton
78b0aac5ec DuplicateLine now duplicates the current selection if there is text selected 2016-10-14 22:22:48 -04:00
Jon Craton
690627a338 Refactored IndentSelection and OutdentSelection to remove duplicate code (#414)
* Refactored indent selection

* Refactored OutdentSelection

* Refactored to use x and y instead of line and j
2016-10-14 16:52:55 -04:00
Zachary Yedidia
25ced4c075 Merge pull request #412 from ilius/pr04.keybindings_help_fixes
Fixes in keybindings.md
2016-10-14 07:37:45 -04:00
Zachary Yedidia
771b5333aa Merge pull request #411 from zenlc2000/master
Reworded first sentence to make it clearer.
2016-10-14 07:35:19 -04:00
Saeed Rasooli
ae72608c5d Bugfix: keybindings.md: fix bad json syntax, due to #407 2016-10-14 14:29:58 +03:30
Saeed Rasooli
2e778a2a8e update keybindings.md due to PR #409 2016-10-14 14:29:58 +03:30
zenlc2000
bc9e811797 Reworded first sentence to make it clearer. 2016-10-13 23:10:37 -06:00
Zachary Yedidia
4db7f33eaf More fixes to search and replace 2016-10-13 20:47:33 -04:00
Zachary Yedidia
d3c5e3ab47 Improvements for softwrap mouse support 2016-10-13 17:09:15 -04:00
Zachary Yedidia
b13c6c4892 Fix problem with regexes in search and replace
Fixes #410
2016-10-13 14:59:57 -04:00
Zachary Yedidia
c50dda244b Fix mouse support with soft wrap 2016-10-13 14:26:45 -04:00
Zachary Yedidia
3fdc2ca0da Always use the selection as search term when using quick search 2016-10-13 12:12:55 -04:00
Zachary Yedidia
6b7ca3c559 Merge pull request #409 from ilius/pr02.improve_search_escape
Improve Search behaviour, and Escape key behaviour
2016-10-13 12:10:11 -04:00
Zachary Yedidia
5c2a2b1b7e Fix problem with horizontal scrolling 2016-10-12 22:05:24 -04:00
Zachary Yedidia
69e45f9a4f Fix problem causing hsplits not to display 2016-10-12 22:03:16 -04:00
Saeed Rasooli
127ebc15b9 Improvement: improve Search behaviour, and Escape key behaviour 2016-10-13 00:49:43 +03:30
Zachary Yedidia
ea1de18326 Add docs 2016-10-12 16:34:34 -04:00
Zachary Yedidia
edd25c68ee Fix glitch with bottomline when softwrap is disabled 2016-10-12 16:30:32 -04:00
Zachary Yedidia
e30a4139e6 Add softwrap 2016-10-12 16:24:00 -04:00
Saeed Rasooli
546acfd21d Fixes in last PR: MoveLinesUp and MoveLinesDown (#408)
* Bugfix: fix panic in MoveLinesUp when moving up the *last* line

* Bugfix: don't panic in Buffer.Line if index is out or range

* clean MoveLinesDown since it won't work for the last line anyway, add comment

* Cleanup: replace spaces with tabs in MoveLinesUp and MoveLinesDown
2016-10-12 11:38:44 -04:00
Zachary Yedidia
d27690b8c6 Merge 2016-10-12 14:47:40 +00:00
Zachary Yedidia
adc56e60fc Use build-date.go in cross compilation script 2016-10-12 14:47:29 +00:00
Zachary Yedidia
266ce5c43b Merge pull request #407 from ilius/pr01.move_up_down
Feature: add MoveLinesUp (Alt + Up) and MoveLinesDown (Alt + Down) actions
2016-10-12 09:51:30 -04:00
Saeed Rasooli
0bf07eadcc Improvement: move MoveLinesUp and MoveLinesDown to Buffer
enables Undo/Redo with EventHandler, #407
2016-10-12 08:15:46 +03:30
Saeed Rasooli
e4386d9398 add help for MoveLinesUp and MoveLinesDown 2016-10-12 08:15:46 +03:30
Saeed Rasooli
c1dd403ab9 Feature: add MoveLinesUp (Alt + Up) and MoveLinesDown (Alt + Down) actions 2016-10-12 08:15:46 +03:30
Zachary Yedidia
0e4f700527 Update installation instructions 2016-10-11 18:21:06 -04:00
Zachary Yedidia
a48c991958 Return 0.0.0-unknown version if building without a git repo 2016-10-11 15:25:39 -04:00
Zachary Yedidia
cbc250b7d0 Improve Makefile
Now you can use 'make update' which will update micro and all the
dependencies (but won't rebuild). The makefile also now supports
having a $GOBIN variable and having multiple directories in your
$GOPATH.
2016-10-11 11:07:53 -04:00
Zachary Yedidia
b27ef219a0 Update readme installation instructions 2016-10-11 09:34:49 -04:00
Zachary Yedidia
d163637fa8 Update docs 2016-10-11 09:13:03 -04:00
Zachary Yedidia
905d4d7020 Make monokai the default colorscheme
Monokai is a better default colorscheme because it has a better 16
color approximation than zenburn. On 16 color terminals, it looks like
zenburn is not syntax highlighting anything.
2016-10-11 09:09:56 -04:00
Zachary Yedidia
f85dd77036 Merge 2016-10-10 21:44:48 -04:00
Zachary Yedidia
8f5f8ffdd6 Fix tabstop sizing with mix of tabs and spaces
Fixes #404
2016-10-10 21:44:16 -04:00
Zachary Yedidia
b09093f78c Merge 2016-10-10 18:40:48 -04:00
Zachary Yedidia
104699e500 Use default foreground for empty indent chars
Fixes #403
2016-10-10 18:40:21 -04:00
Zachary Yedidia
38bf8c0225 Temporary fix for plugin panic
Fixes #402
2016-10-07 20:34:03 -04:00
Zachary Yedidia
6acda994e4 Update docs and readme 2016-10-06 20:36:37 -04:00
Zachary Yedidia
e563211790 Make linter a default plugin once again 2016-10-06 20:28:10 -04:00
Zachary Yedidia
6a5879cc15 Improve binary size by stripping more aggressively 2016-10-06 17:45:28 -04:00
Zachary Yedidia
aa624d86e6 Move linter and go plugins to their own repos
The linter and go plugins are no longer 'default'. Their installation
should be handled by the plugin manager: `> plugin install go` and
`> plugin install linter`.

The autoclose plugin will remain a default plugin because it provides
a more essential feature.

Closes #397
2016-10-06 17:18:53 -04:00
Zachary Yedidia
c410b7b2ce Improve plugin manager error feedback 2016-10-06 13:39:57 -04:00
Zachary Yedidia
79f1539486 Merge pull request #396 from boombuler/help
updated plugin help
2016-10-06 07:22:25 -04:00
Florian Sundermann
d7b7cc954a updated plugin help 2016-10-06 08:24:39 +02:00
Zachary Yedidia
1914a5b5ff Update readme 2016-10-05 18:28:08 -04:00
Zachary Yedidia
921b828afb Add some documentation about plugin manager 2016-10-05 18:26:41 -04:00
Zachary Yedidia
d3d35bd9ff Add more descriptive error messages for plugin installation failures
Ref #378
2016-10-05 18:00:05 -04:00
Zachary Yedidia
76a328a062 Use official plugin channel
Use the channel for official plugins from
https://github.com/micro-editor/plugin-channel

Ref #378
2016-10-05 17:57:03 -04:00
Zachary Yedidia
fb90e169cb Only allow one package per repository
This may be temporary.

Ref #378
2016-10-05 17:52:39 -04:00
Zachary Yedidia
a1a307d858 Merge pull request #378 from boombuler/pm
Plugin-Manager
2016-10-05 17:51:22 -04:00
Zachary Yedidia
3733e7e223 Add 'Unsplit' action and VSplit and HSplit actions
This commit adds the 'Unsplit' action used to close all splits except
the current one.

It also adds the 'VSplit' and 'HSplit' actions which open empty
vertical/horizontal splits so you can bind them to keys.

Closes #228
2016-10-04 11:08:32 -04:00
boombuler
3e8a587aa3 changed json5 repo 2016-10-02 07:57:39 +02:00
boombuler
8f2f1f8c1d skip core dependencies if micro was build with an unknown version. 2016-10-01 09:28:48 +02:00
boombuler
a940ce3036 allow user to set plugin channels / repos in settings.json 2016-10-01 08:37:04 +02:00
boombuler
d7da72a720 fix plugin zips which contain a root directory 2016-10-01 08:05:05 +02:00
boombuler
b54853140a new command plugin list
this command shows all currently installed plugins and their verion
2016-10-01 07:37:20 +02:00
boombuler
8ad2179423 Merge remote-tracking branch 'zyedidia/master' into pm 2016-10-01 07:20:21 +02:00
Zachary Yedidia
3037d72bcb Fix more tabnum issues
Fixes #395
2016-09-30 07:29:24 -04:00
Zachary Yedidia
7d16e97b95 Switch to my fork of json5
This should reduce go get download times for micro considerably
because the original json5 committed a bunch of binaries which
cause the repository to be very large and slow to download.

My fork fixes that.
2016-09-29 18:43:10 -04:00
Zachary Yedidia
0293b774f3 Fix SplitTree tab index
Fixes #392
2016-09-29 14:23:25 -04:00
Zachary Yedidia
32cd94b88f Minor optimization to tabsize fix 2016-09-28 18:08:06 -04:00
Zachary Yedidia
5e5dd78b7c Merge pull request #387 from boombuler/bug379
fixes #379 (second try)
2016-09-28 18:06:18 -04:00
Zachary Yedidia
1c5c741e87 Make sure /Users/zachary/gocode/bin exists before putting binary there 2016-09-28 17:59:40 -04:00
Zachary Yedidia
095e6993a8 Merge pull request #389 from dsnet/master
fix offset calculation for column ruler
2016-09-28 17:57:43 -04:00
Joe Tsai
7c3425a012 fix offset calculation for column ruler
The calculation for the column ruler index should:
* include the offset for the line numbers gutter
* not include the leftmost column since ruler should scroll with the pane

Fixes #379
2016-09-28 13:40:48 -07:00
boombuler
bc724bf781 fixes #379 (second try) 2016-09-28 21:54:34 +02:00
Zachary Yedidia
13144d4b57 Merge pull request #386 from zyedidia/revert-382-bug379
Revert "fixes #379"
2016-09-28 14:07:29 -04:00
Zachary Yedidia
97bdb15bd6 Revert "fixes #379" 2016-09-28 14:07:17 -04:00
Zachary Yedidia
fb69ecdc9b Add 'autosave' option
Closes #278
2016-09-28 13:07:05 -04:00
boombuler
1fe1c3eabb improved plugin search 2016-09-28 18:31:05 +02:00
Zachary Yedidia
191fd5e495 Merge pull request #382 from boombuler/bug379
fixes #379
2016-09-28 12:30:01 -04:00
boombuler
8aa017bfda autocomplete plugin commands 2016-09-28 18:15:39 +02:00
boombuler
9ea947c808 improved logging 2016-09-28 18:00:12 +02:00
boombuler
2a7a55eca4 better plugin search 2016-09-28 17:55:44 +02:00
boombuler
759c00098b Merge remote-tracking branch 'zyedidia/master' into pm 2016-09-28 17:36:37 +02:00
Florian Sundermann
cce36624dc PM should not install already installed plugins. 2016-09-28 16:34:28 +02:00
Zachary Yedidia
4664850186 Merge pull request #384 from boombuler/logview
don't use undo / redo history for log buffer.
2016-09-28 10:24:30 -04:00
Florian Sundermann
d9c666f6df don't use undo / redo history for log buffer. 2016-09-28 15:47:31 +02:00
Florian Sundermann
d7e38a52ea fixes #379
when tabstospaces is off tabs were always treated as
as a number of spaces not as tabs with tabstops.
2016-09-28 08:12:19 +02:00
boombuler
f3f4790103 simple plugin search 2016-09-27 21:25:57 +02:00
boombuler
83c1136ac5 Merge remote-tracking branch 'zyedidia/master' into pm 2016-09-27 20:57:49 +02:00
Zachary Yedidia
0ae5ae5d9a HSplit log, and update docs 2016-09-27 14:29:55 -04:00
Zachary Yedidia
c070e3e8f7 Merge pull request #381 from boombuler/logview
Log View
2016-09-27 14:28:06 -04:00
Zachary Yedidia
0de167b07b Add new plugin runtime function 2016-09-27 14:24:52 -04:00
boombuler
f904e2fe99 always scroll log to the cursor befor drawing and don't ask for save changes for help and log views 2016-09-27 17:52:40 +02:00
boombuler
b195ebad46 AddLog should be "public" accessible 2016-09-27 17:52:05 +02:00
Florian Sundermann
23ef69b935 change pluginmanager json to json5 2016-09-27 13:28:32 +02:00
Florian Sundermann
55c790f069 more tolerant version parsing 2016-09-27 13:26:11 +02:00
Florian Sundermann
4bcb13efc0 try to set a more matching version number 2016-09-27 13:25:17 +02:00
boombuler
50c7441533 also add TermMessage output to log 2016-09-26 19:28:42 +02:00
boombuler
c1a3ee1706 possibility to show a log view 2016-09-26 19:08:37 +02:00
boombuler
357fc09e69 Merge remote-tracking branch 'zyedidia/master' into pm 2016-09-26 18:24:43 +02:00
Zachary Yedidia
c1d08a6dc0 Fix typo 2016-09-26 12:08:35 -04:00
boombuler
f689143670 fixed tests 2016-09-26 17:51:50 +02:00
boombuler
56b3b79c50 removed testing code 2016-09-26 17:37:53 +02:00
Florian Sundermann
f351c251e4 first few pm commands 2016-09-26 16:53:39 +02:00
Zachary Yedidia
5cc66cef42 Fix problems recognizing CtrlH
Fixes #368

The 'Backspace2' key has been renamed to 'Backspace'.
2016-09-26 09:34:55 -04:00
Florian Sundermann
6791759440 Merge remote-tracking branch 'zyedidia/master' into pm 2016-09-26 12:49:57 +02:00
Zachary Yedidia
ac98f21199 Merge pull request #377 from onodera-punpun/patch-2
Add ` to autoclose
2016-09-25 13:32:37 -04:00
Camille
22ebbcfd89 Add ` to autoclose 2016-09-25 19:05:58 +02:00
Zachary Yedidia
292df7a9f7 Add mouse support and binding support to prompts
Closes #244
2016-09-24 15:26:19 -04:00
Zachary Yedidia
64fd96611c Check buffer filetype after loading plugins 2016-09-24 14:30:35 -04:00
Zachary Yedidia
de4a007bdf Merge pull request #371 from boombuler/plugins
Handle Plugins via RT-Files
2016-09-24 14:09:02 -04:00
boombuler
567faeb07e initial commit of pluginmanager 2016-09-23 10:03:42 +02:00
Zachary Yedidia
3afb3d0b22 Merge pull request #370 from boombuler/snippets_core
Snippets core
2016-09-20 08:58:22 -04:00
Florian Sundermann
8172ebf62b fixed loading order
plugins were not able to provide colorschemes
2016-09-19 16:04:59 +02:00
Florian Sundermann
1720d4023f load plugins as rt-files 2016-09-19 14:40:56 +02:00
Florian Sundermann
da6ab78384 fixed build 2016-09-19 13:28:14 +02:00
Florian Sundermann
6fe20fb305 some additions to the plugin API
Those changes were originally used for the snippet plugin which
may not be part of the core.
2016-09-19 13:23:47 +02:00
Zachary Yedidia
d41f0bb324 Merge 2016-09-18 09:30:28 -04:00
Zachary Yedidia
8e555e60f7 Inherit background color from default
Fixes #366
2016-09-18 09:29:58 -04:00
Zachary Yedidia
243f99aeb1 Add function to load runtime files from a directory for a plugin 2016-09-16 16:15:44 -04:00
Zachary Yedidia
ab36db7646 Update yaml header 2016-09-16 15:22:38 -04:00
Zachary Yedidia
2e3c87b67d Add quick start guide to help.md 2016-09-16 12:14:08 -04:00
Zachary Yedidia
a549d12808 Merge pull request #334 from techtonik/filemanagers
Usability integration with file managers
2016-09-16 12:09:17 -04:00
Zachary Yedidia
149fea8b76 Allow plugins to add their own runtime files 2016-09-16 11:02:10 -04:00
Zachary Yedidia
f7295a25d8 Merge pull request #363 from boombuler/rtfiles
Runtime files
2016-09-16 10:33:28 -04:00
Florian Sundermann
9eeb14956c allow plugins to list / read runtime files
also renamed most of the new functions to be
more specific about what kind of files this is for.
2016-09-15 16:42:45 +02:00
Florian Sundermann
796638d095 simplified file handling for runtime files 2016-09-15 15:50:26 +02:00
Zachary Yedidia
79621505f1 Merge pull request #359 from boombuler/params
Params
2016-09-14 17:14:01 -04:00
Zachary Yedidia
e484445b1e Merge pull request #354 from boombuler/help
Help
2016-09-14 17:13:46 -04:00
Zachary Yedidia
5eddba5516 Merge pull request #360 from rgburke/validate-options
Added ability to validate options values when being set
2016-09-14 15:34:18 -04:00
Richard Burke
bdc857952a Added ability to validate options values when being set 2016-09-14 20:06:48 +01:00
Zachary Yedidia
2bcc59faea Fix pyflakes linter pattern
Fixes #358
2016-09-14 12:54:31 -04:00
Florian Sundermann
6cc12b871c include trailing path delimiter
"C:" is not valid on windows but "C:\" is.
"foo" is as valid as "foo/" on other OS...
2016-09-14 16:28:25 +02:00
Florian Sundermann
d201e7c503 fixed directory completion on windows 2016-09-14 16:15:49 +02:00
Zachary Yedidia
c695df0adf Merge pull request #356 from boombuler/bug355
fixed bug 355
2016-09-14 07:29:44 -04:00
Zachary Yedidia
a04e3080fb Merge pull request #357 from boombuler/emptyfiles
don't reuse unsaved file buffers.
2016-09-14 07:28:52 -04:00
boombuler
7d395a29a7 don't clone unsaved file buffers.
if I open multiple empty tabs, I don't want the same "new file buffers"
2016-09-14 09:37:12 +02:00
boombuler
4046bb977e fixed bug 355 2016-09-14 09:24:38 +02:00
boombuler
d250b9d7b0 allow plugins to have a help file 2016-09-13 09:06:06 +02:00
boombuler
a7f159bddc Load help files when needed 2016-09-13 08:53:20 +02:00
Zachary Yedidia
d0fa467a3c Revert "Improve performance for very long lines"
This reverts commit d5694c0f35.

Fixes #351
See #348
2016-09-11 16:05:15 -04:00
anatoly techtonik
f4e0a3c0f8 Fix comma and tabs in markdown help 2016-09-11 22:40:02 +03:00
Zachary Yedidia
0bc80adc28 Fix strange selection for long lines
See #351
2016-09-11 15:00:44 -04:00
Zachary Yedidia
cfdaf0e3f6 Merge pull request #316 from elopio/snapcraft
Add the packaging metadata to build the micro snap
2016-09-10 15:43:39 -04:00
Zachary Yedidia
b5160c5d2c Optimize search and replace a lot 2016-09-10 11:32:54 -04:00
Zachary Yedidia
210a538cdd Improve performance for xml and html files 2016-09-10 10:32:21 -04:00
Zachary Yedidia
fd786b3020 Allow a buffer to be opened simultaneously 2016-09-10 10:30:15 -04:00
Zachary Yedidia
ba9560079c Merge 2016-09-10 10:03:55 -04:00
Zachary Yedidia
d5694c0f35 Improve performance for very long lines 2016-09-10 10:03:51 -04:00
Nickolay
1522b24803 Okay. Bugfix with syntax :P (#339)
* Now it can install micro into /usr/bin (make-build only, unix-only)

* Fixed Syntax Bugging

* Update xml.micro

* Revert Makefile change

* Update xml.micro
2016-09-10 09:18:14 -04:00
Zachary Yedidia
922baa930d Add eval command 2016-09-09 15:54:32 -04:00
Zachary Yedidia
faafda6b21 Remove pkgbuild from pre installed syntax files 2016-09-09 14:48:18 -04:00
Zachary Yedidia
9efc4fb5e9 Remove duplicate PKGBUILD filetype 2016-09-09 12:41:56 -04:00
Zachary Yedidia
37d83a280f Merge 2016-09-09 12:32:05 -04:00
Zachary Yedidia
8c0544c264 Use shell.micro for PKGBUILD files
Fixes #345
2016-09-09 12:31:44 -04:00
Zachary Yedidia
5e6a26a6ea Merge pull request #344 from apjanke/doco-tweaks
doco: grammar and formatting tweaks
2016-09-09 07:43:15 -04:00
Andrew Janke
9a09647330 doco: grammar and formatting tweaks 2016-09-09 00:07:58 -04:00
Zachary Yedidia
0c00e8da0e Merge pull request #341 from boombuler/bug297
fixes #297
2016-09-08 17:31:45 -04:00
Zachary Yedidia
af47cce86b Resize tabs more often
Fixes #343
2016-09-08 17:30:41 -04:00
Zachary Yedidia
301e86a46e Add SaveAs action and command
Fixes #340

You can bind the action `SaveAs` and if you provide an argument to
the `save` command it will save as. For example `> save test.txt`.
2016-09-08 14:13:46 -04:00
Florian Sundermann
0b1afe7f6c fixes #297
use a buffered channel to queue events.
otherwise those events might get lost.
2016-09-08 13:47:13 +02:00
Zachary Yedidia
1739f0631a Merge pull request #338 from akkatracker/patch-1
its --> it's in actions.go comment
2016-09-08 07:22:03 -04:00
Matthew Brener
4f4e87a82c its --> it's in actions.go comment 2016-09-08 16:14:04 +10:00
Zachary Yedidia
49ec611c8f Update readme 2016-09-07 20:28:18 -04:00
Zachary Yedidia
8f06e51170 Add colorcolumn option
Fixes #333

For example: `> set colorcolumn 80`.
2016-09-07 17:17:51 -04:00
anatoly techtonik
a853164302 Usability integration with file managers 2016-09-08 00:11:51 +03:00
Zachary Yedidia
dc8207149b Merge branch 'to-miz-master' 2016-09-07 16:57:58 -04:00
Zachary Yedidia
cc73efc0bd Merge branch 'master' of https://github.com/to-miz/micro into to-miz-master 2016-09-07 16:57:42 -04:00
Zachary Yedidia
4992efd172 Merge pull request #332 from nueh/master
Consider all multi-byte characters a "wordchar", fixes #329
2016-09-07 12:10:24 -04:00
Niklas Hennigs
956b1bdb3a Missed the test for the last commit, now fixed 2016-09-07 17:52:16 +02:00
Niklas Hennigs
cd52aaba13 This fixes zyedidia/micro#329
Now all multi-byte characters are considered a wordchar
2016-09-07 17:29:38 +02:00
Zachary Yedidia
cc6189b7ce Check colorschemes from ~/.config/micro/colorschemes
Fixes #331
2016-09-07 07:25:42 -04:00
Zachary Yedidia
5e82fc4673 Update readme 2016-09-06 20:33:39 -04:00
Zachary Yedidia
107a6f877b Update readme 2016-09-06 20:32:21 -04:00
Zachary Yedidia
e643860e3d Add Open command for view 2016-09-06 19:58:34 -04:00
Zachary Yedidia
0373589ab8 Merge 2016-09-06 19:30:28 -04:00
Zachary Yedidia
dce56a2b85 Have HandleShellCommand return the stdout
HandleShellCommand will now return the stdout as a string and
it also takes an additional flag indicating whether it should
wait before closing the shell and returning to the editor.
2016-09-06 19:27:57 -04:00
Zachary Yedidia
27d2ebfb45 Merge pull request #325 from techtonik/patch-1
Fix CanClose comment after API change
2016-09-06 16:02:45 -04:00
anatoly techtonik
1f457f9d9e Fix CanClose comment after API change
Follow up to 966dac97f8
2016-09-06 22:51:13 +03:00
Zachary Yedidia
c0282c4a3c Add issue template 2016-09-06 11:24:55 -04:00
Zachary Yedidia
0a6e1de404 Merge branch 'boombuler-params' 2016-09-06 10:59:46 -04:00
Zachary Yedidia
131524e670 Merge branch 'params' of https://github.com/boombuler/micro into boombuler-params 2016-09-06 10:59:30 -04:00
Zachary Yedidia
539495d2f7 Add support for macros
Closes #270

CtrlU to toggle recording and CtrlJ to playback.
You can also rebind using the "ToggleMacro" and "PlayMacro"
actions.

Note that recursive macros are not yet supported.
2016-09-06 10:44:15 -04:00
Zachary Yedidia
966dac97f8 Make unsaved changes prompt more clear
Fixes #301
2016-09-06 10:06:36 -04:00
Zachary Yedidia
bf6e596808 Merge pull request #323 from zonuexe/php-keywords
Add PHP Keywords
2016-09-06 07:24:22 -04:00
USAMI Kenta
efa24f5a8d Add PHP Keywords
- trait (PHP 5.5)
- iterable (PHP 7.1)
- void (PHP 7.1)

see https://github.com/ejmr/php-mode/pull/317
2016-09-06 18:25:08 +09:00
Zachary Yedidia
432146b068 Merge pull request #318 from elopio/update_runtime
Update runtime
2016-09-05 16:12:26 -04:00
Leo Arias
02b6eaaff0 Update runtime
closes #317
2016-09-05 18:31:36 +00:00
Leo Arias
e7ee18e421 Add the packaging metadata to build the micro snap 2016-09-05 17:17:59 +00:00
Zachary Yedidia
dc532e337b Merge pull request #315 from boombuler/pascal
added pascal syntax
2016-09-05 12:10:56 -04:00
boombuler
a952e249b4 added pascal syntax 2016-09-05 17:53:49 +02:00
Zachary Yedidia
94465ef1ae Merge 2016-09-05 11:38:16 -04:00
Zachary Yedidia
0a534767f0 Merge branch 'primary-clipboard' 2016-09-05 11:37:49 -04:00
Zachary Yedidia
7937f7038b Merge pull request #314 from mame98/master
Added some SCSS keywords for the css/scss highlighting
2016-09-05 10:50:00 -04:00
Marius Messerschmidt
d6a01ad29f Added some SCSS keywords for the css/scss highlighting
Signed-off-by: Marius Messerschmidt <marius.messerschmidt@googlemail.com>
2016-09-05 16:41:32 +02:00
Zachary Yedidia
b1cb583e8c Update runtime 2016-09-05 10:30:08 -04:00
Zachary Yedidia
ea4d822923 Merge pull request #283 from boombuler/autocompleteplugin
Autocomplete for plugins
2016-09-05 10:29:51 -04:00
Zachary Yedidia
583177feff Update readme 2016-09-05 10:29:15 -04:00
Zachary Yedidia
3bec1b8c1b Merge pull request #313 from adrianvoica/master
Updates to TypeScript dictionary.
2016-09-05 10:28:36 -04:00
Adrian Voica
b992669f5b Updates to TypeScript dictionary. 2016-09-05 17:18:41 +03:00
Zachary Yedidia
086aa61e5a Merge pull request #311 from adrianvoica/master
Added TypeScript syntax highlighting file.
2016-09-05 10:09:23 -04:00
to-miz
725533d991 fixed inserting runes that require ctrl+alt
we check wheter an input is a binding first, only if it is not a binding
do we insert the rune regardless of modifiers
2016-09-05 16:03:05 +02:00
Adrian Voica
c310053777 Added TypeScript syntax highlighting file. 2016-09-05 17:01:37 +03:00
Zachary Yedidia
2041e12eba Fix some issues with mouse selection copying 2016-09-05 08:36:30 -04:00
Zachary Yedidia
5d00522d4e Deselect for CursorStart
Fixes #308
2016-09-05 08:25:49 -04:00
Zachary Yedidia
c71e816e37 Fix recursive function 2016-09-04 21:28:40 -04:00
Zachary Yedidia
6721ec8e7d Copy to primary clipboard for any change in selection 2016-09-04 21:19:14 -04:00
Zachary Yedidia
93eadfb9dc Merge 2016-09-04 18:27:16 -04:00
Zachary Yedidia
cf3ce29a08 Fix YesNoPrompt bug 2016-09-04 18:27:11 -04:00
Zachary Yedidia
cd1117c08c Merge pull request #304 from techtonik/windate
Windows-compatible Makefile
2016-09-04 15:17:27 -04:00
anatoly techtonik
f247823936 Get build date on Windows without Python 2016-09-04 22:07:07 +03:00
anatoly techtonik
4b350d02e0 Let Go choose binary name and extension
This creates micro.exe on Windows
2016-09-04 19:38:50 +03:00
anatoly techtonik
ae3696e82d Use Python to get date in cross-platform way
I was able to build micro with Mozilla's pymake on Windows
2016-09-04 19:31:16 +03:00
boombuler
403a99d2ea removed obsolete replace command preparations 2016-09-03 12:13:25 +02:00
boombuler
b2735d7b5b use build-in functions to quote / unquote 2016-09-03 12:02:49 +02:00
boombuler
699ad316e5 removed old code 2016-09-03 08:18:47 +02:00
boombuler
8617ae5c1f keep trailing space at commandline 2016-09-03 08:16:18 +02:00
boombuler
c5ac5be764 updated plugin help 2016-09-02 19:50:19 +02:00
boombuler
881f57b047 allow plugins to register autocomplete functions 2016-09-02 19:42:33 +02:00
Zachary Yedidia
89c34ed8b3 Copy to primary clipboard on mouse selection 2016-09-02 10:44:32 -04:00
Zachary Yedidia
d9b8a04841 Add support for primary clipboard 2016-09-02 09:40:08 -04:00
Florian Sundermann
ccfe08bc60 allow command parameters to be quoted
this way FileCompletions could contain space runes without
breaking the parameter.
2016-09-02 13:44:22 +02:00
61 changed files with 3109 additions and 983 deletions

9
.github/ISSUE_TEMPLATE vendored Normal file
View File

@@ -0,0 +1,9 @@
## Description of the problem or steps to reproduce
## Specifications
You can use `micro -version` to get the commit hash.
Commit hash:
OS:
Terminal:

View File

@@ -1,42 +1,45 @@
.PHONY: runtime
VERSION = $(shell git describe --tags --abbrev=0)
VERSION = $(shell go run tools/build-version.go)
HASH = $(shell git rev-parse --short HEAD)
DATE = $(shell go run tools/build-date.go)
GOBIN ?= $($GOPATH)/bin
# Builds micro after checking dependencies but without updating the runtime
build: deps tcell
go build -ldflags "-X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(shell date -u '+%B %d, %Y')'" -o micro ./cmd/micro
build: deps
go build -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)'" ./cmd/micro
# Builds micro after building the runtime and checking dependencies
build-all: runtime build
# Builds micro without checking for dependencies
build-quick:
go build -ldflags "-X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(shell date -u '+%B %d, %Y')'" -o micro ./cmd/micro
go build -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)'" ./cmd/micro
# Same as 'build' but installs to $GOPATH/bin afterward
install: build
mv micro $(GOPATH)/bin
# Same as 'build' but installs to $GOBIN afterward
install: deps
go install -ldflags "-s -w -X main.Version=$(VERSION) -X main.CommitHash=$(HASH) -X 'main.CompileDate=$(DATE)'" ./cmd/micro
# Same as 'build-all' but installs to $GOPATH/bin afterward
# Same as 'build-all' but installs to $GOBIN afterward
install-all: runtime install
# Same as 'build-quick' but installs to $GOPATH/bin afterward
install-quick: build-quick
mv micro $(GOPATH)/bin
# Updates tcell
tcell:
git -C $(GOPATH)/src/github.com/zyedidia/tcell pull
# 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)'" ./cmd/micro
# Checks for dependencies
deps:
go get -d ./cmd/micro
update:
git pull
go get -u -d ./cmd/micro
# Builds the runtime
runtime:
go get -u github.com/jteeuwen/go-bindata/...
$(GOPATH)/bin/go-bindata -nometadata -o runtime.go runtime/...
$(GOBIN)/go-bindata -nometadata -o runtime.go runtime/...
mv runtime.go cmd/micro
test:

View File

@@ -17,6 +17,8 @@ Here is a picture of micro editing its source code.
To see more screenshots of micro, showcasing all of the default colorschemes, see [here](http://zbyedidia.webfactional.com/micro/screenshots.html).
You can also check out the website for Micro at https://micro-editor.github.io.
# Features
* Easy to use and to install
@@ -31,16 +33,20 @@ To see more screenshots of micro, showcasing all of the default colorschemes, se
* Cross platform (It should work on all the platforms Go runs on)
* Note that while Windows is supported, there are still some bugs that need to be worked out
* Plugin system (plugins are written in Lua)
* Micro has a built-in plugin manager to automatically install, remove, and update all your plugins
* Persistent undo
* Automatic linting and error notifications
* Syntax highlighting (for over [75 languages](runtime/syntax)!)
* Colorscheme support
* By default, micro comes with 16, 256, and true color themes.
* True color support (set the `MICRO_TRUECOLOR` env variable to 1 to enable it)
* Snippets
* The snippet plugin can be installed with `> plugin install snippets`
* Copy and paste with the system clipboard
* Small and simple
* Easily configurable
* Common editor things such as undo/redo, line numbers, unicode support...
* Macros
* Common editor things such as undo/redo, line numbers, Unicode support, softwrap...
Although not yet implemented, I hope to add more features such as autocompletion ([#174](https://github.com/zyedidia/micro/issues/174)), and multiple cursors ([#5](https://github.com/zyedidia/micro/issues/5)) in the future.
@@ -48,7 +54,7 @@ Although not yet implemented, I hope to add more features such as autocompletion
To install micro, you can download a [prebuilt binary](https://github.com/zyedidia/micro/releases), or you can build it from source.
If you want more information about ways to install micro, see this [wiki page](https://github.com/zyedidia/micro/wiki/Installing-Micro)
If you want more information about ways to install micro, see this [wiki page](https://github.com/zyedidia/micro/wiki/Installing-Micro).
### Prebuilt binaries
@@ -61,19 +67,33 @@ and you'll see all the stable releases with the corresponding binaries.
If you'd like to see more information after installing micro, run `micro -version`.
### Homebrew
You can also install micro using Homebrew on Mac:
```
brew install micro
```
### Building from source
If your operating system does not have binary, but does run Go, you can build from source.
If your operating system does not have a binary release, but does run Go, you can build from source.
Make sure that you have Go version 1.5 or greater (Go 1.4 will work if your version supports CGO).
Make sure that you have Go version 1.5 or greater (Go 1.4 will work if your version supports CGO) and that your `GOPATH` env variable is set (I recommand setting it to `~/go` if you don't have one).
```sh
go get -u github.com/zyedidia/micro/...
```
go get -d github.com/zyedidia/micro
cd $GOPATH/src/github.com/zyedidia/micro
make install
```
The binary will then be installed to `$GOPATH/bin` (or your `$GOBIN`).
You can install directly with `go get` (`go get -u github.com/zyedidia/micro/cmd/micro`) but this isn't recommended because it doesn't build micro with version information which is useful for the plugin manager.
### Linux clipboard support
On Linux, clipboard support requires 'xclip' or 'xsel' command to be installed.
On Linux, clipboard support requires the 'xclip' or 'xsel' commands to be installed.
For Ubuntu:
@@ -89,7 +109,7 @@ If you open micro and it doesn't seem like syntax highlighting is working, this
you are using a terminal which does not support 256 color. Try changing the colorscheme to `simple`
by running `> set colorscheme simple`.
If you are using the default ubuntu terminal, to enable 256 make sure your `TERM` variable is set
If you are using the default Ubuntu terminal, to enable 256 make sure your `TERM` variable is set
to `xterm-256color`.
Many of the Windows terminals don't support more than 16 colors, which means
@@ -121,7 +141,7 @@ click to enable line selection.
# Documentation and Help
Micro has a built-in help system which you can access by pressing `CtrlE` and typing `help`. Additionally, you can
Micro has a built-in help system which you can access by pressing `Ctrl-E` and typing `help`. Additionally, you can
view the help files here:
* [main help](https://github.com/zyedidia/micro/tree/master/runtime/help/help.md)
@@ -138,4 +158,6 @@ a brief introduction to the more powerful configuration features micro offers.
If you find any bugs, please report them! I am also happy to accept pull requests from anyone.
You can use the Github issue tracker to report bugs, ask questions, or suggest new features.
You can use the [GitHub issue tracker](https://github.com/zyedidia/micro/issues) to report bugs, ask questions, or suggest new features.
For a more informal setting to discuss the editor, you can join the [Gitter chat](https://gitter.im/zyedidia/micro).

View File

@@ -1,13 +1,11 @@
package main
import (
"io/ioutil"
"os"
"strconv"
"strings"
"time"
"github.com/mitchellh/go-homedir"
"github.com/yuin/gopher-lua"
"github.com/zyedidia/clipboard"
)
@@ -361,6 +359,8 @@ func (v *View) CursorStart(usePlugin bool) bool {
return false
}
v.deselect(0)
v.Cursor.X = 0
v.Cursor.Y = 0
@@ -494,7 +494,7 @@ func (v *View) Backspace(usePlugin bool) bool {
// and restore the position
// If the user is using spaces instead of tabs and they are deleting
// whitespace at the start of the line, we should delete as if its a
// whitespace at the start of the line, we should delete as if it's a
// tab (tabSize number of spaces)
lineStart := v.Buf.Line(v.Cursor.Y)[:v.Cursor.X]
tabSize := int(v.Buf.Settings["tabsize"].(float64))
@@ -587,31 +587,17 @@ func (v *View) IndentSelection(usePlugin bool) bool {
}
if v.Cursor.HasSelection() {
start := v.Cursor.CurSelection[0].Y
end := v.Cursor.CurSelection[1].Move(-1, v.Buf).Y
startY := v.Cursor.CurSelection[0].Y
endY := v.Cursor.CurSelection[1].Move(-1, v.Buf).Y
endX := v.Cursor.CurSelection[1].Move(-1, v.Buf).X
for i := start; i <= end; i++ {
if v.Buf.Settings["tabstospaces"].(bool) {
tabsize := int(v.Buf.Settings["tabsize"].(float64))
v.Buf.Insert(Loc{0, i}, Spaces(tabsize))
if i == start {
if v.Cursor.CurSelection[0].X > 0 {
v.Cursor.CurSelection[0] = v.Cursor.CurSelection[0].Move(tabsize, v.Buf)
}
}
if i == end {
v.Cursor.CurSelection[1] = Loc{endX + tabsize + 1, end}
}
} else {
v.Buf.Insert(Loc{0, i}, "\t")
if i == start {
if v.Cursor.CurSelection[0].X > 0 {
v.Cursor.CurSelection[0] = v.Cursor.CurSelection[0].Move(1, v.Buf)
}
}
if i == end {
v.Cursor.CurSelection[1] = Loc{endX + 2, end}
}
for y := startY; y <= endY; y++ {
tabsize := len(v.Buf.IndentString())
v.Buf.Insert(Loc{0, y}, v.Buf.IndentString())
if y == startY && v.Cursor.CurSelection[0].X > 0 {
v.Cursor.SetSelectionStart(v.Cursor.CurSelection[0].Move(tabsize, v.Buf))
}
if y == endY {
v.Cursor.SetSelectionEnd(Loc{endX + tabsize + 1, endY})
}
}
v.Cursor.Relocate()
@@ -624,6 +610,31 @@ func (v *View) IndentSelection(usePlugin bool) bool {
return false
}
// OutdentLine moves the current line back one indentation
func (v *View) OutdentLine(usePlugin bool) bool {
if usePlugin && !PreActionCall("OutdentLine", v) {
return false
}
if v.Cursor.HasSelection() {
return false
}
for x := 0; x < len(v.Buf.IndentString()); x++ {
if len(GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y))) == 0 {
break
}
v.Buf.Remove(Loc{0, v.Cursor.Y}, Loc{1, v.Cursor.Y})
v.Cursor.X -= 1
}
v.Cursor.Relocate()
if usePlugin {
return PostActionCall("OutdentLine", v)
}
return true
}
// OutdentSelection takes the current selection and moves it back one indent level
func (v *View) OutdentSelection(usePlugin bool) bool {
if usePlugin && !PreActionCall("OutdentSelection", v) {
@@ -631,37 +642,20 @@ func (v *View) OutdentSelection(usePlugin bool) bool {
}
if v.Cursor.HasSelection() {
start := v.Cursor.CurSelection[0].Y
end := v.Cursor.CurSelection[1].Move(-1, v.Buf).Y
startY := v.Cursor.CurSelection[0].Y
endY := v.Cursor.CurSelection[1].Move(-1, v.Buf).Y
endX := v.Cursor.CurSelection[1].Move(-1, v.Buf).X
for i := start; i <= end; i++ {
if len(GetLeadingWhitespace(v.Buf.Line(i))) > 0 {
if v.Buf.Settings["tabstospaces"].(bool) {
tabsize := int(v.Buf.Settings["tabsize"].(float64))
for j := 0; j < tabsize; j++ {
if len(GetLeadingWhitespace(v.Buf.Line(i))) == 0 {
break
}
v.Buf.Remove(Loc{0, i}, Loc{1, i})
if i == start {
if v.Cursor.CurSelection[0].X > 0 {
v.Cursor.CurSelection[0] = v.Cursor.CurSelection[0].Move(-1, v.Buf)
}
}
if i == end {
v.Cursor.CurSelection[1] = Loc{endX - j, end}
}
}
} else {
v.Buf.Remove(Loc{0, i}, Loc{1, i})
if i == start {
if v.Cursor.CurSelection[0].X > 0 {
v.Cursor.CurSelection[0] = v.Cursor.CurSelection[0].Move(-1, v.Buf)
}
}
if i == end {
v.Cursor.CurSelection[1] = Loc{endX, end}
}
for y := startY; y <= endY; y++ {
for x := 0; x < len(v.Buf.IndentString()); x++ {
if len(GetLeadingWhitespace(v.Buf.Line(y))) == 0 {
break
}
v.Buf.Remove(Loc{0, y}, Loc{1, y})
if y == startY && v.Cursor.CurSelection[0].X > 0 {
v.Cursor.SetSelectionStart(v.Cursor.CurSelection[0].Move(-1, v.Buf))
}
if y == endY {
v.Cursor.SetSelectionEnd(Loc{endX - x, endY})
}
}
}
@@ -684,18 +678,11 @@ func (v *View) InsertTab(usePlugin bool) bool {
if v.Cursor.HasSelection() {
return false
}
// Insert a tab
if v.Buf.Settings["tabstospaces"].(bool) {
tabSize := int(v.Buf.Settings["tabsize"].(float64))
if remainder := v.Cursor.Loc.X % tabSize; remainder != 0 {
tabSize = tabSize - remainder
}
v.Buf.Insert(v.Cursor.Loc, Spaces(tabSize))
for i := 0; i < tabSize; i++ {
v.Cursor.Right()
}
} else {
v.Buf.Insert(v.Cursor.Loc, "\t")
tabBytes := len(v.Buf.IndentString())
bytesUntilIndent := tabBytes - (v.Cursor.GetVisualX() % tabBytes)
v.Buf.Insert(v.Cursor.Loc, v.Buf.IndentString()[:bytesUntilIndent])
for i := 0; i < bytesUntilIndent; i++ {
v.Cursor.Right()
}
@@ -711,7 +698,7 @@ func (v *View) Save(usePlugin bool) bool {
return false
}
if v.Help {
if v.Type == vtHelp {
// We can't save the help text
return false
}
@@ -719,6 +706,8 @@ func (v *View) Save(usePlugin bool) bool {
if v.Buf.Path == "" {
filename, canceled := messenger.Prompt("Filename: ", "Save", NoCompletion)
if !canceled {
// the filename might or might not be quoted, so unquote first then join the strings.
filename = strings.Join(SplitCommandArgs(filename), " ")
v.Buf.Path = filename
v.Buf.Name = filename
} else {
@@ -752,18 +741,36 @@ func (v *View) Save(usePlugin bool) bool {
return false
}
// SaveAs saves the buffer to disk with the given name
func (v *View) SaveAs(usePlugin bool) bool {
filename, canceled := messenger.Prompt("Filename: ", "Save", NoCompletion)
if !canceled {
// the filename might or might not be quoted, so unquote first then join the strings.
filename = strings.Join(SplitCommandArgs(filename), " ")
v.Buf.Path = filename
v.Buf.Name = filename
v.Save(true)
}
return false
}
// Find opens a prompt and searches forward for the input
func (v *View) Find(usePlugin bool) bool {
if usePlugin && !PreActionCall("Find", v) {
return false
}
searchStr := ""
if v.Cursor.HasSelection() {
searchStart = ToCharPos(v.Cursor.CurSelection[1], v.Buf)
searchStart = ToCharPos(v.Cursor.CurSelection[1], v.Buf)
searchStr = v.Cursor.GetSelection()
} else {
searchStart = ToCharPos(v.Cursor.Loc, v.Buf)
}
BeginSearch()
BeginSearch(searchStr)
if usePlugin {
return PostActionCall("Find", v)
@@ -779,9 +786,13 @@ func (v *View) FindNext(usePlugin bool) bool {
if v.Cursor.HasSelection() {
searchStart = ToCharPos(v.Cursor.CurSelection[1], v.Buf)
lastSearch = v.Cursor.GetSelection()
} else {
searchStart = ToCharPos(v.Cursor.Loc, v.Buf)
}
if lastSearch == "" {
return true
}
messenger.Message("Finding: " + lastSearch)
Search(lastSearch, v, true)
@@ -848,7 +859,7 @@ func (v *View) Copy(usePlugin bool) bool {
}
if v.Cursor.HasSelection() {
clipboard.WriteAll(v.Cursor.GetSelection())
clipboard.WriteAll(v.Cursor.GetSelection(), "clipboard")
v.freshClip = true
messenger.Message("Copied selection")
}
@@ -871,10 +882,10 @@ func (v *View) CutLine(usePlugin bool) bool {
}
if v.freshClip == true {
if v.Cursor.HasSelection() {
if clip, err := clipboard.ReadAll(); err != nil {
if clip, err := clipboard.ReadAll("clipboard"); err != nil {
messenger.Error(err)
} else {
clipboard.WriteAll(clip + v.Cursor.GetSelection())
clipboard.WriteAll(clip+v.Cursor.GetSelection(), "clipboard")
}
}
} else if time.Since(v.lastCutTime)/time.Second > 10*time.Second || v.freshClip == false {
@@ -899,7 +910,7 @@ func (v *View) Cut(usePlugin bool) bool {
}
if v.Cursor.HasSelection() {
clipboard.WriteAll(v.Cursor.GetSelection())
clipboard.WriteAll(v.Cursor.GetSelection(), "clipboard")
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
v.freshClip = true
@@ -914,15 +925,20 @@ func (v *View) Cut(usePlugin bool) bool {
return false
}
// DuplicateLine duplicates the current line
// DuplicateLine duplicates the current line or selection
func (v *View) DuplicateLine(usePlugin bool) bool {
if usePlugin && !PreActionCall("DuplicateLine", v) {
return false
}
v.Cursor.End()
v.Buf.Insert(v.Cursor.Loc, "\n"+v.Buf.Line(v.Cursor.Y))
v.Cursor.Right()
if v.Cursor.HasSelection() {
v.Buf.Insert(v.Cursor.CurSelection[1], v.Cursor.GetSelection())
} else {
v.Cursor.End()
v.Buf.Insert(v.Cursor.Loc, "\n"+v.Buf.Line(v.Cursor.Y))
v.Cursor.Right()
}
messenger.Message("Duplicated line")
if usePlugin {
@@ -951,6 +967,84 @@ func (v *View) DeleteLine(usePlugin bool) bool {
return true
}
// MoveLinesUp moves up the current line or selected lines if any
func (v *View) MoveLinesUp(usePlugin bool) bool {
if usePlugin && !PreActionCall("MoveLinesUp", v) {
return false
}
if v.Cursor.HasSelection() {
if v.Cursor.CurSelection[0].Y == 0 {
messenger.Message("Can not move further up")
return true
}
v.Buf.MoveLinesUp(
v.Cursor.CurSelection[0].Y,
v.Cursor.CurSelection[1].Y,
)
v.Cursor.UpN(1)
v.Cursor.CurSelection[0].Y -= 1
v.Cursor.CurSelection[1].Y -= 1
messenger.Message("Moved up selected line(s)")
} else {
if v.Cursor.Loc.Y == 0 {
messenger.Message("Can not move further up")
return true
}
v.Buf.MoveLinesUp(
v.Cursor.Loc.Y,
v.Cursor.Loc.Y+1,
)
v.Cursor.UpN(1)
messenger.Message("Moved up current line")
}
v.Buf.IsModified = true
if usePlugin {
return PostActionCall("MoveLinesUp", v)
}
return true
}
// MoveLinesDown moves down the current line or selected lines if any
func (v *View) MoveLinesDown(usePlugin bool) bool {
if usePlugin && !PreActionCall("MoveLinesDown", v) {
return false
}
if v.Cursor.HasSelection() {
if v.Cursor.CurSelection[1].Y >= len(v.Buf.lines) {
messenger.Message("Can not move further down")
return true
}
v.Buf.MoveLinesDown(
v.Cursor.CurSelection[0].Y,
v.Cursor.CurSelection[1].Y,
)
v.Cursor.DownN(1)
v.Cursor.CurSelection[0].Y += 1
v.Cursor.CurSelection[1].Y += 1
messenger.Message("Moved down selected line(s)")
} else {
if v.Cursor.Loc.Y >= len(v.Buf.lines)-1 {
messenger.Message("Can not move further down")
return true
}
v.Buf.MoveLinesDown(
v.Cursor.Loc.Y,
v.Cursor.Loc.Y+1,
)
v.Cursor.DownN(1)
messenger.Message("Moved down current line")
}
v.Buf.IsModified = true
if usePlugin {
return PostActionCall("MoveLinesDown", v)
}
return true
}
// Paste whatever is in the system clipboard into the buffer
// Delete and paste if the user has a selection
func (v *View) Paste(usePlugin bool) bool {
@@ -958,18 +1052,23 @@ func (v *View) Paste(usePlugin bool) bool {
return false
}
leadingWS := GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y))
clip, _ := clipboard.ReadAll("clipboard")
v.paste(clip)
if v.Cursor.HasSelection() {
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
if usePlugin {
return PostActionCall("Paste", v)
}
clip, _ := clipboard.ReadAll()
clip = strings.Replace(clip, "\n", "\n"+leadingWS, -1)
v.Buf.Insert(v.Cursor.Loc, clip)
v.Cursor.Loc = v.Cursor.Loc.Move(Count(clip), v.Buf)
v.freshClip = false
messenger.Message("Pasted clipboard")
return true
}
// PastePrimary pastes from the primary clipboard (only use on linux)
func (v *View) PastePrimary(usePlugin bool) bool {
if usePlugin && !PreActionCall("Paste", v) {
return false
}
clip, _ := clipboard.ReadAll("primary")
v.paste(clip)
if usePlugin {
return PostActionCall("Paste", v)
@@ -983,8 +1082,8 @@ func (v *View) SelectAll(usePlugin bool) bool {
return false
}
v.Cursor.CurSelection[0] = v.Buf.Start()
v.Cursor.CurSelection[1] = v.Buf.End()
v.Cursor.SetSelectionStart(v.Buf.Start())
v.Cursor.SetSelectionEnd(v.Buf.End())
// Put the cursor at the beginning
v.Cursor.X = 0
v.Cursor.Y = 0
@@ -1001,23 +1100,15 @@ func (v *View) OpenFile(usePlugin bool) bool {
return false
}
if v.CanClose("Continue? (y,n,s) ", 'y', 'n', 's') {
if v.CanClose() {
filename, canceled := messenger.Prompt("File to open: ", "Open", FileCompletion)
if canceled {
return false
}
home, _ := homedir.Dir()
filename = strings.Replace(filename, "~", home, 1)
file, err := ioutil.ReadFile(filename)
// the filename might or might not be quoted, so unquote first then join the strings.
filename = strings.Join(SplitCommandArgs(filename), " ")
var buf *Buffer
if err != nil {
// File does not exist -- create an empty buffer with that name
buf = NewBuffer([]byte{}, filename)
} else {
buf = NewBuffer(file, filename)
}
v.OpenBuffer(buf)
v.Open(filename)
if usePlugin {
return PostActionCall("OpenFile", v)
@@ -1244,7 +1335,7 @@ func (v *View) ToggleHelp(usePlugin bool) bool {
return false
}
if !v.Help {
if v.Type != vtHelp {
// Open the default help
v.openHelp("help")
} else {
@@ -1266,7 +1357,7 @@ func (v *View) ShellMode(usePlugin bool) bool {
input, canceled := messenger.Prompt("$ ", "Shell", NoCompletion)
if !canceled {
// The true here is for openTerm to make the command interactive
HandleShellCommand(input, true)
HandleShellCommand(input, true, true)
if usePlugin {
return PostActionCall("ShellMode", v)
}
@@ -1291,6 +1382,21 @@ func (v *View) CommandMode(usePlugin bool) bool {
return false
}
// Escape leaves current mode / quits the editor
func (v *View) Escape(usePlugin bool) bool {
// check if user is searching, or the last search is still active
if searching || lastSearch != "" {
ExitSearch(v)
return true
}
// check if a prompt is shown, hide it and don't quit
if messenger.hasPrompt {
messenger.Reset() // FIXME
return true
}
return v.Quit(usePlugin)
}
// Quit quits the editor
// This behavior needs to be changed and should really only quit the editor if this
// is the last view
@@ -1301,7 +1407,7 @@ func (v *View) Quit(usePlugin bool) bool {
}
// Make sure not to quit if there are unsaved changes
if v.CanClose("Quit anyway? (y,n,s) ", 'y', 'n', 's') {
if v.CanClose() {
v.CloseBuffer()
if len(tabs[curTab].views) > 1 {
v.splitNode.Delete()
@@ -1347,7 +1453,7 @@ func (v *View) QuitAll(usePlugin bool) bool {
closeAll := true
for _, tab := range tabs {
for _, v := range tab.views {
if !v.CanClose("Quit anyway? (y,n,s) ", 'y', 'n', 's') {
if !v.CanClose() {
closeAll = false
}
}
@@ -1431,6 +1537,55 @@ func (v *View) NextTab(usePlugin bool) bool {
return false
}
// VSplitBinding opens an empty vertical split
func (v *View) VSplitBinding(usePlugin bool) bool {
if usePlugin && !PreActionCall("VSplit", v) {
return false
}
v.VSplit(NewBuffer([]byte{}, ""))
if usePlugin {
return PostActionCall("VSplit", v)
}
return false
}
// HSplitBinding opens an empty horizontal split
func (v *View) HSplitBinding(usePlugin bool) bool {
if usePlugin && !PreActionCall("HSplit", v) {
return false
}
v.HSplit(NewBuffer([]byte{}, ""))
if usePlugin {
return PostActionCall("HSplit", v)
}
return false
}
// Unsplit closes all splits in the current tab except the active one
func (v *View) Unsplit(usePlugin bool) bool {
if usePlugin && !PreActionCall("Unsplit", v) {
return false
}
curView := tabs[curTab].curView
for i := len(tabs[curTab].views) - 1; i >= 0; i-- {
view := tabs[curTab].views[i]
if view != nil && view.Num != curView {
view.Quit(true)
// messenger.Message("Quit ", view.Buf.Path)
}
}
if usePlugin {
return PostActionCall("Unsplit", v)
}
return false
}
// NextSplit changes the view to the next split
func (v *View) NextSplit(usePlugin bool) bool {
if usePlugin && !PreActionCall("NextSplit", v) {
@@ -1469,6 +1624,64 @@ func (v *View) PreviousSplit(usePlugin bool) bool {
return false
}
var curMacro []interface{}
var recordingMacro bool
// ToggleMacro toggles recording of a macro
func (v *View) ToggleMacro(usePlugin bool) bool {
if usePlugin && !PreActionCall("ToggleMacro", v) {
return false
}
recordingMacro = !recordingMacro
if recordingMacro {
curMacro = []interface{}{}
messenger.Message("Recording")
} else {
messenger.Message("Stopped recording")
}
if usePlugin {
return PostActionCall("ToggleMacro", v)
}
return true
}
// PlayMacro plays back the most recently recorded macro
func (v *View) PlayMacro(usePlugin bool) bool {
if usePlugin && !PreActionCall("PlayMacro", v) {
return false
}
for _, action := range curMacro {
switch t := action.(type) {
case rune:
// Insert a character
if v.Cursor.HasSelection() {
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
}
v.Buf.Insert(v.Cursor.Loc, string(t))
v.Cursor.Right()
for _, pl := range loadedPlugins {
_, err := Call(pl+".onRune", string(t), v)
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
}
}
case func(*View, bool) bool:
t(v, true)
}
}
if usePlugin {
return PostActionCall("PlayMacro", v)
}
return true
}
// None is no action
func None() bool {
return false

View File

@@ -8,6 +8,8 @@ import (
"github.com/mitchellh/go-homedir"
)
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`.
@@ -16,12 +18,14 @@ import (
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 {
home, _ := homedir.Dir()
directories := strings.Join(dirs[:len(dirs)-1], sep)
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
if strings.HasPrefix(directories, "~") {
directories = strings.Replace(directories, "~", home, 1)
}
@@ -29,6 +33,7 @@ func FileComplete(input string) (string, []string) {
} else {
files, err = ioutil.ReadDir(".")
}
var suggestions []string
if err != nil {
return "", suggestions
@@ -79,9 +84,9 @@ func CommandComplete(input string) (string, []string) {
func HelpComplete(input string) (string, []string) {
var suggestions []string
for _, topic := range helpFiles {
for _, file := range ListRuntimeFiles(RTHelp) {
topic := file.Name()
if strings.HasPrefix(topic, input) {
suggestions = append(suggestions, topic)
}
}
@@ -123,3 +128,50 @@ func OptionComplete(input string) (string, []string) {
}
return chosen, suggestions
}
// MakeCompletion registeres 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
}
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
}
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

@@ -5,7 +5,7 @@ import (
"os"
"strings"
"github.com/yosuke-furukawa/json5/encoding/json5"
"github.com/zyedidia/json5/encoding/json5"
"github.com/zyedidia/tcell"
)
@@ -52,9 +52,13 @@ var bindingActions = map[string]func(*View, bool) bool{
"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,
@@ -71,6 +75,7 @@ var bindingActions = map[string]func(*View, bool) bool{
"ClearStatus": (*View).ClearStatus,
"ShellMode": (*View).ShellMode,
"CommandMode": (*View).CommandMode,
"Escape": (*View).Escape,
"Quit": (*View).Quit,
"QuitAll": (*View).QuitAll,
"AddTab": (*View).AddTab,
@@ -78,6 +83,11 @@ var bindingActions = map[string]func(*View, bool) bool{
"NextTab": (*View).NextTab,
"NextSplit": (*View).NextSplit,
"PreviousSplit": (*View).PreviousSplit,
"Unsplit": (*View).Unsplit,
"VSplit": (*View).VSplitBinding,
"HSplit": (*View).HSplitBinding,
"ToggleMacro": (*View).ToggleMacro,
"PlayMacro": (*View).PlayMacro,
// This was changed to InsertNewline but I don't want to break backwards compatibility
"InsertEnter": (*View).InsertNewline,
@@ -202,12 +212,11 @@ var bindingKeys = map[string]tcell.Key{
"CtrlRightSq": tcell.KeyCtrlRightSq,
"CtrlCarat": tcell.KeyCtrlCarat,
"CtrlUnderscore": tcell.KeyCtrlUnderscore,
"Backspace": tcell.KeyBackspace,
"Tab": tcell.KeyTab,
"Esc": tcell.KeyEsc,
"Escape": tcell.KeyEscape,
"Enter": tcell.KeyEnter,
"Backspace2": tcell.KeyBackspace2,
"Backspace": tcell.KeyBackspace2,
// I renamed these keys to PageUp and PageDown but I don't want to break someone's keybindings
"PgUp": tcell.KeyPgUp,
@@ -264,7 +273,8 @@ modSearch:
case strings.HasPrefix(k, "-"):
// We optionally support dashes between modifiers
k = k[1:]
case strings.HasPrefix(k, "Ctrl"):
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"):
@@ -358,6 +368,8 @@ func DefaultBindings() map[string]string {
"ShiftRight": "SelectRight",
"AltLeft": "WordLeft",
"AltRight": "WordRight",
"AltUp": "MoveLinesUp",
"AltDown": "MoveLinesDown",
"AltShiftRight": "SelectWordRight",
"AltShiftLeft": "SelectWordLeft",
"CtrlLeft": "StartOfLine",
@@ -370,12 +382,12 @@ func DefaultBindings() map[string]string {
"CtrlShiftDown": "SelectToEnd",
"Enter": "InsertNewline",
"Space": "InsertSpace",
"CtrlH": "Backspace",
"Backspace": "Backspace",
"Backspace2": "Backspace",
"Alt-CtrlH": "DeleteWordLeft",
"Alt-Backspace": "DeleteWordLeft",
"Alt-Backspace2": "DeleteWordLeft",
"Tab": "IndentSelection,InsertTab",
"Backtab": "OutdentSelection",
"Backtab": "OutdentSelection,OutdentLine",
"CtrlO": "OpenFile",
"CtrlS": "Save",
"CtrlF": "Find",
@@ -402,11 +414,12 @@ func DefaultBindings() map[string]string {
"CtrlR": "ToggleRuler",
"CtrlL": "JumpLine",
"Delete": "Delete",
"Esc": "ClearStatus",
"CtrlB": "ShellMode",
"CtrlQ": "Quit",
"CtrlE": "CommandMode",
"CtrlW": "NextSplit",
"CtrlU": "ToggleMacro",
"CtrlJ": "PlayMacro",
// Emacs-style keybindings
"Alt-f": "WordRight",
@@ -415,5 +428,13 @@ func DefaultBindings() map[string]string {
"Alt-e": "EndOfLine",
"Alt-p": "CursorUp",
"Alt-n": "CursorDown",
// Integration with file managers
"F1": "ToggleHelp",
"F2": "Save",
"F4": "Quit",
"F7": "Find",
"F10": "Quit",
"Esc": "Escape",
}
}

View File

@@ -55,6 +55,16 @@ type SerializedBuffer struct {
// NewBuffer creates a new buffer from `txt` with path and name `path`
func NewBuffer(txt []byte, 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(txt)
@@ -172,6 +182,14 @@ 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
@@ -251,7 +269,14 @@ func (b *Buffer) SaveAs(filename string) error {
b.UpdateRules()
b.Name = filename
b.Path = filename
data := []byte(b.String())
str := b.String()
if b.Settings["eofnewline"].(bool) {
end := b.End()
if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
b.Insert(end, "\n")
}
}
data := []byte(str)
err := ioutil.WriteFile(filename, data, 0644)
if err == nil {
b.IsModified = false
@@ -334,8 +359,20 @@ func (b *Buffer) End() Loc {
return Loc{utf8.RuneCount(b.lines[b.NumLines-1]), 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])
}
@@ -353,3 +390,48 @@ func (b *Buffer) Lines(start, end int) []string {
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]),
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},
)
}

View File

@@ -2,7 +2,6 @@ package main
import (
"fmt"
"io/ioutil"
"regexp"
"strconv"
"strings"
@@ -16,66 +15,53 @@ type Colorscheme map[string]tcell.Style
// The current colorscheme
var colorscheme Colorscheme
var preInstalledColors = []string{"default", "simple", "solarized", "solarized-tc", "atom-dark-tc", "monokai", "gruvbox", "zenburn", "bubblegum"}
// ColorschemeExists checks if a given colorscheme exists
func ColorschemeExists(colorschemeName string) bool {
files, _ := ioutil.ReadDir(configDir)
for _, f := range files {
if f.Name() == colorschemeName+".micro" {
return true
}
}
for _, name := range preInstalledColors {
if name == colorschemeName {
return true
}
}
return false
return FindRuntimeFile(RTColorscheme, colorschemeName) != nil
}
// InitColorscheme picks and initializes the colorscheme when micro starts
func InitColorscheme() {
colorscheme = make(Colorscheme)
if screen != nil {
screen.SetStyle(tcell.StyleDefault.
Foreground(tcell.ColorDefault).
Background(tcell.ColorDefault))
}
LoadDefaultColorscheme()
}
// LoadDefaultColorscheme loads the default colorscheme from $(configDir)/colorschemes
func LoadDefaultColorscheme() {
LoadColorscheme(globalSettings["colorscheme"].(string), configDir+"/colorschemes")
LoadColorscheme(globalSettings["colorscheme"].(string))
}
// LoadColorscheme loads the given colorscheme from a directory
func LoadColorscheme(colorschemeName, dir string) {
files, _ := ioutil.ReadDir(dir)
found := false
for _, f := range files {
if f.Name() == colorschemeName+".micro" {
text, err := ioutil.ReadFile(dir + "/" + f.Name())
if err != nil {
fmt.Println("Error loading colorscheme:", err)
continue
}
colorscheme = ParseColorscheme(string(text))
found = true
}
}
for _, name := range preInstalledColors {
if name == colorschemeName {
data, err := Asset("runtime/colorschemes/" + name + ".micro")
if err != nil {
TermMessage("Unable to load pre-installed colorscheme " + name)
continue
}
colorscheme = ParseColorscheme(string(data))
found = true
}
}
if !found {
func LoadColorscheme(colorschemeName string) {
file := FindRuntimeFile(RTColorscheme, colorschemeName)
if file == nil {
TermMessage(colorschemeName, "is not a valid colorscheme")
} else {
if data, err := file.Data(); err != nil {
fmt.Println("Error loading colorscheme:", err)
} else {
colorscheme = ParseColorscheme(string(data))
// Default style
defStyle = tcell.StyleDefault.
Foreground(tcell.ColorDefault).
Background(tcell.ColorDefault)
// There may be another default style defined in the colorscheme
// In that case we should use that one
if style, ok := colorscheme["default"]; ok {
defStyle = style
}
if screen != nil {
screen.SetStyle(defStyle)
}
}
}
}
@@ -102,7 +88,12 @@ func ParseColorscheme(text string) Colorscheme {
link := string(matches[1])
colors := string(matches[2])
c[link] = StringToStyle(colors)
style := StringToStyle(colors)
c[link] = style
if link == "default" {
defStyle = style
}
} else {
fmt.Println("Color-link statement is not valid:", line)
}
@@ -115,8 +106,7 @@ func ParseColorscheme(text string) Colorscheme {
// The strings must be in the format "extra foregroundcolor,backgroundcolor"
// The 'extra' can be bold, reverse, or underline
func StringToStyle(str string) tcell.Style {
var fg string
bg := "default"
var fg, bg string
split := strings.Split(str, ",")
if len(split) > 1 {
fg, bg = split[0], split[1]
@@ -126,7 +116,19 @@ func StringToStyle(str string) tcell.Style {
fg = strings.TrimSpace(fg)
bg = strings.TrimSpace(bg)
style := defStyle.Foreground(StringToColor(fg)).Background(StringToColor(bg))
var fgColor, bgColor tcell.Color
if fg == "" {
fgColor, _, _ = defStyle.Decompose()
} else {
fgColor = StringToColor(fg)
}
if bg == "" {
_, bgColor, _ = defStyle.Decompose()
} else {
bgColor = StringToColor(bg)
}
style := defStyle.Foreground(fgColor).Background(bgColor)
if strings.Contains(str, "bold") {
style = style.Bold(true)
}

View File

@@ -2,6 +2,8 @@ package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
@@ -24,19 +26,27 @@ type StrCommand struct {
var commands map[string]Command
var commandActions = map[string]func([]string){
"Set": Set,
"SetLocal": SetLocal,
"Show": Show,
"Run": Run,
"Bind": Bind,
"Quit": Quit,
"Save": Save,
"Replace": Replace,
"VSplit": VSplit,
"HSplit": HSplit,
"Tab": NewTab,
"Help": Help,
var commandActions map[string]func([]string)
func init() {
commandActions = map[string]func([]string){
"Set": Set,
"SetLocal": SetLocal,
"Show": Show,
"Run": Run,
"Bind": Bind,
"Quit": Quit,
"Save": Save,
"Replace": Replace,
"VSplit": VSplit,
"HSplit": HSplit,
"Tab": NewTab,
"Help": Help,
"Eval": Eval,
"ToggleLog": ToggleLog,
"Plugin": PluginCmd,
"Reload": Reload,
}
}
// InitCommands initializes the default commands
@@ -81,9 +91,103 @@ func DefaultCommands() map[string]StrCommand {
"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}},
}
}
// 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.
for _, lp := range loadedPlugins {
if lp == plugin {
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{})
}
}
}
} else {
messenger.Error("Not enough arguments")
}
}
func ToggleLog(args []string) {
buffer := messenger.getBuffer()
if CurView().Type != vtLog {
CurView().HSplit(buffer)
CurView().Type = vtLog
} else {
CurView().Quit(true)
}
}
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 {
@@ -91,7 +195,7 @@ func Help(args []string) {
CurView().openHelp("help")
} else {
helpPage := args[0]
if _, ok := helpPages[helpPage]; ok {
if FindRuntimeFile(RTHelp, helpPage) != nil {
CurView().openHelp(helpPage)
} else {
messenger.Error("Sorry, no help for ", helpPage)
@@ -143,6 +247,18 @@ func HSplit(args []string) {
}
}
// 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 {
@@ -225,7 +341,7 @@ func Bind(args []string) {
// Run runs a shell command in the background
func Run(args []string) {
// Run a shell command in the background (openTerm is false)
HandleShellCommand(strings.Join(args, " "), false)
HandleShellCommand(JoinCommandArgs(args...), false, true)
}
// Quit closes the main view
@@ -236,48 +352,32 @@ func Quit(args []string) {
// Save saves the buffer in the main view
func Save(args []string) {
// Save the main view
CurView().Save(true)
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) {
// This is a regex to parse the replace expression
// We allow no quotes if there are no spaces, but if you want to search
// for or replace an expression with spaces, you can add double quotes
r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|[^\s]*`)
replaceCmd := r.FindAllString(strings.Join(args, " "), -1)
if len(replaceCmd) < 2 {
if len(args) < 2 {
// We need to find both a search and replace expression
messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
return
}
var flags string
if len(replaceCmd) == 3 {
if len(args) == 3 {
// The user included some flags
flags = replaceCmd[2]
flags = args[2]
}
search := string(replaceCmd[0])
replace := string(replaceCmd[1])
search := string(args[0])
replace := string(args[1])
// If the search and replace expressions have quotes, we need to remove those
if strings.HasPrefix(search, `"`) && strings.HasSuffix(search, `"`) {
search = search[1 : len(search)-1]
}
if strings.HasPrefix(replace, `"`) && strings.HasSuffix(replace, `"`) {
replace = replace[1 : len(replace)-1]
}
// We replace all escaped double quotes to real double quotes
search = strings.Replace(search, `\"`, `"`, -1)
replace = strings.Replace(replace, `\"`, `"`, -1)
// Replace some things so users can actually insert newlines and tabs in replacements
replace = strings.Replace(replace, "\\n", "\n", -1)
replace = strings.Replace(replace, "\\t", "\t", -1)
regex, err := regexp.Compile(search)
regex, err := regexp.Compile("(?m)" + search)
if err != nil {
// There was an error with the user's regex
messenger.Error(err.Error())
@@ -324,13 +424,27 @@ func Replace(args []string) {
}
}
} else {
for {
match := regex.FindStringIndex(view.Buf.String())
if match == nil {
break
bufStr := view.Buf.String()
matches := regex.FindAllStringIndex(bufStr, -1)
if matches != nil && len(matches) > 0 {
prevMatchCount := runePos(matches[0][0], bufStr)
searchCount := runePos(matches[0][1], bufStr) - prevMatchCount
from := FromCharPos(matches[0][0], view.Buf)
to := from.Move(searchCount, view.Buf)
adjust := Count(replace) - searchCount
view.Buf.Replace(from, to, replace)
if len(matches) > 1 {
for _, match := range matches[1:] {
found++
matchCount := runePos(match[0], bufStr)
searchCount = runePos(match[1], bufStr) - matchCount
from = from.Move(matchCount-prevMatchCount+adjust, view.Buf)
to = from.Move(searchCount, view.Buf)
view.Buf.Replace(from, to, replace)
prevMatchCount = matchCount
adjust = Count(replace) - searchCount
}
}
found++
view.Buf.Replace(FromCharPos(match[0], view.Buf), FromCharPos(match[1], view.Buf), replace)
}
}
view.Cursor.Relocate()
@@ -346,8 +460,8 @@ func Replace(args []string) {
// RunShellCommand executes a shell command and returns the output/error
func RunShellCommand(input string) (string, error) {
inputCmd := strings.Split(input, " ")[0]
args := strings.Split(input, " ")[1:]
inputCmd := SplitCommandArgs(input)[0]
args := SplitCommandArgs(input)[1:]
cmd := exec.Command(inputCmd, args...)
outputBytes := &bytes.Buffer{}
@@ -362,8 +476,8 @@ func RunShellCommand(input string) (string, error) {
// HandleShellCommand runs the shell command
// The openTerm argument specifies whether a terminal should be opened (for viewing output
// or interacting with stdin)
func HandleShellCommand(input string, openTerm bool) {
inputCmd := strings.Split(input, " ")[0]
func HandleShellCommand(input string, openTerm bool, waitToFinish bool) string {
inputCmd := SplitCommandArgs(input)[0]
if !openTerm {
// Simply run the command in the background and notify the user when it's done
messenger.Message("Running...")
@@ -388,12 +502,13 @@ func HandleShellCommand(input string, openTerm bool) {
screen.Fini()
screen = nil
args := strings.Split(input, " ")[1:]
args := SplitCommandArgs(input)[1:]
// Set up everything for the command
var outputBuf bytes.Buffer
cmd := exec.Command(inputCmd, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stdout = io.MultiWriter(os.Stdout, &outputBuf)
cmd.Stderr = os.Stderr
// This is a trap for Ctrl-C so that it doesn't kill micro
@@ -406,26 +521,35 @@ func HandleShellCommand(input string, openTerm bool) {
}
}()
// Start the command
cmd.Start()
cmd.Wait()
err := cmd.Wait()
// This is just so we don't return right away and let the user press enter to return
TermMessage("")
output := outputBuf.String()
if err != nil {
output = err.Error()
}
if waitToFinish {
// This is just so we don't return right away and let the user press enter to return
TermMessage("")
}
// Start the screen back up
InitScreen()
return output
}
return ""
}
// HandleCommand handles input from the user
func HandleCommand(input string) {
inputCmd := strings.Split(input, " ")[0]
args := strings.Split(input, " ")[1:]
args := SplitCommandArgs(input)
inputCmd := args[0]
if _, ok := commands[inputCmd]; !ok {
messenger.Error("Unknown command ", inputCmd)
} else {
commands[inputCmd].action(args)
commands[inputCmd].action(args[1:])
}
}

View File

@@ -1,5 +1,7 @@
package main
import "github.com/zyedidia/clipboard"
// 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
@@ -33,6 +35,24 @@ func (c *Cursor) ResetSelection() {
c.CurSelection[1] = c.buf.Start()
}
// SetSelectionStart sets the start of the selection
func (c *Cursor) SetSelectionStart(pos Loc) {
c.CurSelection[0] = pos
// Copy to primary clipboard for linux
if c.HasSelection() {
clipboard.WriteAll(c.GetSelection(), "primary")
}
}
// SetSelectionEnd sets the end of the selection
func (c *Cursor) SetSelectionEnd(pos Loc) {
c.CurSelection[1] = pos
// Copy to primary clipboard for linux
if c.HasSelection() {
clipboard.WriteAll(c.GetSelection(), "primary")
}
}
// HasSelection returns whether or not the user has selected anything
func (c *Cursor) HasSelection() bool {
return c.CurSelection[0] != c.CurSelection[1]
@@ -43,7 +63,7 @@ func (c *Cursor) DeleteSelection() {
if c.CurSelection[0].GreaterThan(c.CurSelection[1]) {
c.buf.Remove(c.CurSelection[1], c.CurSelection[0])
c.Loc = c.CurSelection[1]
} else if c.GetSelection() == "" {
} else if !c.HasSelection() {
return
} else {
c.buf.Remove(c.CurSelection[0], c.CurSelection[1])
@@ -62,12 +82,12 @@ func (c *Cursor) GetSelection() string {
// SelectLine selects the current line
func (c *Cursor) SelectLine() {
c.Start()
c.CurSelection[0] = c.Loc
c.SetSelectionStart(c.Loc)
c.End()
if c.buf.NumLines-1 > c.Y {
c.CurSelection[1] = c.Loc.Move(1, c.buf)
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
} else {
c.CurSelection[1] = c.Loc
c.SetSelectionEnd(c.Loc)
}
c.OrigSelection = c.CurSelection
@@ -77,13 +97,13 @@ func (c *Cursor) SelectLine() {
func (c *Cursor) AddLineToSelection() {
if c.Loc.LessThan(c.OrigSelection[0]) {
c.Start()
c.CurSelection[0] = c.Loc
c.CurSelection[1] = c.OrigSelection[1]
c.SetSelectionStart(c.Loc)
c.SetSelectionEnd(c.OrigSelection[1])
}
if c.Loc.GreaterThan(c.OrigSelection[1]) {
c.End()
c.CurSelection[1] = c.Loc.Move(1, c.buf)
c.CurSelection[0] = c.OrigSelection[0]
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
c.SetSelectionStart(c.OrigSelection[0])
}
if c.Loc.LessThan(c.OrigSelection[1]) && c.Loc.GreaterThan(c.OrigSelection[0]) {
@@ -98,8 +118,8 @@ func (c *Cursor) SelectWord() {
}
if !IsWordChar(string(c.RuneUnder(c.X))) {
c.CurSelection[0] = c.Loc
c.CurSelection[1] = c.Loc.Move(1, c.buf)
c.SetSelectionStart(c.Loc)
c.SetSelectionEnd(c.Loc.Move(1, c.buf))
c.OrigSelection = c.CurSelection
return
}
@@ -110,14 +130,14 @@ func (c *Cursor) SelectWord() {
backward--
}
c.CurSelection[0] = Loc{backward, c.Y}
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.CurSelection[1] = Loc{forward, c.Y}.Move(1, c.buf)
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
c.OrigSelection[1] = c.CurSelection[1]
c.Loc = c.CurSelection[1]
}
@@ -136,8 +156,8 @@ func (c *Cursor) AddWordToSelection() {
backward--
}
c.CurSelection[0] = Loc{backward, c.Y}
c.CurSelection[1] = c.OrigSelection[1]
c.SetSelectionStart(Loc{backward, c.Y})
c.SetSelectionEnd(c.OrigSelection[1])
}
if c.Loc.GreaterThan(c.OrigSelection[1]) {
@@ -147,8 +167,8 @@ func (c *Cursor) AddWordToSelection() {
forward++
}
c.CurSelection[1] = Loc{forward, c.Y}.Move(1, c.buf)
c.CurSelection[0] = c.OrigSelection[0]
c.SetSelectionEnd(Loc{forward, c.Y}.Move(1, c.buf))
c.SetSelectionStart(c.OrigSelection[0])
}
c.Loc = c.CurSelection[1]
@@ -157,11 +177,11 @@ func (c *Cursor) AddWordToSelection() {
// SelectTo selects from the current cursor location to the given location
func (c *Cursor) SelectTo(loc Loc) {
if loc.GreaterThan(c.OrigSelection[0]) {
c.CurSelection[0] = c.OrigSelection[0]
c.CurSelection[1] = loc
c.SetSelectionStart(c.OrigSelection[0])
c.SetSelectionEnd(loc)
} else {
c.CurSelection[0] = loc
c.CurSelection[1] = c.OrigSelection[0]
c.SetSelectionStart(loc)
c.SetSelectionEnd(c.OrigSelection[0])
}
}

View File

@@ -1,9 +1,11 @@
package main
import (
"strings"
"time"
dmp "github.com/sergi/go-diff/diffmatchpatch"
"github.com/yuin/gopher-lua"
)
const (
@@ -114,6 +116,17 @@ func (eh *EventHandler) Execute(t *TextEvent) {
eh.RedoStack = new(Stack)
}
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
}
}
ExecuteTextEvent(t, eh.buf)
}

View File

@@ -1,25 +0,0 @@
package main
var helpPages map[string]string
var helpFiles = []string{
"help",
"keybindings",
"plugins",
"colors",
"options",
"commands",
"tutorial",
}
// LoadHelp loads the help text from inside the binary
func LoadHelp() {
helpPages = make(map[string]string)
for _, file := range helpFiles {
data, err := Asset("runtime/help/" + file + ".md")
if err != nil {
TermMessage("Unable to load help text", file)
}
helpPages[file] = string(data)
}
}

View File

@@ -1,11 +1,10 @@
package main
import (
"github.com/zyedidia/tcell"
"io/ioutil"
"path/filepath"
"regexp"
"strings"
"github.com/zyedidia/tcell"
)
// FileTypeRules represents a complete set of syntax rules for a filetype
@@ -29,150 +28,16 @@ type SyntaxRule struct {
var syntaxFiles map[[2]*regexp.Regexp]FileTypeRules
// These syntax files are pre installed and embedded in the resulting binary by go-bindata
var preInstalledSynFiles = []string{
"Dockerfile",
"apacheconf",
"arduino",
"asciidoc",
"asm",
"awk",
"c",
"caddyfile",
"cmake",
"coffeescript",
"colortest",
"conf",
"conky",
"csharp",
"css",
"cython",
"d",
"dart",
"dot",
"erb",
"fish",
"fortran",
"gdscript",
"gentoo-ebuild",
"gentoo-etc-portage",
"git-commit",
"git-config",
"git-rebase-todo",
"glsl",
"go",
"golo",
"groff",
"haml",
"haskell",
"html",
"ini",
"inputrc",
"java",
"javascript",
"json",
"keymap",
"kickstart",
"ledger",
"lilypond",
"lisp",
"lua",
"makefile",
"man",
"markdown",
"mpdconf",
"micro",
"nanorc",
"nginx",
"ocaml",
"patch",
"peg",
"perl",
"perl6",
"php",
"pkg-config",
"pkgbuild",
"po",
"pov",
"privoxy-action",
"privoxy-config",
"privoxy-filter",
"puppet",
"python",
"r",
"reST",
"rpmspec",
"ruby",
"rust",
"scala",
"sed",
"sh",
"sls",
"sql",
"swift",
"systemd",
"tcl",
"tex",
"vala",
"vi",
"xml",
"xresources",
"yaml",
"yum",
"zsh",
}
// LoadSyntaxFiles loads the syntax files from the default directory (configDir)
func LoadSyntaxFiles() {
// Load the user's custom syntax files, if there are any
LoadSyntaxFilesFromDir(configDir + "/syntax")
// Load the pre-installed syntax files from inside the binary
for _, filetype := range preInstalledSynFiles {
data, err := Asset("runtime/syntax/" + filetype + ".micro")
if err != nil {
TermMessage("Unable to load pre-installed syntax file " + filetype)
continue
}
LoadSyntaxFile(string(data), filetype+".micro")
}
}
// LoadSyntaxFilesFromDir loads the syntax files from a specified directory
// To load the syntax files, we must fill the `syntaxFiles` map
// This involves finding the regex for syntax and if it exists, the regex
// for the header. Then we must get the text for the file and the filetype.
func LoadSyntaxFilesFromDir(dir string) {
colorscheme = make(Colorscheme)
InitColorscheme()
// Default style
defStyle = tcell.StyleDefault.
Foreground(tcell.ColorDefault).
Background(tcell.ColorDefault)
// There may be another default style defined in the colorscheme
// In that case we should use that one
if style, ok := colorscheme["default"]; ok {
defStyle = style
}
if screen != nil {
screen.SetStyle(defStyle)
}
syntaxFiles = make(map[[2]*regexp.Regexp]FileTypeRules)
files, _ := ioutil.ReadDir(dir)
for _, f := range files {
if filepath.Ext(f.Name()) == ".micro" {
filename := dir + "/" + f.Name()
text, err := ioutil.ReadFile(filename)
if err != nil {
TermMessage("Error loading syntax file " + filename + ": " + err.Error())
return
}
LoadSyntaxFile(string(text), filename)
for _, f := range ListRuntimeFiles(RTSyntax) {
data, err := f.Data()
if err != nil {
TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
} else {
LoadSyntaxFile(string(data), f.Name())
}
}
}

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"os"
"strconv"
"strings"
"github.com/zyedidia/clipboard"
"github.com/zyedidia/tcell"
@@ -22,6 +21,7 @@ func TermMessage(msg ...interface{}) {
screenWasNil := screen == nil
if !screenWasNil {
screen.Fini()
screen = nil
}
fmt.Println(msg...)
@@ -44,6 +44,7 @@ func TermError(filename string, lineNum int, err string) {
// 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
@@ -68,16 +69,30 @@ type Messenger struct {
gutterMessage bool
}
func (m *Messenger) AddLog(msg string) {
buffer := m.getBuffer()
buffer.insert(buffer.End(), []byte(msg+"\n"))
buffer.Cursor.Loc = buffer.End()
buffer.Cursor.Relocate()
}
func (m *Messenger) getBuffer() *Buffer {
if m.log == nil {
m.log = NewBuffer([]byte{}, "")
m.log.Name = "Log"
}
return m.log
}
// Message sends a message to the user
func (m *Messenger) Message(msg ...interface{}) {
buf := new(bytes.Buffer)
fmt.Fprint(buf, msg...)
m.message = buf.String()
m.message = fmt.Sprint(msg...)
m.style = defStyle
if _, ok := colorscheme["message"]; ok {
m.style = colorscheme["message"]
}
m.AddLog(m.message)
m.hasMessage = true
}
@@ -93,6 +108,7 @@ func (m *Messenger) Error(msg ...interface{}) {
if _, ok := colorscheme["error-message"]; ok {
m.style = colorscheme["error-message"]
}
m.AddLog(m.message)
m.hasMessage = true
}
@@ -114,11 +130,17 @@ func (m *Messenger) YesNoPrompt(prompt string) (bool, bool) {
switch e.Key() {
case tcell.KeyRune:
if e.Rune() == 'y' {
m.AddLog("\t--> y")
m.hasPrompt = false
return true, false
} else if 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.hasPrompt = false
return false, true
}
}
@@ -144,6 +166,7 @@ func (m *Messenger) LetterPrompt(prompt string, responses ...rune) (rune, bool)
case tcell.KeyRune:
for _, r := range responses {
if e.Rune() == r {
m.AddLog("\t--> " + string(r))
m.Clear()
m.Reset()
m.hasPrompt = false
@@ -151,6 +174,7 @@ func (m *Messenger) LetterPrompt(prompt string, responses ...rune) (rune, bool)
}
}
case tcell.KeyCtrlC, tcell.KeyCtrlQ, tcell.KeyEscape:
m.AddLog("\t--> (cancel)")
m.Clear()
m.Reset()
m.hasPrompt = false
@@ -168,6 +192,8 @@ const (
CommandCompletion
HelpCompletion
OptionCompletion
PluginCmdCompletion
PluginNameCompletion
)
// Prompt sends the user a message and waits for a response to be typed in
@@ -196,14 +222,16 @@ func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Comple
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 := strings.Split(m.response, " ")
args := SplitCommandArgs(m.response)
currentArgNum := len(args) - 1
currentArg := args[currentArgNum]
var completionType Completion
@@ -229,6 +257,12 @@ func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Comple
chosen, suggestions = HelpComplete(currentArg)
} else if completionType == OptionCompletion {
chosen, suggestions = OptionComplete(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 {
@@ -236,10 +270,7 @@ func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Comple
}
if chosen != "" {
if len(args) > 1 {
chosen = " " + chosen
}
m.response = strings.Join(args[:len(args)-1], " ") + chosen
m.response = JoinCommandArgs(append(args[:len(args)-1], chosen)...)
m.cursorx = Count(m.response)
}
}
@@ -268,36 +299,58 @@ func (m *Messenger) Prompt(prompt, historyType string, completionTypes ...Comple
func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
switch e := event.(type) {
case *tcell.EventKey:
if e.Key() != tcell.KeyRune || e.Modifiers() != 0 {
for key, actions := range bindings {
if e.Key() == key.keyCode {
if e.Key() == tcell.KeyRune {
if e.Rune() != key.r {
continue
}
}
if e.Modifiers() == key.modifiers {
for _, action := range actions {
funcName := FuncName(action)
switch funcName {
case "main.(*View).CursorUp":
if m.historyNum > 0 {
m.historyNum--
m.response = history[m.historyNum]
m.cursorx = Count(m.response)
}
case "main.(*View).CursorDown":
if m.historyNum < len(history)-1 {
m.historyNum++
m.response = history[m.historyNum]
m.cursorx = Count(m.response)
}
case "main.(*View).CursorLeft":
if m.cursorx > 0 {
m.cursorx--
}
case "main.(*View).CursorRight":
if m.cursorx < Count(m.response) {
m.cursorx++
}
case "main.(*View).CursorStart", "main.(*View).StartOfLine":
m.cursorx = 0
case "main.(*View).CursorEnd", "main.(*View).EndOfLine":
m.cursorx = Count(m.response)
case "main.(*View).Backspace":
if m.cursorx > 0 {
m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
m.cursorx--
}
case "main.(*View).Paste":
clip, _ := clipboard.ReadAll("clipboard")
m.response = Insert(m.response, m.cursorx, clip)
m.cursorx += Count(clip)
}
}
}
}
}
}
switch e.Key() {
case tcell.KeyUp:
if m.historyNum > 0 {
m.historyNum--
m.response = history[m.historyNum]
m.cursorx = Count(m.response)
}
case tcell.KeyDown:
if m.historyNum < len(history)-1 {
m.historyNum++
m.response = history[m.historyNum]
m.cursorx = Count(m.response)
}
case tcell.KeyLeft:
if m.cursorx > 0 {
m.cursorx--
}
case tcell.KeyRight:
if m.cursorx < Count(m.response) {
m.cursorx++
}
case tcell.KeyBackspace2, tcell.KeyBackspace:
if m.cursorx > 0 {
m.response = string([]rune(m.response)[:m.cursorx-1]) + string([]rune(m.response)[m.cursorx:])
m.cursorx--
}
case tcell.KeyCtrlV:
clip, _ := clipboard.ReadAll()
m.response = Insert(m.response, m.cursorx, clip)
m.cursorx += Count(clip)
case tcell.KeyRune:
m.response = Insert(m.response, m.cursorx, string(e.Rune()))
m.cursorx++
@@ -308,6 +361,23 @@ func (m *Messenger) HandleEvent(event tcell.Event, history []string) {
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)
}
}
}
}
}

View File

@@ -5,8 +5,10 @@ import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/go-errors/errors"
"github.com/layeh/gopher-luar"
@@ -23,6 +25,7 @@ const (
synLinesDown = 75 // How many lines down to look to do syntax highlighting
doubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click
undoThreshold = 500 // If two events are less than n milliseconds apart, undo both of them
autosaveTime = 8 // Number of seconds to wait before autosaving
)
var (
@@ -60,7 +63,8 @@ var (
// Channel of jobs running in the background
jobs chan JobFunction
// Event channel
events chan tcell.Event
events chan tcell.Event
autosave chan bool
)
// LoadInput determines which files should be loaded into buffers
@@ -195,6 +199,31 @@ func RedrawAll() {
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()
LoadSyntaxFiles()
for _, tab := range tabs {
for _, v := range tab.views {
v.Buf.UpdateRules()
if v.Buf.Settings["syntax"].(bool) {
v.matches = Match(v)
}
}
}
}
// Passing -version as a flag will have micro print out the version number
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.")
@@ -202,7 +231,7 @@ var flagStartPos = flag.String("startpos", "", "LINE,COL to start the cursor at
func main() {
flag.Usage = func() {
fmt.Println("Usage: micro [OPTIONS] [FILE]...")
fmt.Println("Micro's options can be set via command line arguments for quick adjustments. For real configuration, please use the bindings.json file (see 'help options').\n")
fmt.Print("Micro's options can be set via command line arguments for quick adjustments. For real configuration, please use the bindings.json file (see 'help options').\n\n")
flag.PrintDefaults()
}
@@ -230,20 +259,7 @@ func main() {
encoding.Register()
tcell.SetEncodingFallback(tcell.EncodingFallbackASCII)
// Find the user's configuration directory (probably $XDG_CONFIG_HOME/micro)
InitConfigDir()
// Load the user's settings
InitGlobalSettings()
InitCommands()
InitBindings()
// Load the syntax files, including the colorscheme
LoadSyntaxFiles()
// Load the help files
LoadHelp()
LoadAll()
// Start the screen
InitScreen()
@@ -280,6 +296,8 @@ func main() {
v.matches = Match(v)
}
}
t.Resize()
}
}
@@ -306,19 +324,42 @@ func main() {
L.SetGlobal("HandleCommand", luar.New(L, HandleCommand))
L.SetGlobal("HandleShellCommand", luar.New(L, HandleShellCommand))
L.SetGlobal("GetLeadingWhitespace", luar.New(L, GetLeadingWhitespace))
L.SetGlobal("MakeCompletion", luar.New(L, MakeCompletion))
L.SetGlobal("NewBuffer", luar.New(L, NewBuffer))
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("JoinPaths", luar.New(L, filepath.Join))
L.SetGlobal("configDir", luar.New(L, configDir))
L.SetGlobal("Reload", luar.New(L, LoadAll))
// Used for asynchronous jobs
L.SetGlobal("JobStart", luar.New(L, JobStart))
L.SetGlobal("JobSend", luar.New(L, JobSend))
L.SetGlobal("JobStop", luar.New(L, JobStop))
LoadPlugins()
// 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))
jobs = make(chan JobFunction, 100)
events = make(chan tcell.Event)
events = make(chan tcell.Event, 100)
autosave = make(chan bool)
LoadPlugins()
// Load the syntax files, including the colorscheme
LoadSyntaxFiles()
for _, t := range tabs {
for _, v := range t.views {
v.Buf.FindFileType()
v.Buf.UpdateRules()
for _, pl := range loadedPlugins {
_, err := Call(pl+".onViewOpen", v)
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
@@ -339,6 +380,15 @@ func main() {
}
}()
go func() {
for {
time.Sleep(autosaveTime * time.Second)
if globalSettings["autosave"].(bool) {
autosave <- true
}
}
}()
for {
// Display everything
RedrawAll()
@@ -351,50 +401,61 @@ func main() {
// 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:
CurView().Save(true)
case event = <-events:
}
switch e := event.(type) {
case *tcell.EventMouse:
if e.Buttons() == tcell.Button1 {
// If the user left clicked we check a couple things
_, h := screen.Size()
x, y := e.Position()
if y == h-1 && messenger.message != "" && 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)
continue
}
for event != nil {
switch e := event.(type) {
case *tcell.EventMouse:
if e.Buttons() == tcell.Button1 {
// If the user left clicked we check a couple things
_, h := screen.Size()
x, y := e.Position()
if y == h-1 && messenger.message != "" && 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
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
}
}
}
}
}
}
// This function checks the mouse event for the possibility of changing the current tab
// If the tab was changed it returns true
if TabbarHandleMouseEvent(event) {
continue
}
// 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 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)
}
}
}

View File

@@ -12,12 +12,6 @@ import (
var loadedPlugins []string
var preInstalledPlugins = []string{
"go",
"linter",
"autoclose",
}
// Call calls the lua function 'function'
// If it does not exist nothing happens, if there is an error,
// the error is returned
@@ -85,6 +79,30 @@ func LuaFunctionCommand(function string) func([]string) {
}
}
// 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
}
}
func LuaFunctionJob(function string) func(string, ...string) {
return func(output string, args ...string) {
_, err := Call(function, unpack(append([]string{output}, args...))...)
@@ -96,47 +114,28 @@ func LuaFunctionJob(function string) func(string, ...string) {
// LoadPlugins loads the pre-installed plugins and the plugins located in ~/.config/micro/plugins
func LoadPlugins() {
files, _ := ioutil.ReadDir(configDir + "/plugins")
for _, plugin := range files {
if plugin.IsDir() {
pluginName := plugin.Name()
files, _ := ioutil.ReadDir(configDir + "/plugins/" + pluginName)
for _, f := range files {
if f.Name() == pluginName+".lua" {
data, _ := ioutil.ReadFile(configDir + "/plugins/" + pluginName + "/" + f.Name())
pluginDef := "\nlocal P = {}\n" + pluginName + " = P\nsetmetatable(" + pluginName + ", {__index = _G})\nsetfenv(1, P)\n"
if err := L.DoString(pluginDef + string(data)); err != nil {
TermMessage(err)
continue
}
loadedPlugins = append(loadedPlugins, pluginName)
}
}
}
}
for _, pluginName := range preInstalledPlugins {
for _, plugin := range ListRuntimeFiles(RTPlugin) {
alreadyExists := false
pluginName := plugin.Name()
for _, pl := range loadedPlugins {
if pl == pluginName {
alreadyExists = true
break
}
}
if !alreadyExists {
plugin := "runtime/plugins/" + pluginName + "/" + pluginName + ".lua"
data, err := Asset(plugin)
data, err := plugin.Data()
if err != nil {
TermMessage("Error loading pre-installed plugin: " + pluginName)
TermMessage("Error loading plugin: " + pluginName)
continue
}
pluginDef := "\nlocal P = {}\n" + pluginName + " = P\nsetmetatable(" + pluginName + ", {__index = _G})\nsetfenv(1, P)\n"
if err := L.DoString(pluginDef + string(data)); err != nil {
TermMessage(err)
continue
}
loadedPlugins = append(loadedPlugins, pluginName)
}
}

611
cmd/micro/pluginmanager.go Normal file
View File

@@ -0,0 +1,611 @@
package main
import (
"archive/zip"
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/blang/semver"
"github.com/yuin/gopher-lua"
"github.com/zyedidia/json5/encoding/json5"
)
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 := range loadedPlugins {
version := GetInstalledPluginVersion(name)
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
}
}
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 {
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)
}
}
// 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 {
plugins = loadedPlugins
}
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

@@ -0,0 +1,55 @@
package main
import (
"github.com/blang/semver"
"testing"
"github.com/zyedidia/json5/encoding/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)
}
}

187
cmd/micro/rtfiles.go Normal file
View File

@@ -0,0 +1,187 @@
package main
import (
"io/ioutil"
"os"
"path"
"path/filepath"
)
const (
RTColorscheme = "colorscheme"
RTSyntax = "syntax"
RTHelp = "help"
RTPlugin = "plugin"
)
// RuntimeFile allows the program to read runtime data like colorschemes or syntax files
type RuntimeFile interface {
// Name returns a name of the file without paths or extensions
Name() string
// Data returns the content of the file.
Data() ([]byte, error)
}
// allFiles contains all available files, mapped by filetype
var allFiles map[string][]RuntimeFile
// some file on filesystem
type realFile string
// some asset file
type assetFile string
// some file on filesystem but with a different name
type namedFile struct {
realFile
name string
}
func (rf realFile) Name() string {
fn := filepath.Base(string(rf))
return fn[:len(fn)-len(filepath.Ext(fn))]
}
func (rf realFile) Data() ([]byte, error) {
return ioutil.ReadFile(string(rf))
}
func (af assetFile) Name() string {
fn := path.Base(string(af))
return fn[:len(fn)-len(path.Ext(fn))]
}
func (af assetFile) Data() ([]byte, error) {
return Asset(string(af))
}
func (nf namedFile) Name() string {
return nf.name
}
// AddRuntimeFile registers a file for the given filetype
func AddRuntimeFile(fileType string, file RuntimeFile) {
if allFiles == nil {
allFiles = make(map[string][]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) {
files, _ := ioutil.ReadDir(directory)
for _, f := range files {
if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok {
fullPath := filepath.Join(directory, f.Name())
AddRuntimeFile(fileType, realFile(fullPath))
}
}
}
// AddRuntimeFilesFromAssets registers each file from the given asset-directory for
// the filetype which matches the file-pattern
func AddRuntimeFilesFromAssets(fileType, directory, pattern string) {
files, err := AssetDir(directory)
if err != nil {
return
}
for _, f := range files {
if ok, _ := path.Match(pattern, f); ok {
AddRuntimeFile(fileType, assetFile(path.Join(directory, f)))
}
}
}
// FindRuntimeFile finds a runtime file of the given filetype and name
// will return nil if no file was found
func FindRuntimeFile(fileType, name string) RuntimeFile {
for _, f := range ListRuntimeFiles(fileType) {
if f.Name() == name {
return f
}
}
return nil
}
// ListRuntimeFiles lists all known runtime files for the given filetype
func ListRuntimeFiles(fileType string) []RuntimeFile {
if files, ok := allFiles[fileType]; ok {
return files
}
return []RuntimeFile{}
}
// InitRuntimeFiles initializes all assets file and the config directory
func InitRuntimeFiles() {
add := func(fileType, dir, pattern string) {
AddRuntimeFilesFromDirectory(fileType, filepath.Join(configDir, dir), pattern)
AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern)
}
add(RTColorscheme, "colorschemes", "*.micro")
add(RTSyntax, "syntax", "*.micro")
add(RTHelp, "help", "*.md")
// Search configDir for plugin-scripts
files, _ := ioutil.ReadDir(filepath.Join(configDir, "plugins"))
for _, f := range files {
if f.IsDir() {
scriptPath := filepath.Join(configDir, "plugins", f.Name(), f.Name()+".lua")
if _, err := os.Stat(scriptPath); err == nil {
AddRuntimeFile(RTPlugin, realFile(scriptPath))
}
}
}
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))
}
}
}
}
// PluginReadRuntimeFile allows plugin scripts to read the content of a runtime file
func PluginReadRuntimeFile(fileType, name string) string {
if file := FindRuntimeFile(fileType, name); file != nil {
if data, err := file.Data(); err == nil {
return string(data)
}
}
return ""
}
// PluginListRuntimeFiles allows plugins to lists all runtime files of the given type
func PluginListRuntimeFiles(fileType string) []string {
files := ListRuntimeFiles(fileType)
result := make([]string, len(files))
for i, f := range files {
result[i] = f.Name()
}
return result
}
// PluginAddRuntimeFile adds a file to the runtime files for a plugin
func PluginAddRuntimeFile(plugin, filetype, filePath string) {
fullpath := filepath.Join(configDir, "plugins", plugin, filePath)
if _, err := os.Stat(fullpath); err == nil {
AddRuntimeFile(filetype, realFile(fullpath))
} else {
fullpath = path.Join("runtime", "plugins", plugin, filePath)
AddRuntimeFile(filetype, assetFile(fullpath))
}
}
// 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)
if _, err := os.Stat(fullpath); err == nil {
AddRuntimeFilesFromDirectory(filetype, fullpath, pattern)
} else {
fullpath = path.Join("runtime", "plugins", plugin, directory)
AddRuntimeFilesFromAssets(filetype, fullpath, pattern)
}
}

File diff suppressed because one or more lines are too long

View File

@@ -21,10 +21,12 @@ var (
)
// BeginSearch starts a search
func BeginSearch() {
func BeginSearch(searchStr string) {
searchHistory = append(searchHistory, "")
messenger.historyNum = len(searchHistory) - 1
searching = true
messenger.response = searchStr
messenger.cursorx = Count(searchStr)
messenger.hasPrompt = true
messenger.Message("Find: ")
}
@@ -41,13 +43,27 @@ func EndSearch() {
}
}
// exit 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.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape, tcell.KeyEnter:
case tcell.KeyEscape:
// Exit the search mode
ExitSearch(v)
return
case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEnter:
// Done
EndSearch()
return
@@ -125,8 +141,8 @@ func Search(searchStr string, v *View, down bool) {
return
}
v.Cursor.CurSelection[0] = FromCharPos(charPos+runePos(match[0], str), v.Buf)
v.Cursor.CurSelection[1] = FromCharPos(charPos+runePos(match[1], str), v.Buf)
v.Cursor.SetSelectionStart(FromCharPos(charPos+runePos(match[0], str), v.Buf))
v.Cursor.SetSelectionEnd(FromCharPos(charPos+runePos(match[1], str), v.Buf))
v.Cursor.Loc = v.Cursor.CurSelection[1]
if v.Relocate() {
v.matches = Match(v)

View File

@@ -8,13 +8,24 @@ import (
"strconv"
"strings"
"github.com/yosuke-furukawa/json5/encoding/json5"
"github.com/zyedidia/glob"
"github.com/zyedidia/json5/encoding/json5"
)
type optionValidator func(string, interface{}) error
// The options that the user can set
var globalSettings map[string]interface{}
// Options with validators
var optionValidators = map[string]optionValidator{
"tabsize": validatePositiveValue,
"scrollmargin": validateNonNegativeValue,
"scrollspeed": validateNonNegativeValue,
"colorscheme": validateColorscheme,
"colorcolumn": validateNonNegativeValue,
}
// InitGlobalSettings initializes the options map and sets all options to their default values
func InitGlobalSettings() {
defaults := DefaultGlobalSettings()
@@ -165,8 +176,11 @@ func GetOption(name string) interface{} {
func DefaultGlobalSettings() map[string]interface{} {
return map[string]interface{}{
"autoindent": true,
"colorscheme": "zenburn",
"autosave": false,
"colorcolumn": float64(0),
"colorscheme": "default",
"cursorline": true,
"eofnewline": false,
"ignorecase": false,
"indentchar": " ",
"infobar": true,
@@ -175,10 +189,15 @@ func DefaultGlobalSettings() map[string]interface{} {
"saveundo": false,
"scrollspeed": float64(2),
"scrollmargin": float64(3),
"softwrap": false,
"statusline": true,
"syntax": true,
"tabsize": float64(4),
"tabstospaces": false,
"pluginchannels": []string{
"https://raw.githubusercontent.com/micro-editor/plugin-channel/master/channel.json",
},
"pluginrepos": []string{},
}
}
@@ -187,7 +206,10 @@ func DefaultGlobalSettings() map[string]interface{} {
func DefaultLocalSettings() map[string]interface{} {
return map[string]interface{}{
"autoindent": true,
"autosave": false,
"colorcolumn": float64(0),
"cursorline": true,
"eofnewline": false,
"filetype": "Unknown",
"ignorecase": false,
"indentchar": " ",
@@ -196,6 +218,7 @@ func DefaultLocalSettings() map[string]interface{} {
"saveundo": false,
"scrollspeed": float64(2),
"scrollmargin": float64(3),
"softwrap": false,
"statusline": true,
"syntax": true,
"tabsize": float64(4),
@@ -208,12 +231,6 @@ func DefaultLocalSettings() map[string]interface{} {
// 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 option == "colorscheme" {
if !ColorschemeExists(value) {
return errors.New(value + " is not a valid colorscheme")
}
}
if _, ok := globalSettings[option]; !ok {
if _, ok := CurView().Buf.Settings[option]; !ok {
return errors.New("Invalid option")
@@ -222,23 +239,33 @@ func SetOption(option, value string) error {
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")
}
globalSettings[option] = b
nativeValue = b
} else if kind == reflect.String {
globalSettings[option] = value
nativeValue = value
} else if kind == reflect.Float64 {
i, err := strconv.Atoi(value)
if err != nil {
return errors.New("Invalid value")
}
globalSettings[option] = float64(i)
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()
for _, tab := range tabs {
@@ -275,23 +302,33 @@ func SetLocalOption(option, value string, view *View) error {
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")
}
buf.Settings[option] = b
nativeValue = b
} else if kind == reflect.String {
buf.Settings[option] = value
nativeValue = value
} else if kind == reflect.Float64 {
i, err := strconv.Atoi(value)
if err != nil {
return errors.New("Invalid value")
}
buf.Settings[option] = float64(i)
nativeValue = float64(i)
} else {
return errors.New("Option has unsupported value type")
}
if err := optionIsValid(option, nativeValue); err != nil {
return err
}
buf.Settings[option] = nativeValue
if option == "statusline" {
view.ToggleStatusLine()
if buf.Settings["syntax"].(bool) {
@@ -327,3 +364,55 @@ func SetOptionAndSettings(option, value string) {
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
}

View File

@@ -1,24 +1,30 @@
package main
// SpltType specifies whether a split is horizontal or vertical
type SplitType bool
const (
VerticalSplit = false
// VerticalSplit type
VerticalSplit = false
// HorizontalSplit type
HorizontalSplit = true
)
// A Node on the split tree
type Node interface {
VSplit(buf *Buffer)
HSplit(buf *Buffer)
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
@@ -27,6 +33,7 @@ func NewLeafNode(v *View, parent *SplitTree) *LeafNode {
return n
}
// A SplitTree is a Node itself and it contains other nodes
type SplitTree struct {
kind SplitType
@@ -42,6 +49,7 @@ type SplitTree struct {
tabNum int
}
// VSplit creates a vertical split
func (l *LeafNode) VSplit(buf *Buffer) {
tab := tabs[l.parent.tabNum]
if l.parent.kind == VerticalSplit {
@@ -56,6 +64,7 @@ func (l *LeafNode) VSplit(buf *Buffer) {
s := new(SplitTree)
s.kind = VerticalSplit
s.parent = l.parent
s.tabNum = l.parent.tabNum
newView := NewView(buf)
newView.TabNum = l.parent.tabNum
newView.Num = len(tab.views)
@@ -68,6 +77,7 @@ func (l *LeafNode) VSplit(buf *Buffer) {
}
}
// HSplit creates a horizontal split
func (l *LeafNode) HSplit(buf *Buffer) {
tab := tabs[l.parent.tabNum]
if l.parent.kind == HorizontalSplit {
@@ -81,6 +91,7 @@ func (l *LeafNode) HSplit(buf *Buffer) {
} else {
s := new(SplitTree)
s.kind = HorizontalSplit
s.tabNum = l.parent.tabNum
s.parent = l.parent
newView := NewView(buf)
newView.TabNum = l.parent.tabNum
@@ -94,6 +105,7 @@ func (l *LeafNode) HSplit(buf *Buffer) {
}
}
// Delete deletes a split
func (l *LeafNode) Delete() {
i := search(l.parent.children, l)
@@ -115,6 +127,7 @@ func (l *LeafNode) Delete() {
}
}
// 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 {
@@ -130,6 +143,7 @@ func (s *SplitTree) Cleanup() {
}
}
// ResizeSplits resizes all the splits correctly
func (s *SplitTree) ResizeSplits() {
for i, node := range s.children {
if n, ok := node.(*LeafNode); ok {
@@ -193,7 +207,10 @@ func findView(haystack []*View, needle *View) int {
return 0
}
// VSplit is here just to make SplitTree fit the Node interface
func (s *SplitTree) VSplit(buf *Buffer) {}
// HSplit is here just to make SplitTree fit the Node interface
func (s *SplitTree) HSplit(buf *Buffer) {}
func (s *SplitTree) String() string {

View File

@@ -37,7 +37,7 @@ func (sline *Statusline) Display() {
file += " " + sline.view.Buf.FileType()
rightText := helpBinding + " for help "
if sline.view.Help {
if sline.view.Type == vtHelp {
rightText = helpBinding + " to close help "
}

View File

@@ -37,11 +37,14 @@ func NewTabFromView(v *View) *Tab {
t.tree.height--
}
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
}

View File

@@ -1,8 +1,11 @@
package main
import (
"bytes"
"os"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"time"
@@ -20,7 +23,7 @@ func Count(s string) int {
return utf8.RuneCountInString(s)
}
// NumOccurrences counts the number of occurences of a byte in a string
// 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++ {
@@ -62,7 +65,7 @@ func Max(a, b int) int {
func IsWordChar(str string) bool {
if len(str) > 1 {
// Unicode
return false
return true
}
c := str[0]
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_')
@@ -154,7 +157,19 @@ func GetModTime(path string) (time.Time, bool) {
// StringWidth returns the width of a string where tabs count as `tabsize` width
func StringWidth(str string, tabsize int) int {
sw := runewidth.StringWidth(str)
sw += NumOccurrences(str, '\t') * (tabsize - 1)
lineIdx := 0
for _, ch := range str {
switch ch {
case '\t':
ts := tabsize - (lineIdx % tabsize)
sw += ts - 1
lineIdx += ts
case '\n':
lineIdx = 0
default:
lineIdx++
}
}
return sw
}
@@ -162,16 +177,22 @@ func StringWidth(str string, tabsize int) int {
// 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
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
}
@@ -217,3 +238,99 @@ func Abs(n int) int {
}
return n
}
// FuncName returns the name of a given function object
func FuncName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
// SplitCommandArgs separates multiple command arguments which may be quoted.
// The returned slice contains at least one string
func SplitCommandArgs(input string) []string {
var result []string
var curQuote *bytes.Buffer
curArg := new(bytes.Buffer)
escape := false
finishQuote := func() {
if curQuote == nil {
return
}
str := curQuote.String()
if unquoted, err := strconv.Unquote(str); err == nil {
str = unquoted
}
curArg.WriteString(str)
curQuote = nil
}
appendResult := func() {
finishQuote()
escape = false
str := curArg.String()
result = append(result, str)
curArg.Reset()
}
for _, r := range input {
if r == ' ' && curQuote == nil {
appendResult()
} else {
runeHandled := false
appendRuneToBuff := func() {
if curQuote != nil {
curQuote.WriteRune(r)
} else {
curArg.WriteRune(r)
}
runeHandled = true
}
if r == '"' && curQuote == nil {
curQuote = new(bytes.Buffer)
appendRuneToBuff()
} else {
if curQuote != nil && !escape {
if r == '"' {
appendRuneToBuff()
finishQuote()
} else if r == '\\' {
appendRuneToBuff()
escape = true
continue
}
}
}
if !runeHandled {
appendRuneToBuff()
}
}
escape = false
}
appendResult()
return result
}
// JoinCommandArgs joins multiple command arguments and quote the strings if needed.
func JoinCommandArgs(args ...string) string {
buf := new(bytes.Buffer)
first := true
for _, arg := range args {
if first {
first = false
} else {
buf.WriteRune(' ')
}
quoted := strconv.Quote(arg)
if quoted[1:len(quoted)-1] != arg || strings.ContainsRune(arg, ' ') {
buf.WriteString(quoted)
} else {
buf.WriteString(arg)
}
}
return buf.String()
}

View File

@@ -1,6 +1,9 @@
package main
import "testing"
import (
"reflect"
"testing"
)
func TestNumOccurences(t *testing.T) {
var tests = []struct {
@@ -47,15 +50,15 @@ func TestIsWordChar(t *testing.T) {
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(")") == true {
t.Errorf("IsWordChar()) = true")
}
@@ -63,3 +66,91 @@ func TestIsWordChar(t *testing.T) {
t.Errorf("IsWordChar(\n)) = true")
}
}
func TestJoinAndSplitCommandArgs(t *testing.T) {
tests := []struct {
Query []string
Wanted string
}{
{[]string{`test case`}, `"test case"`},
{[]string{`quote "test"`}, `"quote \"test\""`},
{[]string{`slash\\\ test`}, `"slash\\\\\\ test"`},
{[]string{`path 1`, `path\" 2`}, `"path 1" "path\\\" 2"`},
{[]string{`foo`}, `foo`},
{[]string{`foo\"bar`}, `"foo\\\"bar"`},
{[]string{``}, ``},
{[]string{`"`}, `"\""`},
{[]string{`a`, ``}, `a `},
{[]string{``, ``, ``, ``}, ` `},
{[]string{"\n"}, `"\n"`},
{[]string{"foo\tbar"}, `"foo\tbar"`},
}
for i, test := range tests {
if result := JoinCommandArgs(test.Query...); test.Wanted != result {
t.Errorf("JoinCommandArgs failed at Test %d\nGot: %q", i, result)
}
if result := SplitCommandArgs(test.Wanted); !reflect.DeepEqual(test.Query, result) {
t.Errorf("SplitCommandArgs failed at Test %d\nGot: `%q`", i, result)
}
}
splitTests := []struct {
Query string
Wanted []string
}{
{`"hallo""Welt"`, []string{`halloWelt`}},
{`"hallo" "Welt"`, []string{`hallo`, `Welt`}},
{`\"`, []string{`\"`}},
{`"foo`, []string{`"foo`}},
{`"foo"`, []string{`foo`}},
{`"\"`, []string{`"\"`}},
{`"C:\\"foo.txt`, []string{`C:\foo.txt`}},
{`"\n"new"\n"line`, []string{"\nnew\nline"}},
}
for i, test := range splitTests {
if result := SplitCommandArgs(test.Query); !reflect.DeepEqual(test.Wanted, result) {
t.Errorf("SplitCommandArgs failed at Split-Test %d\nGot: `%q`", i, result)
}
}
}
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)
}
}

View File

@@ -1,14 +1,24 @@
package main
import (
"io/ioutil"
"strconv"
"strings"
"time"
"github.com/mattn/go-runewidth"
"github.com/mitchellh/go-homedir"
"github.com/zyedidia/tcell"
)
type ViewType int
const (
vtDefault ViewType = iota
vtHelp
vtLog
)
// The View struct stores information about a view into a buffer.
// It stores information about the cursor, and the viewport
// that the user sees the buffer from.
@@ -17,18 +27,15 @@ type View struct {
Cursor *Cursor
// The topmost line, used for vertical scrolling
Topline int
Topline int
Bottomline int
// The leftmost column, used for horizontal scrolling
leftCol int
// Percentage of the terminal window that this view takes up (from 0 to 100)
widthPercent int
heightPercent int
// Specifies whether or not this view holds a help buffer
Help bool
Type ViewType
// Actual with and height
// Actual width and height
width int
height int
@@ -122,6 +129,7 @@ func NewViewWidthHeight(buf *Buffer, w, h int) *View {
return v
}
// ToggleStatusLine creates an extra row for the statusline if necessary
func (v *View) ToggleStatusLine() {
if v.Buf.Settings["statusline"].(bool) {
v.height--
@@ -130,6 +138,7 @@ func (v *View) ToggleStatusLine() {
}
}
// ToggleTabbar creates an extra row for the tabbar if necessary
func (v *View) ToggleTabbar() {
if len(tabs) > 1 {
if v.y == 0 {
@@ -145,6 +154,20 @@ func (v *View) ToggleTabbar() {
}
}
func (v *View) paste(clip string) {
leadingWS := GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y))
if v.Cursor.HasSelection() {
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
}
clip = strings.Replace(clip, "\n", "\n"+leadingWS, -1)
v.Buf.Insert(v.Cursor.Loc, clip)
v.Cursor.Loc = v.Cursor.Loc.Move(Count(clip), v.Buf)
v.freshClip = false
messenger.Message("Pasted clipboard")
}
// ScrollUp scrolls the view up n lines (if possible)
func (v *View) ScrollUp(n int) {
// Try to scroll by n but if it would overflow, scroll by 1
@@ -168,16 +191,21 @@ func (v *View) ScrollDown(n int) {
// CanClose returns whether or not the view can be closed
// If there are unsaved changes, the user will be asked if the view can be closed
// causing them to lose the unsaved changes
// The message is what to print after saying "You have unsaved changes. "
func (v *View) CanClose(msg string, responses ...rune) bool {
if v.Buf.IsModified {
char, canceled := messenger.LetterPrompt("You have unsaved changes. "+msg, responses...)
func (v *View) CanClose() bool {
if v.Type == vtDefault && v.Buf.IsModified {
var char rune
var canceled bool
if v.Buf.Settings["autosave"].(bool) {
char = 'y'
} else {
char, canceled = messenger.LetterPrompt("Save changes to "+v.Buf.Name+" before closing? (y,n,esc) ", 'y', 'n')
}
if !canceled {
if char == 'y' {
return true
} else if char == 's' {
v.Save(true)
return true
} else if char == 'n' {
return true
}
}
} else {
@@ -208,6 +236,23 @@ func (v *View) OpenBuffer(buf *Buffer) {
v.lastClickTime = time.Time{}
}
// Open opens the given file in the view
func (v *View) Open(filename string) {
home, _ := homedir.Dir()
filename = strings.Replace(filename, "~", home, 1)
file, err := ioutil.ReadFile(filename)
var buf *Buffer
if err != nil {
messenger.Message(err.Error())
// File does not exist -- create an empty buffer with that name
buf = NewBuffer([]byte{}, filename)
} else {
buf = NewBuffer(file, filename)
}
v.OpenBuffer(buf)
}
// CloseBuffer performs any closing functions on the buffer
func (v *View) CloseBuffer() {
if v.Buf != nil {
@@ -217,7 +262,7 @@ func (v *View) CloseBuffer() {
// ReOpen reloads the current buffer
func (v *View) ReOpen() {
if v.CanClose("Continue? (y,n,s) ", 'y', 'n', 's') {
if v.CanClose() {
screen.Clear()
v.Buf.ReOpen()
v.Relocate()
@@ -239,9 +284,49 @@ func (v *View) VSplit(buf *Buffer) bool {
return false
}
// GetSoftWrapLocation gets the location of a visual click on the screen and converts it to col,line
func (v *View) GetSoftWrapLocation(vx, vy int) (int, int) {
if !v.Buf.Settings["softwrap"].(bool) {
vx = v.Cursor.GetCharPosInLine(vy, vx)
return vx, vy
}
screenX, screenY := 0, v.Topline
for lineN := v.Topline; lineN < v.Bottomline; lineN++ {
line := v.Buf.Line(lineN)
colN := 0
for _, ch := range line {
if screenX >= v.width-v.lineNumOffset {
screenX = 0
screenY++
}
if screenX == vx && screenY == vy {
return colN, lineN
}
if ch == '\t' {
screenX += int(v.Buf.Settings["tabsize"].(float64)) - 1
}
screenX++
colN++
}
if screenY == vy {
return colN, lineN
}
screenX = 0
screenY++
}
return 0, 0
}
// Relocate moves the view window so that the cursor is in view
// This is useful if the user has scrolled far away, and then starts typing
func (v *View) Relocate() bool {
height := v.Bottomline - v.Topline
ret := false
cy := v.Cursor.Y
scrollmargin := int(v.Buf.Settings["scrollmargin"].(float64))
@@ -252,22 +337,24 @@ func (v *View) Relocate() bool {
v.Topline = cy
ret = true
}
if cy > v.Topline+v.height-1-scrollmargin && cy < v.Buf.NumLines-scrollmargin {
v.Topline = cy - v.height + 1 + scrollmargin
if cy > v.Topline+height-1-scrollmargin && cy < v.Buf.NumLines-scrollmargin {
v.Topline = cy - height + 1 + scrollmargin
ret = true
} else if cy >= v.Buf.NumLines-scrollmargin && cy > v.height {
v.Topline = v.Buf.NumLines - v.height
} else if cy >= v.Buf.NumLines-scrollmargin && cy > height {
v.Topline = v.Buf.NumLines - height
ret = true
}
cx := v.Cursor.GetVisualX()
if cx < v.leftCol {
v.leftCol = cx
ret = true
}
if cx+v.lineNumOffset+1 > v.leftCol+v.width {
v.leftCol = cx - v.width + v.lineNumOffset + 1
ret = true
if !v.Buf.Settings["softwrap"].(bool) {
cx := v.Cursor.GetVisualX()
if cx < v.leftCol {
v.leftCol = cx
ret = true
}
if cx+v.lineNumOffset+1 > v.leftCol+v.width {
v.leftCol = cx - v.width + v.lineNumOffset + 1
ret = true
}
}
return ret
}
@@ -289,7 +376,8 @@ func (v *View) MoveToMouseClick(x, y int) {
x = 0
}
x = v.Cursor.GetCharPosInLine(y, x)
x, y = v.GetSoftWrapLocation(x, y)
// x = v.Cursor.GetCharPosInLine(y, x)
if x > Count(v.Buf.Line(y)) {
x = Count(v.Buf.Line(y))
}
@@ -311,7 +399,34 @@ func (v *View) HandleEvent(event tcell.Event) {
// Window resized
tabs[v.TabNum].Resize()
case *tcell.EventKey:
if e.Key() == tcell.KeyRune && (e.Modifiers() == 0 || e.Modifiers() == tcell.ModShift) {
// Check first if input is a key binding, if it is we 'eat' the input and don't insert a rune
isBinding := false
if e.Key() != tcell.KeyRune || e.Modifiers() != 0 {
for key, actions := range bindings {
if e.Key() == key.keyCode {
if e.Key() == tcell.KeyRune {
if e.Rune() != key.r {
continue
}
}
if e.Modifiers() == key.modifiers {
relocate = false
isBinding = true
for _, action := range actions {
relocate = action(v, true) || relocate
funcName := FuncName(action)
if funcName != "main.(*View).ToggleMacro" && funcName != "main.(*View).PlayMacro" {
if recordingMacro {
curMacro = append(curMacro, action)
}
}
}
break
}
}
}
}
if !isBinding && e.Key() == tcell.KeyRune {
// Insert a character
if v.Cursor.HasSelection() {
v.Cursor.DeleteSelection()
@@ -326,21 +441,9 @@ func (v *View) HandleEvent(event tcell.Event) {
TermMessage(err)
}
}
} else {
for key, actions := range bindings {
if e.Key() == key.keyCode {
if e.Key() == tcell.KeyRune {
if e.Rune() != key.r {
continue
}
}
if e.Modifiers() == key.modifiers {
relocate = false
for _, action := range actions {
relocate = action(v, true) || relocate
}
}
}
if recordingMacro {
curMacro = append(curMacro, e.Rune())
}
}
case *tcell.EventPaste:
@@ -411,9 +514,13 @@ func (v *View) HandleEvent(event tcell.Event) {
} else if v.doubleClick {
v.Cursor.AddWordToSelection()
} else {
v.Cursor.CurSelection[1] = v.Cursor.Loc
v.Cursor.SetSelectionEnd(v.Cursor.Loc)
}
}
case tcell.Button2:
// Middle mouse button was clicked,
// We should paste primary
v.PastePrimary(true)
case tcell.ButtonNone:
// Mouse event with no click
if !v.mouseReleased {
@@ -427,7 +534,7 @@ func (v *View) HandleEvent(event tcell.Event) {
if !v.doubleClick && !v.tripleClick {
v.MoveToMouseClick(x, y)
v.Cursor.CurSelection[1] = v.Cursor.Loc
v.Cursor.SetSelectionEnd(v.Cursor.Loc)
}
v.mouseReleased = true
}
@@ -445,9 +552,6 @@ func (v *View) HandleEvent(event tcell.Event) {
if relocate {
v.Relocate()
}
if v.Buf.Settings["syntax"].(bool) {
v.matches = Match(v)
}
}
// GutterMessage creates a message in this view's gutter
@@ -483,15 +587,18 @@ func (v *View) ClearAllGutterMessages() {
// Opens the given help page in a new horizontal split
func (v *View) openHelp(helpPage string) {
if v.Help {
helpBuffer := NewBuffer([]byte(helpPages[helpPage]), helpPage+".md")
helpBuffer.Name = "Help"
v.OpenBuffer(helpBuffer)
if data, err := FindRuntimeFile(RTHelp, helpPage).Data(); err != nil {
TermMessage("Unable to load help text", helpPage, "\n", err)
} else {
helpBuffer := NewBuffer([]byte(helpPages[helpPage]), helpPage+".md")
helpBuffer := NewBuffer(data, helpPage+".md")
helpBuffer.Name = "Help"
v.HSplit(helpBuffer)
CurView().Help = true
if v.Type == vtHelp {
v.OpenBuffer(helpBuffer)
} else {
v.HSplit(helpBuffer)
CurView().Type = vtHelp
}
}
}
@@ -503,6 +610,15 @@ func (v *View) drawCell(x, y int, ch rune, combc []rune, style tcell.Style) {
// DisplayView renders the view to the screen
func (v *View) DisplayView() {
if v.Type == vtLog {
// Log views should always follow the cursor...
v.Relocate()
}
if v.Buf.Settings["syntax"].(bool) {
v.matches = Match(v)
}
// The charNum we are currently displaying
// starts at the start of the viewport
charNum := Loc{0, v.Topline}
@@ -535,17 +651,22 @@ func (v *View) DisplayView() {
}
// These represent the current screen coordinates
screenX, screenY := 0, 0
screenX, screenY := v.x, v.y-1
highlightStyle := defStyle
curLineN := 0
// ViewLine is the current line from the top of the viewport
for viewLine := 0; viewLine < v.height; viewLine++ {
screenY = v.y + viewLine
screenY++
screenX = v.x
// This is the current line number of the buffer that we are drawing
curLineN := viewLine + v.Topline
curLineN = viewLine + v.Topline
if screenY-v.y >= v.height {
break
}
if v.x != 0 {
// Draw the split divider
@@ -610,9 +731,9 @@ func (v *View) DisplayView() {
}
}
lineNumStyle := defStyle
if v.Buf.Settings["ruler"] == true {
// Write the line number
lineNumStyle := defStyle
if style, ok := colorscheme["line-number"]; ok {
lineNumStyle = style
}
@@ -642,7 +763,23 @@ func (v *View) DisplayView() {
// Now we actually draw the line
colN := 0
strWidth := 0
tabSize := int(v.Buf.Settings["tabsize"].(float64))
for _, ch := range line {
if v.Buf.Settings["softwrap"].(bool) {
if screenX-v.x >= v.width {
screenY++
for i := 0; i < v.lineNumOffset; i++ {
screen.SetContent(v.x+i, screenY, ' ', nil, lineNumStyle)
}
screenX = v.x + v.lineNumOffset
}
}
if tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN && colN == v.Cursor.X {
v.DisplayCursor(screenX-v.leftCol, screenY)
}
lineStyle := defStyle
if v.Buf.Settings["syntax"].(bool) {
@@ -678,7 +815,7 @@ func (v *View) DisplayView() {
// First the user may have configured an `indent-char` to be displayed to show that this
// is a tab character
lineIndentStyle := defStyle
if style, ok := colorscheme["indent-char"]; ok {
if style, ok := colorscheme["indent-char"]; ok && v.Buf.Settings["indentchar"].(string) != " " {
lineIndentStyle = style
}
if v.Cursor.HasSelection() &&
@@ -703,8 +840,9 @@ func (v *View) DisplayView() {
v.drawCell(screenX-v.leftCol, screenY, indentChar[0], nil, lineIndentStyle)
}
// Now the tab has to be displayed as a bunch of spaces
tabSize := int(v.Buf.Settings["tabsize"].(float64))
for i := 0; i < tabSize-1; i++ {
visLoc := strWidth
remainder := tabSize - (visLoc % tabSize)
for i := 0; i < remainder-1; i++ {
screenX++
if screenX-v.x-v.leftCol >= v.lineNumOffset {
v.drawCell(screenX-v.leftCol, screenY, ' ', nil, lineStyle)
@@ -728,9 +866,14 @@ func (v *View) DisplayView() {
charNum = charNum.Move(1, v.Buf)
screenX++
colN++
strWidth += StringWidth(string(ch), tabSize)
}
// Here we are at a newline
if tabs[curTab].curView == v.Num && !v.Cursor.HasSelection() && v.Cursor.Y == curLineN && colN == v.Cursor.X {
v.DisplayCursor(screenX-v.leftCol, screenY)
}
// The newline may be selected, in which case we should draw the selection style
// with a space to represent it
if v.Cursor.HasSelection() &&
@@ -757,27 +900,35 @@ func (v *View) DisplayView() {
}
}
if screenX-v.x-v.leftCol+i >= v.lineNumOffset {
colorcolumn := int(v.Buf.Settings["colorcolumn"].(float64))
if colorcolumn != 0 && screenX-v.lineNumOffset+i == colorcolumn-1 {
if style, ok := colorscheme["color-column"]; ok {
fg, _, _ := style.Decompose()
lineStyle = lineStyle.Background(fg)
}
}
v.drawCell(screenX-v.leftCol+i, screenY, ' ', nil, lineStyle)
}
}
}
v.Bottomline = curLineN
if !v.Buf.Settings["softwrap"].(bool) {
v.Bottomline++
}
}
// DisplayCursor draws the current buffer's cursor to the screen
func (v *View) DisplayCursor() {
// Don't draw the cursor if it is out of the viewport or if it has a selection
if (v.Cursor.Y-v.Topline < 0 || v.Cursor.Y-v.Topline > v.height-1) || v.Cursor.HasSelection() {
screen.HideCursor()
} else {
screen.ShowCursor(v.x+v.Cursor.GetVisualX()+v.lineNumOffset-v.leftCol, v.Cursor.Y-v.Topline+v.y)
}
func (v *View) DisplayCursor(x, y int) {
// screen.ShowCursor(v.x+v.Cursor.GetVisualX()+v.lineNumOffset-v.leftCol, y)
screen.ShowCursor(x, y)
}
// Display renders the view, the cursor, and statusline
func (v *View) Display() {
v.DisplayView()
if v.Num == tabs[curTab].curView {
v.DisplayCursor()
// Don't draw the cursor if it is out of the viewport or if it has a selection
if (v.Cursor.Y-v.Topline < 0 || v.Cursor.Y-v.Topline > v.height-1) || v.Cursor.HasSelection() {
screen.HideCursor()
}
_, screenH := screen.Size()
if v.Buf.Settings["statusline"].(bool) {

View File

@@ -17,3 +17,4 @@ color-link current-line-number "#656866,#1D1F21"
color-link gutter-error "#FF4444,#1D1F21"
color-link gutter-warning "#EEEE77,#1D1F21"
color-link cursor-line "#2D2F31"
color-link color-column "#2D2F31"

View File

@@ -17,3 +17,4 @@ color-link gutter-error "197,231"
color-link gutter-warning "134,231"
color-link line-number "246,254"
color-link cursor-line "254"
color-link color-column "254"

View File

@@ -1,20 +1,21 @@
color-link default "188,237"
color-link comment "108,237"
color-link constant.string "174,237"
color-link constant.number "116,237"
color-link constant "181,237"
color-link identifier "223,237"
color-link statement "223,237"
color-link preproc "223,237"
color-link type "187,237"
color-link special "181,237"
color-link underlined "188,237"
color-link error "115,236"
color-link todo "bold 254,237"
color-link statusline "186,236"
color-link indent-char "238,237"
color-link line-number "188,238"
color-link gutter-error "237,174"
color-link gutter-warning "174,237"
color-link cursor-line "238"
color-link current-line-number "188,237"
# This is the monokai colorscheme
color-link default "#F8F8F2,#282828"
color-link comment "#75715E,#282828"
color-link identifier "#66D9EF,#282828"
color-link constant "#AE81FF,#282828"
color-link constant.string "#E6DB74,#282828"
color-link statement "#F92672,#282828"
color-link preproc "#CB4B16,#282828"
color-link type "#66D9EF,#282828"
color-link special "#A6E22E,#282828"
color-link underlined "#D33682,#282828"
color-link error "bold #CB4B16,#282828"
color-link todo "bold #D33682,#282828"
color-link statusline "#282828,#F8F8F2"
color-link indent-char "#505050,#282828"
color-link line-number "#AAAAAA,#323232"
color-link current-line-number "#AAAAAA,#282828"
color-link gutter-error "#CB4B16,#282828"
color-link gutter-warning "#E6DB74,#282828"
color-link cursor-line "#323232"
color-link color-column "#323232"

View File

@@ -13,3 +13,4 @@ color-link todo "bold 223,235"
color-link line-number "243,237"
color-link current-line-number "172,237"
color-link cursor-line "237"
color-link color-column "237"

View File

@@ -17,3 +17,4 @@ color-link current-line-number "#AAAAAA,#282828"
color-link gutter-error "#CB4B16,#282828"
color-link gutter-warning "#E6DB74,#282828"
color-link cursor-line "#323232"
color-link color-column "#323232"

View File

@@ -14,3 +14,4 @@ color-link current-line-number "red"
color-link gutter-error ",red"
color-link gutter-warning "red"
color-link cursor-line "white"
color-link color-column "white"

View File

@@ -16,3 +16,4 @@ color-link current-line-number "#586E75,#002833"
color-link gutter-error "#003541,#CB4B16"
color-link gutter-warning "#CB4B16,#002833"
color-link cursor-line "#003541"
color-link color-column "#003541"

View File

@@ -15,3 +15,4 @@ color-link current-line-number "brightgreen,default"
color-link gutter-error "black,brightred"
color-link gutter-warning "brightred,default"
color-link cursor-line "black"
color-link color-column "black"

View File

@@ -17,4 +17,5 @@ color-link line-number "248,238"
color-link gutter-error "237,174"
color-link gutter-warning "174,237"
color-link cursor-line "238"
color-link color-column "238"
color-link current-line-number "188,237"

View File

@@ -12,21 +12,20 @@ Micro comes with a number of colorschemes by default. Here is the list:
* simple: this is the simplest colorscheme. It uses 16 colors which are
set by your terminal
* zenburn: this is micro's default colorscheme because it looks very good
and works in 256 color terminals.
this colorscheme also has the name 'default'
* monokai: this is the monokai colorscheme; you may recognize it as
Sublime Text's default colorscheme. It requires true color to
look perfect, but the 256 color approximation looks very good as well.
It's also the default colorscheme.
* zenburn: The 'zenburn' colorscheme and works well with 256 color terminals
* solarized: this is the solarized colorscheme.
You should have the solarized color palette in your terminal to use it.
* solarized-tc: this is the solarized colorscheme for true color, just
* solarized-tc: this is the solarized colorscheme for true color; just
make sure your terminal supports true color before using it and that the
MICRO_TRUECOLOR environment variable is set to 1 before starting micro.
* monokai: this is the monokai colorscheme, you may recognize it as
sublime text's default colorscheme. It requires true color to
look perfect, but the 256 color approximation looks very good as well.
* atom-dark-tc: this colorscheme is based off of Atom's "dark" colorscheme.
It requires true color to look good.
@@ -108,6 +107,8 @@ Here is a list of the colorscheme groups that you can use:
* gutter-error
* gutter-warning
* cursor-line
* current-line-number
* color-column
Colorschemes can be placed in the `~/.config/micro/colorschemes` directory to be used.

View File

@@ -5,12 +5,13 @@ Here are the possible commands that you can use.
* `quit`: Quits micro.
* `save`: Saves the current buffer.
* `save filename?`: Saves the current buffer. If the filename is provided it will
'save as' the filename.
* `replace "search" "value" flags`: This will replace `search` with `value`.
The `flags` are optional.
At this point, there is only one flag: `c`, which enables `check` mode
which asks if you'd like to perform the replacement each time
which asks if you'd like to perform the replacement each time.
Note that `search` must be a valid regex. If one of the arguments
does not have any spaces in it, you may omit the quotes.
@@ -23,6 +24,10 @@ Here are the possible commands that you can use.
* `show option`: shows the current value of the given option.
* `eval "expression"`: Evaluates a Lua expression. Note that micro will not
print anything so you should use `messenger:Message(...)` to display a
value.
* `run sh-command`: runs the given shell command in the background. The
command's output will be displayed in one line when it finishes running.
@@ -30,19 +35,34 @@ Here are the possible commands that you can use.
keybindings above for more info about what keys and actions are available.
* `vsplit filename`: opens a vertical split with `filename`. If no filename is
provided, a vertical split is opened with an empty buffer
provided, a vertical split is opened with an empty buffer.
* `hsplit filename`: same as `vsplit` but opens a horizontal split instead of
a vertical split
a vertical split.
* `tab filename`: opens the given file in a new tab.
* `log`: opens a log of all messages and debug statements.
* `plugin install plugin_name`: installs the given plugin.
* `plugin remove plugin_name`: removes the given plugin.
* `plugin list`: lists all installed plugins.
* `plugin update`: updates all installed plugins.
* `plugin search plugin_name`: searches for the given plugin.
Note that you can find a list of all available plugins at
github.com/micro-editor/plugin-channel.
You can also see more information about the plugin manager
in the `Plugin Manager` section of the `plugins` help topic.
* `reload`: reloads all runtime files.
---
The following commands are provided by the default plugins:
* `lint`: Lint the current file for errors.
* `gofmt`: Run gofmt on the current file.
* `goimports`: Run goimports on the current file.

View File

@@ -3,18 +3,32 @@
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.
*Press CtrlQ to quit, and CtrlS to save.*
If you want to see all the keybindings press CtrlE and type `help keybindings`.
See the next section for more information about documentation and help.
### Quick-start
Press CtrlQ to quit, and CtrlS to save. Press CtrlE to start typing commands
and you can see which commands are available by pressing tab, or by
viewing the help topic `> help commands`. When I write `> ...` I mean press
CtrlE and then type whatever is there.
Move the cursor around with the mouse or the arrow keys.
If the colorscheme doesn't look good, you can change it with `> set colorscheme ...`.
You can press tab to see the available colorschemes, or see more information with
`> help colors`.
Press CtrlW to move between splits, and type `> vsplit filename` or `> hsplit filename`
to open a new split.
### Accessing more help
Micro has a built-in help system much like Vim's (although less extensive).
To use it, press CtrlE to access command mode and type in help followed by a topic.
Typing help followed by nothing will open this page.
To use it, press CtrlE to access command mode and type in `help` followed by a topic.
Typing `help` followed by nothing will open this page.
Here are the possible help topics that you can read:
@@ -26,7 +40,7 @@ Here are the possible help topics that you can read:
* colors: Explains micro's colorscheme and syntax highlighting engine and how to create your
own colorschemes or add new languages to the engine
For example to open the help page on plugins you would press CtrlE and type `help plugins`.
For example, to open the help page on plugins you would press CtrlE and type `help plugins`.
I recommend looking at the `tutorial` help file because it is short for each section and
gives concrete examples of how to use the various configuration options in micro. However,

View File

@@ -1,76 +1,87 @@
# Keybindings
Here are the default keybindings in json form which is also how
you can rebind them to your liking.
Here are the default keybindings in json format. You can rebind them to your liking, following the same format.
```json
{
"Up": "CursorUp",
"Down": "CursorDown",
"Right": "CursorRight",
"Left": "CursorLeft",
"ShiftUp": "SelectUp",
"ShiftDown": "SelectDown",
"ShiftLeft": "SelectLeft",
"ShiftRight": "SelectRight",
"AltLeft": "WordLeft",
"AltRight": "WordRight",
"AltShiftRight": "SelectWordRight",
"AltShiftLeft": "SelectWordLeft",
"CtrlLeft": "StartOfLine",
"CtrlRight": "EndOfLine",
"CtrlShiftLeft": "SelectToStartOfLine",
"CtrlShiftRight": "SelectToEndOfLine",
"CtrlUp": "CursorStart",
"CtrlDown": "CursorEnd",
"CtrlShiftUp": "SelectToStart",
"CtrlShiftDown": "SelectToEnd",
"Enter": "InsertEnter",
"Space": "InsertSpace",
"Backspace": "Backspace",
"Backspace2": "Backspace",
"Alt-Backspace": "DeleteWordLeft",
"Alt-Backspace2": "DeleteWordLeft",
"Tab": "InsertTab,IndentSelection",
"CtrlO": "OpenFile",
"CtrlS": "Save",
"CtrlF": "Find",
"CtrlN": "FindNext",
"CtrlP": "FindPrevious",
"CtrlZ": "Undo",
"CtrlY": "Redo",
"CtrlC": "Copy",
"CtrlX": "Cut",
"CtrlK": "CutLine",
"CtrlD": "DuplicateLine",
"CtrlV": "Paste",
"CtrlA": "SelectAll",
"CtrlT": "AddTab",
"CtrlRightSq": "PreviousTab",
"CtrlBackslash": "NextTab",
"Home": "Start",
"End": "End",
"CtrlHome": "CursorStart",
"CtrlEnd": "CursorEnd",
"PageUp": "CursorPageUp",
"PageDown": "CursorPageDown",
"CtrlG": "ToggleHelp",
"CtrlR": "ToggleRuler",
"CtrlL": "JumpLine",
"Delete": "Delete",
"Esc": "ClearStatus",
"CtrlB": "ShellMode",
"CtrlQ": "Quit",
"CtrlE": "CommandMode",
"CtrlW": "NextSplit",
// Emacs-style keybindings
"Alt-f": "WordRight",
"Alt-b": "WordLeft",
"Alt-a": "StartOfLine",
"Alt-e": "EndOfLine",
"Alt-p": "CursorUp",
"Alt-n": "CursorDown"
"Up": "CursorUp",
"Down": "CursorDown",
"Right": "CursorRight",
"Left": "CursorLeft",
"ShiftUp": "SelectUp",
"ShiftDown": "SelectDown",
"ShiftLeft": "SelectLeft",
"ShiftRight": "SelectRight",
"AltLeft": "WordLeft",
"AltRight": "WordRight",
"AltShiftRight": "SelectWordRight",
"AltShiftLeft": "SelectWordLeft",
"AltUp": "MoveLinesUp",
"AltDown": "MoveLinesDown",
"CtrlLeft": "StartOfLine",
"CtrlRight": "EndOfLine",
"CtrlShiftLeft": "SelectToStartOfLine",
"CtrlShiftRight": "SelectToEndOfLine",
"CtrlUp": "CursorStart",
"CtrlDown": "CursorEnd",
"CtrlShiftUp": "SelectToStart",
"CtrlShiftDown": "SelectToEnd",
"Enter": "InsertNewline",
"Space": "InsertSpace",
"CtrlH": "Backspace",
"Backspace": "Backspace",
"Alt-CtrlH": "DeleteWordLeft",
"Alt-Backspace": "DeleteWordLeft",
"Tab": "IndentSelection,InsertTab",
"Backtab": "OutdentSelection",
"CtrlO": "OpenFile",
"CtrlS": "Save",
"CtrlF": "Find",
"CtrlN": "FindNext",
"CtrlP": "FindPrevious",
"CtrlZ": "Undo",
"CtrlY": "Redo",
"CtrlC": "Copy",
"CtrlX": "Cut",
"CtrlK": "CutLine",
"CtrlD": "DuplicateLine",
"CtrlV": "Paste",
"CtrlA": "SelectAll",
"CtrlT": "AddTab",
"CtrlRightSq": "PreviousTab",
"CtrlBackslash": "NextTab",
"Home": "StartOfLine",
"End": "EndOfLine",
"CtrlHome": "CursorStart",
"CtrlEnd": "CursorEnd",
"PageUp": "CursorPageUp",
"PageDown": "CursorPageDown",
"CtrlG": "ToggleHelp",
"CtrlR": "ToggleRuler",
"CtrlL": "JumpLine",
"Delete": "Delete",
"CtrlB": "ShellMode",
"CtrlQ": "Quit",
"CtrlE": "CommandMode",
"CtrlW": "NextSplit",
"CtrlU": "ToggleMacro",
"CtrlJ": "PlayMacro",
// 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
"F1": "ToggleHelp",
"F2": "Save",
"F4": "Quit",
"F7": "Find",
"F10": "Quit",
"Esc": "Escape",
}
```
@@ -130,6 +141,8 @@ WordRight
WordLeft
SelectWordRight
SelectWordLeft
MoveLinesUp
MoveLinesDown
DeleteWordRight
DeleteWordLeft
SelectToStartOfLine
@@ -141,6 +154,7 @@ Delete
Center
InsertTab
Save
SaveAs
Find
FindNext
FindPrevious
@@ -176,7 +190,12 @@ AddTab
PreviousTab
NextTab
NextSplit
Unsplit
VSplit
HSplit
PreviousSplit
ToggleMacro
PlayMacro
```
Here is the list of all possible keys you can bind:
@@ -305,9 +324,11 @@ Tab
Esc
Escape
Enter
Backspace2
```
Note: On some old terminal emulators and on Windows machines, `CtrlH` should be used
for backspace.
Additionally, alt keys can be bound by using `Alt-key`. For example `Alt-a`
or `Alt-Up`. Micro supports an optional `-` between modifiers like `Alt` and `Ctrl`
so `Alt-a` could be rewritten as `Alta` (case matters for alt bindings). This is

View File

@@ -14,7 +14,7 @@ Here are the options that you can set:
default value: `default`
Note that the default colorschemes (default, solarized, and solarized-tc)
are not located in configDir, because they are embedded in the micro binary
are not located in configDir, because they are embedded in the micro binary.
The colorscheme can be selected from all the files in the
~/.config/micro/colorschemes/ directory. Micro comes by default with three
@@ -23,6 +23,15 @@ Here are the options that you can set:
You can read more about micro's colorschemes in the `colors` help topic
(`help colors`).
* `colorcolumn`: if this is not set to 0, it will display a column at the specified
column. This is useful if you want column 80 to be highlighted special for example.
default value: `0`
* `eofnewline`: micro will automatically add a newline to the file.
default value: `false`
* `tabsize`: sets the tab size to `option`
default value: `4`
@@ -88,24 +97,39 @@ Here are the options that you can set:
default value: `2`
* `softwrap`: should micro wrap lines that are too long to fit on the screen
default value: `off`
* `autosave`: micro will save the buffer every 8 seconds automatically.
Micro also will automatically save and quit when you exit without asking.
Be careful when using this feature, because you might accidentally save a file,
overwriting what was there before.
default value: `off`
* `pluginchannels`: contains all the channels micro's plugin manager will search
for plugins in. A channel is simply a list of 'repository' json files which contain
metadata about the given plugin. See the `Plugin Manager` section of the `plugins` help topic
for more information.
default value: `https://github.com/micro-editor/plugin-channel`
* `pluginrepos`: contains all the 'repositories' micro's plugin manager will search for
plugins in. A repository consists of a `repo.json` file which contains metadata for a
single plugin.
default value: ` `
---
Default plugin options:
* `linter`: lint languages on save (supported languages are C, D, Go, Java,
Javascript, Lua). Provided by the `linter` plugin.
* `autoclose`: Automatically close `{}` `()` `[]` `""` `''`. Provided by the `autoclose` plugin
default value: `on`
* `autoclose`: Automatically close `{}` `()` `[]` `""` `''`. Provided by the autoclose plugin
default value: `on`
* `goimports`: Run goimports on save. Provided by the `go` plugin.
default value: `off`
* `gofmt`: Run gofmt on save. Provided by the `go` plugin.
* `linter`: Automatically lint when the file is saved. Provided by the `linter` plugin
default value: `on`

View File

@@ -43,35 +43,48 @@ for functions are given using Go's type system):
* `OS`: variable which gives the OS micro is currently running on (this is the same
as Go's GOOS variable, so `darwin`, `windows`, `linux`, `freebsd`...)
* `configDir`: contains the path to the micro configuration files
* `tabs`: a list of all the tabs currently in use
* `curTab`: the index of the current tabs in the tabs list
* `messenger`: lets you send messages to the user or create prompts
* `RuneStr(r rune) string`: returns a string containing the given rune
* `Loc(x, y int) Loc`: returns a new `Loc` struct
* `JoinPaths(dir... string) string` combines multiple directories to a full path
* `GetOption(name string)`: returns the value of the requested option
* `AddOption(name string, value interface{})`: sets the given option with the given
value (`interface{}` means any type in Go).
value (`interface{}` means any type in Go)
* `SetOption(option, value string)`: sets the given option to the value. This will
set the option globally, unless it is a local only option.
* `SetLocalOption(option, value string, buffer *Buffer)`: sets the given option to
the value locally in the given buffer.
the value locally in the given buffer
* `BindKey(key, action string)`: binds `key` to `action`.
* `BindKey(key, action string)`: binds `key` to `action`
* `MakeCommand(name, function string, completions ...Completion)`:
creates a command with `name` which will call `function` when executed.
Use 0 for completions to get NoCompletion.
* `MakeCompletion(function string)`:
creates a `Completion` to use with `MakeCommand`
* `CurView()`: returns the current view
* `HandleCommand(cmd string)`: runs the given command
* `HandleShellCommand(shellCmd string, interactive bool)`: runs the given shell
command
* `HandleShellCommand(shellCmd string, interactive bool, waitToClose bool)`: runs the given shell
command. The `interactive` bool specifies whether the command should run in the background. The
`waitToClose` bool only applies if `interactive` is true and means that it should wait before
returning to the editor.
* `JobStart(cmd string, onStdout, onStderr, onExit string, userargs ...string)`:
Starts running the given shell command in the background. `onStdout` `onStderr` and `onExit`
@@ -104,7 +117,89 @@ The possible methods which you can call using the `messenger` variable are:
If you want a standard prompt, just use `messenger.Prompt(prompt, "", 0)`
# Adding help files, syntax files, or colorschemes in your plugin
You can use the `AddRuntimeFile(name, type, path string)` function to add various kinds of
files to your plugin. For example, if you'd like to add a help topic and to your plugin
called `test`, you would create the `test.md` file for example, and runt the function:
```lua
AddRuntimeFile("test", "help", "test.md")
```
Use `AddRuntimeFilesFromDirectory(name, type, dir, pattern)` to add a number of files
to the runtime.
# Autocomplete command arguments
See this example to learn how to use `MakeCompletion` and `MakeCommand`
```lua
local function StartsWith(String,Start)
String = String:upper()
Start = Start:upper()
return string.sub(String,1,string.len(Start))==Start
end
function complete(input)
local allCompletions = {"Hello", "World", "Foo", "Bar"}
local result = {}
for i,v in pairs(allCompletions) do
if StartsWith(v, input) then
table.insert(result, v)
end
end
return result
end
function foo(arg)
messenger:Message(arg)
end
MakeCommand("foo", "example.foo", MakeCompletion("example.complete"))
```
# Default plugins
For examples of plugins, see the default plugins `linter`, `go`, and `autoclose`.
They are stored in Micro's github repository [here](https://github.com/zyedidia/micro/tree/master/runtime/plugins).
For examples of plugins, see the default `autoclose` and `linter` plugins
(stored in the normal micro core repo under `runtime/plugins`) as well as
any plugins that are stored in the official channel [here](https://github.com/micro-editor/plugin-channel).
# Plugin Manager
Micro also has a built in plugin manager which you can invoke with the `> plugin ...` command.
For the valid commands you can use, see the `command` help topic.
The manager fetches plugins from the channels (which is simply a list of plugin metadata)
which it knows about. By default, micro only knows about the official channel which is located
at github.com/micro-editor/plugin-channel but you can add your own third-party channels using
the `pluginchannels` option and you can directly link third-party plugins to allow installation
through the plugin manager with the `pluginrepos` option.
If you'd like to publish a plugin you've made as an official plugin, you should upload your
plugin online (to Github preferably) and add a `repo.json` file. This file will contain the
metadata for your plugin. Here is an example:
```json
[{
"Name": "pluginname",
"Description": "Here is a nice concise description of my plugin",
"Tags": ["python", "linting"],
"Versions": [
{
"Version": "1.0.0",
"Url": "https://github.com/user/plugin/archive/v1.0.0.zip",
"Require": {
"micro": ">=1.0.3"
}
}
]
}]
```
Then open a pull request at github.com/micro-editor/plugin-channel adding a link to the
raw `repo.json` that is in your plugin repository.
To make updating the plugin work, the first line of your plugins lua code should contain the version of the plugin. (Like this: `VERSION = "1.0.0"`)
Please make sure to use [semver](http://semver.org/) for versioning.

View File

@@ -6,6 +6,17 @@ and use `init.lua` to configure micro to your liking.
Hopefully you'll find this useful.
### Plugins
Micro has a plugin manager which can automatically download plugins for you.
To see the plugins 'official' plugins, go to github.com/micro-editor/plugin-channel.
For example, if you'd like to install the snippets plugin (listed in that repo),
just press `CtrlE` to execute a command, and type `plugin install snippets`.
For more information about the plugin manager, see the end of the `plugins` help
topic.
### Settings
In micro, your settings are stored in `~/.config/micro/settings.json`, a file
@@ -80,7 +91,7 @@ You can do that by putting the following in `init.lua`:
function gorun()
local buf = CurView().Buf -- The current buffer
if buf:FileType() == "go" then
HandleShellCommand("go run " .. buf.Path, true) -- true means don't run it in the background
HandleShellCommand("go run " .. buf.Path, true, true) -- the first true means don't run it in the background
end
end

View File

@@ -6,7 +6,7 @@ if GetOption("autoclose") == nil then
AddOption("autoclose", true)
end
local autoclosePairs = {"\"\"", "''", "()", "{}", "[]"}
local autoclosePairs = {"\"\"", "''", "``", "()", "{}", "[]"}
local autoNewlinePairs = {"()", "{}", "[]"}
function onRune(r, v)

View File

@@ -1,52 +0,0 @@
if GetOption("goimports") == nil then
AddOption("goimports", false)
end
if GetOption("gofmt") == nil then
AddOption("gofmt", true)
end
MakeCommand("goimports", "go.goimports", 0)
MakeCommand("gofmt", "go.gofmt", 0)
function onViewOpen(view)
if view.Buf:FileType() == "go" then
SetLocalOption("tabstospaces", "off", view)
end
end
function onSave(view)
if CurView().Buf:FileType() == "go" then
if GetOption("goimports") then
goimports()
elseif GetOption("gofmt") then
gofmt()
end
end
end
function gofmt()
CurView():Save(false)
local handle = io.popen("gofmt -w " .. CurView().Buf.Path)
local result = handle:read("*a")
handle:close()
CurView():ReOpen()
end
function goimports()
CurView():Save(false)
local handle = io.popen("goimports -w " .. CurView().Buf.Path)
local result = split(handle:read("*a"), ":")
handle:close()
CurView():ReOpen()
end
function split(str, sep)
local result = {}
local regex = ("([^%s]+)"):format(sep)
for each in str:gmatch(regex) do
table.insert(result, each)
end
return result
end

View File

@@ -22,7 +22,7 @@ function runLinter()
elseif ft == "lua" then
lint("luacheck", "luacheck --no-color " .. file, "%f:%l:%d+: %m")
elseif ft == "python" then
lint("pyflakes", "pyflakes " .. file, "%f:%l: %m")
lint("pyflakes", "pyflakes " .. file, "%f:%l:.-:? %m")
elseif ft == "c" then
lint("gcc", "gcc -fsyntax-only -Wall -Wextra " .. file, "%f:%l:%d+:.+: %m")
elseif ft == "d" then
@@ -51,7 +51,7 @@ end
function onExit(output, linter, errorformat)
local lines = split(output, "\n")
local regex = errorformat:gsub("%%f", "(.+)"):gsub("%%l", "(%d+)"):gsub("%%m", "(.+)")
local regex = errorformat:gsub("%%f", "(..-)"):gsub("%%l", "(%d+)"):gsub("%%m", "(.+)")
for _,line in ipairs(lines) do
-- Trim whitespace
line = line:match("^%s*(.+)%s*$")

View File

@@ -2,25 +2,19 @@
syntax "dockerfile" "Dockerfile[^/]*$" "\.dockerfile$"
## Keywords
red (i) "^(FROM|MAINTAINER|RUN|CMD|LABEL|EXPOSE|ENV|ADD|COPY|ENTRYPOINT|VOLUME|USER|WORKDIR|ONBUILD)[[:space:]]"
color keyword (i) "^(FROM|MAINTAINER|RUN|CMD|LABEL|EXPOSE|ENV|ADD|COPY|ENTRYPOINT|VOLUME|USER|WORKDIR|ONBUILD|ARG|HEALTHCHECK|STOPSIGNAL|SHELL)[[:space:]]"
## Brackets & parenthesis
color brightgreen "(\(|\)|\[|\])"
color statement "(\(|\)|\[|\])"
## Double ampersand
color brightmagenta "&&"
color special "&&"
## Comments
cyan (i) "^[[:space:]]*#.*$"
## Blank space at EOL
color ,green "[[:space:]]+$"
color comment (i) "^[[:space:]]*#.*$"
## Strings, single-quoted
color brightwhite "'([^']|(\\'))*'" "%[qw]\{[^}]*\}" "%[qw]\([^)]*\)" "%[qw]<[^>]*>" "%[qw]\[[^]]*\]" "%[qw]\$[^$]*\$" "%[qw]\^[^^]*\^" "%[qw]![^!]*!"
color constant.string "'([^']|(\\'))*'" "%[qw]\{[^}]*\}" "%[qw]\([^)]*\)" "%[qw]<[^>]*>" "%[qw]\[[^]]*\]" "%[qw]\$[^$]*\$" "%[qw]\^[^^]*\^" "%[qw]![^!]*!"
## Strings, double-quoted
color brightwhite ""([^"]|(\\"))*"" "%[QW]?\{[^}]*\}" "%[QW]?\([^)]*\)" "%[QW]?<[^>]*>" "%[QW]?\[[^]]*\]" "%[QW]?\$[^$]*\$" "%[QW]?\^[^^]*\^" "%[QW]?![^!]*!"
## Single and double quotes
color brightyellow "('|\")"
color constant.string ""([^"]|(\\"))*"" "%[QW]?\{[^}]*\}" "%[QW]?\([^)]*\)" "%[QW]?<[^>]*>" "%[QW]?\[[^]]*\]" "%[QW]?\$[^$]*\$" "%[QW]?\^[^^]*\^" "%[QW]?![^!]*!"

View File

@@ -27,6 +27,7 @@ Here is a list of the files that have been converted to properly use colorscheme
* rust
* java
* javascript
* pascal
* python
* ruby
* sh

View File

@@ -11,6 +11,12 @@ color special "!important"
color identifier ":active|:focus|:hover|:link|:visited|:link|:after|:before|$"
color special "(\{|\}|\(|\)|\;|:|\]|~|<|>|,)"
# SCSS Varaibles
color identifier (i) "\$\{?[0-9A-Z_!@#$*?-]+\}?"
# SCSS Commands
color statement "@import|@mixin|@extend"
# Strings
color constant ""(\\.|[^"])*""
color constant "'(\\.|[^'])*'"

View File

@@ -1,7 +1,7 @@
## Here is a short improved example for HTML.
##
syntax "html" "\.htm[l]?$"
color identifier start="<" end=">"
color identifier "<.*?>"
color special "&[^;[[:space:]]]*;"
color constant ""[^"]*"|qq\|.*\|"
color statement "(alt|bgcolor|height|href|label|longdesc|name|onclick|onfocus|onload|onmouseover|size|span|src|style|target|type|value|width)="

View File

@@ -0,0 +1,24 @@
syntax "pascal" "\.pas$"
# color identifier "\b[\pL_][\pL_\pN]*\b"
color comment "//.*"
color comment start="\(\*" end="\*\)"
color comment start="({)(?:[^$])" end="}"
color special start="asm" end="end"
color type "\b(?i:(string|ansistring|widestring|shortstring|char|ansichar|widechar|boolean|byte|shortint|word|smallint|longword|cardinal|longint|integer|int64|single|currency|double|extended))\b"
color statement "\b(?i:(and|asm|array|begin|break|case|const|constructor|continue|destructor|div|do|downto|else|end|file|for|function|goto|if|implementation|in|inline|interface|label|mod|not|object|of|on|operator|or|packed|procedure|program|record|repeat|resourcestring|set|shl|shr|then|to|type|unit|until|uses|var|while|with|xor))\b"
color statement "\b(?i:(as|class|dispose|except|exit|exports|finalization|finally|inherited|initialization|is|library|new|on|out|property|raise|self|threadvar|try))\b"
color statement "\b(?i:(absolute|abstract|alias|assembler|cdecl|cppdecl|default|export|external|forward|generic|index|local|name|nostackframe|oldfpccall|override|pascal|private|protected|public|published|read|register|reintroduce|safecall|softfloat|specialize|stdcall|virtual|write))\b"
color constant "\b(?i:(false|true|nil))\b"
color constant "\$[0-9A-Fa-f]+" "\b[+-]?[0-9]+([.]?[0-9]+)?(?i:e[+-]?[0-9]+)?"
color constant.string "'(?:[^']+|'')*'"
color preproc start="{\$" end="}"

View File

@@ -3,7 +3,7 @@ color default start="<\?(php|=)?" end="\?>"
color special "([a-zA-Z0-9_-]+)\("
color identifier "(var|class|goto|extends|function|echo|case|break|default|exit|switch|foreach|endforeach|while|const|static|extends|as|require|include|require_once|include_once|define|do|continue|declare|goto|print|in|interface|[E|e]xception|array|int|string|bool)[\s|\)]"
color identifier "(var|class|goto|extends|function|echo|case|break|default|exit|switch|foreach|endforeach|while|const|static|extends|as|require|include|require_once|include_once|define|do|continue|declare|goto|print|in|trait|interface|[E|e]xception|array|int|string|bool|iterable|void)[\s|\)]"
color identifier "[a-zA-Z\\]+::"
@@ -27,4 +27,4 @@ color default "[\(|\)|/|+|-|\*|\[|,|;]"
color constant.string "('.*?'|\".*?\")"
color comment start="/\*" end="\*/"
color comment "(#.*|//.*)$"
color comment "(#.*|//.*)$"

View File

@@ -1,23 +0,0 @@
## Arch PKGBUILD files
##
syntax "pkgbuild" "^.*PKGBUILD$"
color green start="^." end="$"
color cyan "^.*(pkgbase|pkgname|pkgver|pkgrel|pkgdesc|arch|url|license).*=.*$"
color brightcyan "\<(pkgbase|pkgname|pkgver|pkgrel|pkgdesc|arch|url|license)\>"
color brightcyan "(\$|\$\{|\$\()(pkgbase|pkgname|pkgver|pkgrel|pkgdesc|arch|url|license)(\}|\))"
color cyan "^.*(depends|makedepends|optdepends|conflicts|provides|replaces).*=.*$"
color brightcyan "\<(depends|makedepends|optdepends|conflicts|provides|replaces)\>"
color brightcyan "(\$|\$\{|\$\()(depends|makedepends|optdepends|conflicts|provides|replaces)(\}|\))"
color cyan "^.*(groups|backup|noextract|options).*=.*$"
color brightcyan "\<(groups|backup|noextract|options)\>"
color brightcyan "(\$|\$\{|\$\()(groups|backup|noextract|options)(\}|\))"
color cyan "^.*(install|source|md5sums|sha1sums|sha256sums|sha384sums|sha512sums).*=.*$"
color brightcyan "\<(install|source|md5sums|sha1sums|sha256sums|sha384sums|sha512sums)\>"
color brightcyan "(\$|\$\{|\$\()(install|source|md5sums|sha1sums|sha256sums|sha384sums|sha512sums)(\}|\))"
color brightcyan "\<(startdir|srcdir|pkgdir)\>"
color cyan "\.install"
color brightwhite "=" "'" "\(" "\)" "\"" "#.*$" "\," "\{" "\}"
color brightred "build\(\)"
color brightred "package_.*.*$"
color brightred "\<(configure|make|cmake|scons)\>"
color red "\<(DESTDIR|PREFIX|prefix|sysconfdir|datadir|libdir|includedir|mandir|infodir)\>"

View File

@@ -0,0 +1,22 @@
syntax "typescript" "\.ts$"
color constant.number "\b[-+]?([1-9][0-9]*|0[0-7]*|0x[0-9a-fA-F]+)([uU][lL]?|[lL][uU]?)?\b"
color constant.number "\b[-+]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)([EePp][+-]?[0-9]+)?[fFlL]?"
color constant.number "\b[-+]?([0-9]+[EePp][+-]?[0-9]+)[fFlL]?"
color identifier "[A-Za-z_][A-Za-z0-9_]*[[:space:]]*[(]"
color statement "\b(abstract|as|async|await|break|case|catch|class|const|constructor|continue)\b"
color statement "\b(debugger|declare|default|delete|do|else|enum|export|extends|finally|for|from)\b"
color statement "\b(function|get|if|implements|import|in|instanceof|interface|is|let|module|namespace)\b"
color statement "\b(new|of|package|private|protected|public|require|return|set|static|super|switch)\b"
color statement "\b(this|throw|try|type|typeof|var|void|while|with|yield)\b"
color constant "\b(false|true|null|undefined|NaN)\b"
color type "\b(Array|Boolean|Date|Enumerator|Error|Function|Math)\b"
color type "\b(Number|Object|RegExp|String|Symbol)\b"
color type "\b(any|boolean|never|number|string|symbol)\b"
color statement "[-+/*=<>!~%?:&|]"
color constant "/[^*]([^/]|(\\/))*[^\\]/[gim]*"
color constant "\\[0-7][0-7]?[0-7]?|\\x[0-9a-fA-F]+|\\[bfnrt'"\?\\]"
color comment "(^|[[:space:]])//.*"
color comment "/\*.+\*/"
color todo "TODO:?"
color constant.string ""(\\.|[^"])*"|'(\\.|[^'])*'"

View File

@@ -1,11 +1,7 @@
## Here is an example for xml files.
##
syntax "xml" "\.([jrs]?html?|xml|sgml?|rng|plist)$"
color white "^.+$"
color green start="<" end=">"
color cyan "<[^> ]+"
color cyan ">"
color yellow start="<!DOCTYPE" end="[/]?>"
color yellow start="<!--" end="-->"
color red "&[^;]*;"
syntax "xml" "\.(xml|sgml?|rng|plist)$"
color identifier "<.*?>"
color comment start="<!DOCTYPE" end="[/]?>"
color comment start="<!--" end="-->"
color special "&[^;]*;"

View File

@@ -1,11 +1,12 @@
syntax "yaml" "\.ya?ml$"
header "^---" "%YAML"
header "%YAML"
color type "(^| )!!(binary|bool|float|int|map|null|omap|seq|set|str) "
color constant "\b(YES|yes|Y|y|ON|on|NO|no|N|n|OFF|off)\b"
color constant "\b(true|false)\b"
color statement ":[[:space:]]" "\[" "\]" ":[[:space:]]+[|>]" "^[[:space:]]*- "
color identifier "[[:space:]][\*&][A-Za-z0-9]+"
color type "([-\w]+:\s+)|([-\w]+:$)"
color constant.string ""(\\.|[^"])*"|'(\\.|[^'])*'"
color comment "(^|[[:space:]])#([^{].*)?$"
color special "^---" "^\.\.\." "^%YAML" "^%TAG"

19
snapcraft.yaml Normal file
View File

@@ -0,0 +1,19 @@
name: micro
version: master
summary: A modern and intuitive terminal-based text editor
description: |
Micro is a terminal-based text editor that aims to be easy to use and
intuitive, while also taking advantage of the full capabilities of modern
terminals.
confinement: strict
apps:
micro:
command: bin/micro
plugs: [home]
parts:
micro:
source: .
plugin: go
go-importpath: github.com/zyedidia/micro

10
tools/build-date.go Normal file
View File

@@ -0,0 +1,10 @@
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println(time.Now().Local().Format("January 02, 2006"))
}

65
tools/build-version.go Normal file
View File

@@ -0,0 +1,65 @@
package main
import (
"fmt"
"os/exec"
"strings"
"github.com/blang/semver"
)
func getTag(match ...string) (string, *semver.PRVersion) {
args := append([]string{
"describe", "--tags",
}, match...)
if tag, err := exec.Command("git", args...).Output(); err != nil {
return "", nil
} else {
tagParts := strings.Split(string(tag), "-")
if len(tagParts) == 3 {
if ahead, err := semver.NewPRVersion(tagParts[1]); err == nil {
return tagParts[0], &ahead
}
}
return tagParts[0], nil
}
}
func main() {
// Find the last vX.X.X Tag and get how many builds we are ahead of it.
versionStr, ahead := getTag("--match", "v*")
version, err := semver.ParseTolerant(versionStr)
if err != nil {
// no version tag found so just return what ever we can find.
fmt.Println("0.0.0-unknown")
return
}
// Get the tag of the current revision.
tag, _ := getTag("--exact-match")
if tag == versionStr {
// Seems that we are going to build a release.
// So the version number should already be correct.
fmt.Println(version.String())
return
}
// If we don't have any tag assume "dev"
if tag == "" {
tag = "dev"
}
// Get the most likely next version:
version.Patch = version.Patch + 1
if pr, err := semver.NewPRVersion(tag); err == nil {
// append the tag as pre-release name
version.Pre = append(version.Pre, pr)
}
if ahead != nil {
// if we know how many commits we are ahead of the last release, append that too.
version.Pre = append(version.Pre, *ahead)
}
fmt.Println(version.String())
}

View File

@@ -7,54 +7,56 @@ cp LICENSE micro-$1
cp README.md micro-$1
HASH="$(git rev-parse --short HEAD)"
VERSION="$(go run tools/build-version.go)"
DATE="$(go run tools/build-date.go)"
# Mac
echo "OSX 64"
GOOS=darwin GOARCH=amd64 go build -ldflags "-X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$(date -u '+%B %d, %Y')'" -o micro-$1/micro ./cmd/micro
GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w -X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$DATE'" -o micro-$1/micro ./cmd/micro
tar -czf micro-$1-osx.tar.gz micro-$1
mv micro-$1-osx.tar.gz binaries
# Linux
echo "Linux 64"
GOOS=linux GOARCH=amd64 go build -ldflags "-X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$(date -u '+%B %d, %Y')'" -o micro-$1/micro ./cmd/micro
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$DATE'" -o micro-$1/micro ./cmd/micro
tar -czf micro-$1-linux64.tar.gz micro-$1
mv micro-$1-linux64.tar.gz binaries
echo "Linux 32"
GOOS=linux GOARCH=386 go build -ldflags "-X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$(date -u '+%B %d, %Y')'" -o micro-$1/micro ./cmd/micro
GOOS=linux GOARCH=386 go build -ldflags "-s -w -X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$DATE'" -o micro-$1/micro ./cmd/micro
tar -czf micro-$1-linux32.tar.gz micro-$1
mv micro-$1-linux32.tar.gz binaries
echo "Linux arm"
GOOS=linux GOARCH=arm go build -ldflags "-X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$(date -u '+%B %d, %Y')'" -o micro-$1/micro ./cmd/micro
GOOS=linux GOARCH=arm go build -ldflags "-s -w -X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$DATE'" -o micro-$1/micro ./cmd/micro
tar -czf micro-$1-linux-arm.tar.gz micro-$1
mv micro-$1-linux-arm.tar.gz binaries
# NetBSD
echo "NetBSD 64"
GOOS=netbsd GOARCH=amd64 go build -ldflags "-X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$(date -u '+%B %d, %Y')'" -o micro-$1/micro ./cmd/micro
GOOS=netbsd GOARCH=amd64 go build -ldflags "-s -w -X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$DATE'" -o micro-$1/micro ./cmd/micro
tar -czf micro-$1-netbsd64.tar.gz micro-$1
mv micro-$1-netbsd64.tar.gz binaries
echo "NetBSD 32"
GOOS=netbsd GOARCH=386 go build -ldflags "-X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$(date -u '+%B %d, %Y')'" -o micro-$1/micro ./cmd/micro
GOOS=netbsd GOARCH=386 go build -ldflags "-s -w -X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$DATE'" -o micro-$1/micro ./cmd/micro
tar -czf micro-$1-netbsd32.tar.gz micro-$1
mv micro-$1-netbsd32.tar.gz binaries
# OpenBSD
echo "OpenBSD 64"
GOOS=openbsd GOARCH=amd64 go build -ldflags "-X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$(date -u '+%B %d, %Y')'" -o micro-$1/micro ./cmd/micro
GOOS=openbsd GOARCH=amd64 go build -ldflags "-s -w -X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$DATE'" -o micro-$1/micro ./cmd/micro
tar -czf micro-$1-openbsd64.tar.gz micro-$1
mv micro-$1-openbsd64.tar.gz binaries
echo "OpenBSD 32"
GOOS=openbsd GOARCH=386 go build -ldflags "-X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$(date -u '+%B %d, %Y')'" -o micro-$1/micro ./cmd/micro
GOOS=openbsd GOARCH=386 go build -ldflags "-s -w -X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$DATE'" -o micro-$1/micro ./cmd/micro
tar -czf micro-$1-openbsd32.tar.gz micro-$1
mv micro-$1-openbsd32.tar.gz binaries
# FreeBSD
echo "FreeBSD 64"
GOOS=freebsd GOARCH=amd64 go build -ldflags "-X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$(date -u '+%B %d, %Y')'" -o micro-$1/micro ./cmd/micro
GOOS=freebsd GOARCH=amd64 go build -ldflags "-s -w -X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$DATE'" -o micro-$1/micro ./cmd/micro
tar -czf micro-$1-freebsd64.tar.gz micro-$1
mv micro-$1-freebsd64.tar.gz binaries
echo "FreeBSD 32"
GOOS=freebsd GOARCH=386 go build -ldflags "-X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$(date -u '+%B %d, %Y')'" -o micro-$1/micro ./cmd/micro
GOOS=freebsd GOARCH=386 go build -ldflags "-s -w -X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$DATE'" -o micro-$1/micro ./cmd/micro
tar -czf micro-$1-freebsd32.tar.gz micro-$1
mv micro-$1-freebsd32.tar.gz binaries
@@ -62,11 +64,11 @@ rm micro-$1/micro
# Windows
echo "Windows 64"
GOOS=windows GOARCH=amd64 go build -ldflags "-X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$(date -u '+%B %d, %Y')'" -o micro-$1/micro.exe ./cmd/micro
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$DATE'" -o micro-$1/micro.exe ./cmd/micro
zip -r -q -T micro-$1-win64.zip micro-$1
mv micro-$1-win64.zip binaries
echo "Windows 32"
GOOS=windows GOARCH=386 go build -ldflags "-X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$(date -u '+%B %d, %Y')'" -o micro-$1/micro.exe ./cmd/micro
GOOS=windows GOARCH=386 go build -ldflags "-s -w -X main.Version=$1 -X main.CommitHash=$HASH -X 'main.CompileDate=$DATE'" -o micro-$1/micro.exe ./cmd/micro
zip -r -q -T micro-$1-win32.zip micro-$1
mv micro-$1-win32.zip binaries