Compare commits

..

221 Commits

Author SHA1 Message Date
cutelisp
5eddf5b85d
Change MainTab calls (#3750)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
Swaping `MainTab` calls to `h.tab` will not change the normal micro
behaviour.
This changes will make possible to call `ForceQuit`, `VSplitIndex` and
`HSplitIndex` for tabs that aren't main one.
2025-05-26 22:07:14 +02:00
Jöran Karl
cd0dc9a701
options: Add truecolor to control the usage (#2867)
- `auto`: enable usage of true color if it is supported, otherwise disable it
- `on`: force usage of true color
- `off`: disable true color usage

Co-authored-by: Dmytro Maluka <dmitrymaluka@gmail.com>
2025-05-26 18:25:07 +02:00
Codemanticism
bf255b6c35
rust.yaml: Add the keyword "union" (#3759)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2025-05-25 12:59:59 +02:00
Dmytro Maluka
98ff79dbca
Relocate buffer view after setting options that affect it (#3743)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
Whenever the user changes the value of an option that affects the
calculation of display params in updateDisplayInfo(), we should
immediately recalculate those params and relocate the buffer view
accordingly.

For example: after enabling ruler via `set ruler on`, if the cursor is
currently at the rightmost edge of the bufpane and softwrap is disabled,
then the cursor becomes invisible (since it is now outside the view,
since the buffer view width is decreased as a result of adding the ruler
but StartCol is not updated accordingly), it only becomes visible again
after the user types a character (or performs some other action that
triggers updateDisplayInfo() + relocate). Fix it.
2025-05-11 16:20:23 +02:00
cutelisp
44d0368747
FIX: ruler drawn on top of the tab bar (#3744)
Wrap function lacked a condition to avoid drawing below 0.
2025-05-11 15:32:35 +02:00
cutelisp
895d9d2c82
Fix Scrollbar covering cursor (#3741) 2025-05-11 15:22:59 +02:00
Jöran Karl
809db4ee24
Merge pull request #3738 from JoeKar/fix/termcmd
Some checks are pending
Build and Test / test (1.19.x, macos-latest) (push) Waiting to run
Build and Test / test (1.19.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.19.x, windows-latest) (push) Waiting to run
Build and Test / test (1.23.x, macos-latest) (push) Waiting to run
Build and Test / test (1.23.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.23.x, windows-latest) (push) Waiting to run
command: Fix crash caused by `TermCmd()`
2025-05-10 20:29:20 +02:00
Jöran Karl
58b6917526 command: Apply small cosmetics to openTerm() 2025-05-08 06:27:11 +02:00
Jöran Karl
63b6a1e6cf command: Extract term() as dedicated openTerm() function 2025-05-08 06:27:07 +02:00
Jöran Karl
4769a94fb1 command: Exit loop in TermCmd() after terminal call
Otherwise the last opened pane is closed instead of the active one.
2025-05-07 19:33:31 +02:00
Shinsuke Nashimoto
91832d0016
Fixed a broken colorscheme (sunny-day) due to a typo (#3735)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2025-05-06 20:38:45 +02:00
Jöran Karl
06fe85c8c9
Merge pull request #3708 from Neko-Box-Coder/ChainedParentsFix
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
Fix unable to perform proportional resize caused by chained parents after quiting a nested VSplit inside a HSplit
2025-04-29 21:06:19 +02:00
cutelisp
ca32ffbb4a
Change variable visibility (#3720)
Changed DoubleClick and TripleClick to public so they can be accessed by
Lua plugins.
2025-04-29 20:55:01 +02:00
cutelisp
b61c8a4e1a
Deleted duplicated line (#3728)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2025-04-28 19:55:03 +02:00
Jöran Karl
333770bbd0
Merge pull request #3727 from JoeKar/fix/path2filepath
Some checks are pending
Build and Test / test (1.19.x, macos-latest) (push) Waiting to run
Build and Test / test (1.19.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.19.x, windows-latest) (push) Waiting to run
Build and Test / test (1.23.x, macos-latest) (push) Waiting to run
Build and Test / test (1.23.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.23.x, windows-latest) (push) Waiting to run
Convert leftover usages of `path` to `filepath`
2025-04-27 13:16:00 +02:00
Jöran Karl
7e583fe6ff lua: Remove duplicated export of filepath.Join() 2025-04-26 20:37:51 +02:00
Jöran Karl
1eef4bb3e0 Convert leftover usages of path to filepath 2025-04-26 20:37:49 +02:00
Neko Box Coder
8e7089993d
Simplifying unsplit logic 2025-04-26 18:11:19 +01:00
Neko Box Coder
080d216ffd
Fixing chained parents by flattening the tree on unsplit 2025-04-26 18:11:19 +01:00
Jöran Karl
e5c3a3edc3
Merge pull request #3719 from niten94/sbuf-switch-skipsave
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
Skip save on `open` or `term` command if buffer is shared
2025-04-24 13:36:16 +02:00
niten94
c457ae421a Generalize save prompt on close code into method
This slightly changes the open and term command to be similar with the
Quit action, where the buffer or pane is replaced after the prompts are
completed if "n" wasn't pressed after the 1st prompt.
2025-04-18 19:21:27 +08:00
niten94
0d5b2b73e3 Skip save on open or term command if buffer is shared 2025-04-18 19:19:19 +08:00
Mikko
79fe4ae3e3
fix cycling through completion suggestions ending in non-word character (#3650)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2025-04-15 21:02:41 +02:00
cutelisp
b88300ef7f
Fix comment (#3716)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2025-04-12 12:27:00 +02:00
Jöran Karl
d36e7f4057
Merge pull request #3714 from niten94/terminal-20250105
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
Update `micro-editor/terminal` and support terminal emulator in platforms
2025-04-09 20:08:08 +02:00
niten94
2bb3c9aa73 Add Solaris and Illumos targets in cross-compile.sh 2025-04-05 17:49:57 +08:00
niten94
f2454c9248 Specify tags where term emulation is unsupported
Copy build constraints in actions_other.go to terminal_unsupported.go,
to avoid maintaining separate build constraints that are similar with
terminal_supported.go.
2025-04-05 17:49:57 +08:00
niten94
a699cac3be Support term emulation on solaris, netbsd, openbsd/*
Support terminal emulation on platforms below:
- Solaris
- NetBSD: Supported in creack/pty since v1.1.12
- OpenBSD with GOOS != amd64
  - Other architectures are supported now in creack/pty
2025-04-05 17:49:57 +08:00
niten94
9fdf5f3a26 Update micro-editor/terminal to 2025-03-24
Syscall is replaced with IoctlSetWinsize in this version to fix a build
error that occurs with Solaris.
2025-04-05 17:49:36 +08:00
Jöran Karl
f4d62a498b
Merge pull request #3704 from dmaluka/gofmt-cleanup
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
gofmt cleanup
2025-03-25 20:19:52 +01:00
Dmytro Maluka
948b05745f Fix remaining gofmt complaints 2025-03-24 23:04:06 +01:00
Dmytro Maluka
eadc402ae0 Import paths: fix non-alphabetic order
Make gofmt happy about that.
2025-03-24 23:01:48 +01:00
Dmytro Maluka
1bd86a8f79 Build constraints: switch to new syntax
Make gofmt happy about that.
2025-03-24 22:54:32 +01:00
Jöran Karl
219fb12482
Merge pull request #3697 from JoeKar/doc/syntax
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
doc: syntax: Add hint about incompatibilities to previous versions
2025-03-15 20:29:04 +01:00
Jöran Karl
02e69dddbe doc: syntax: Add hint about incompatibilities to previous versions 2025-03-15 17:45:45 +01:00
Sertonix
78f0a9cd30 doc: syntax: remove syntax_checker.go from README.md
The patch is taken from:
https://github.com/zyedidia/micro/pull/2738

The mentioned file was already removed with the following commit:
fe3186ba9d

The `micro` binary itself takes now care of validating the syntax definitions
and informs about possible issues.
2025-03-15 14:58:14 +01:00
Dmytro Maluka
0b75031ac5
syntax: asm: highlight C-like comments (#3696)
Different assemblers have different syntaxes for comments: ";", "#",
"!", "|", "@", "*" and finally C-like comments "//" and "/* ... */".

Micro currently highlights only ";". This is causing various problems
with broken highlighting with other types of comments (i.e. those not
recognized by micro as comments), when the text in those comments
contains special characters, causing wrong highlighting of text after
them.

On the other hand, highlighting comments like "#", "|" etc would cause
conflicts with other syntax elements, e.g. constants in ARM assembly,
preprocessor directives, arithmetic expressions etc.

So let's highlight at least C-like comments. They are quite commonly
used and they are not so likely to cause conflicts with other syntax
elements.
2025-03-15 14:29:45 +01:00
Dmytro Maluka
fa317456e9
Merge pull request #3689 from Neko-Box-Coder/BetterSaveCmd
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
Updating SaveCmd to use saveBufToFile instead
2025-03-12 22:11:51 +01:00
Neko Box Coder
82b700390d
ReloadSettings only when we need to when saving a file (#3688) 2025-03-12 22:06:24 +01:00
Neko Box Coder
9003243178
Removing the use of SetName() for file buffers 2025-03-12 19:24:36 +00:00
Mikko
7d16dcdaa6
List more bindable actions in help keybindings (#3685)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2025-03-11 07:35:24 +01:00
Neko Box Coder
c9f12206e9
Updating SaveCmd to use saveBufToFile instead 2025-03-11 03:11:03 +00:00
Jöran Karl
98356765c1
Merge pull request #3273 from JoeKar/fix/save-atomically
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
save: Perform write process safe
2025-03-08 14:04:41 +01:00
Dmytro Maluka
2ae9812f47
Merge pull request #3673 from niten94/status-pass-repodir
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
`status.lua`: Display commit and branch of repository where file is located
2025-03-04 20:15:23 +01:00
niten94
85e2b3bd86 status.lua: Display hash and branch of file
Return current commit hash and branch of repository where file in buffer
is located instead of current directory.
2025-03-02 09:40:27 +08:00
edwloef
3c68655f33
add syntax highlighting for new rust keywords and types (#3677)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2025-03-01 13:58:30 +01:00
Jöran Karl
8b21724c6e buffer: Store the encoding inside the buffer 2025-02-28 19:02:16 +01:00
Jöran Karl
fe134b92d5 history: Perform write process safe 2025-02-28 18:57:53 +01:00
Jöran Karl
6164050425 save: Update the modification time of the buffer only in case of file changes 2025-02-28 18:57:53 +01:00
Jöran Karl
49aebe8aca save+util: Provide a meaningful error message for safe (over-)write fails 2025-02-28 18:57:53 +01:00
Jöran Karl
79ce93fb7d backup: Clear the requested backup upon completion notification
Now the main go routine takes care of the backup synchronization.
2025-02-28 18:57:53 +01:00
Jöran Karl
771aab251c save+backup: Process the save & backup with a sequential channel
As advantage we don't need to synchonize them any longer and
don't need further insufficient lock mechanisms.
2025-02-28 18:57:53 +01:00
Jöran Karl
35d295dd04 buffer: Remove superfluous backupTime 2025-02-28 18:57:53 +01:00
Jöran Karl
8c883c6210 backup: Rearrange and extend BackupMsg 2025-02-28 18:57:53 +01:00
Jöran Karl
c4dcef3e66 micro: Provide recovery of settings.json & bindings.json 2025-02-28 18:57:53 +01:00
Jöran Karl
e15bb88270 micro: Generalize exit behavior 2025-02-28 18:57:53 +01:00
Jöran Karl
9592bb1549 save: Further rework of overwriteFile()
- extract the open logic into `openFile()` and return a `wrappedFile`
- extract the closing logic into `Close()` and make a method of `wrappedFile`
- rename `writeFile()` into `Write()` and make a method of `wrappedFile`

This allows to use the split parts alone while keeping overwriteFile() as simple
interface to use all in a row.
2025-02-28 18:57:53 +01:00
Jöran Karl
f8d98558f0 save: Merge overwrite() into overwriteFile() and extract writeFile() 2025-02-28 18:57:53 +01:00
Jöran Karl
c926649496 settings: Perform write process safe 2025-02-28 18:57:53 +01:00
Jöran Karl
63d68ec441 bindings: Perform write process safe 2025-02-28 18:57:53 +01:00
Jöran Karl
c972360386 serialize: Perform write process safe 2025-02-28 18:57:53 +01:00
Jöran Karl
022ec0228a util: Provide SafeWrite() to generalize the internal file write process
SafeWrite() will create a temporary intermediate file.
2025-02-28 18:57:53 +01:00
Jöran Karl
4ac8c786f5 backup: Perform write process safe 2025-02-28 18:57:53 +01:00
Jöran Karl
21b7080935 util: Provide AppendBackupSuffix() for further transformations 2025-02-28 18:57:53 +01:00
Jöran Karl
1663a1a6e4 actions: Don't overwrite the buffers Path
This is fully handled within the buffers `save` domain.
2025-02-28 18:57:53 +01:00
Jöran Karl
9b53257e50 save: Perform write process safe 2025-02-28 18:57:53 +01:00
Jöran Karl
6e8daa117a ioutil: Remove deprecated functions where possible 2025-02-28 18:57:53 +01:00
Jöran Karl
18a81f043c util: Generalize the file mode of 0666 with util.FileMode 2025-02-28 18:57:53 +01:00
Jöran Karl
69064cf808 util: Improve and rename EscapePath() to DetermineEscapePath()
If the new URL encoded path is found then it has precedence over the '%' escaped
path. In case none of both is found the new URL approach is used.
2025-02-28 18:57:53 +01:00
Jöran Karl
e828027cc0 clean: Remove some unneeded filepath.Join() calls 2025-02-28 18:57:53 +01:00
Jöran Karl
c2bc4688dd clean: Inform about all failed write steps 2025-02-28 18:57:53 +01:00
Jöran Karl
5aac42dbe7 bindings: Convert os.IsNotExist() into errors.Is() 2025-02-28 18:57:53 +01:00
Jöran Karl
42ae05b082 backup: Lock the buffer lines in Backup() 2025-02-28 18:57:53 +01:00
Jöran Karl
0b871e174f backup: Store the file with the endings of the buffer 2025-02-28 18:57:53 +01:00
Jöran Karl
7c659d1820 backup: Convert os.IsNotExist() into errors.Is() 2025-02-28 18:57:53 +01:00
Jöran Karl
6066c1a10e buffer: Convert os.Is() into errors.Is() 2025-02-28 18:57:53 +01:00
Jöran Karl
6bcec2100c open & write: Process regular files only 2025-02-28 18:57:53 +01:00
Jöran Karl
edc5ff75e3 save: Convert os.IsNotExist() into errors.Is() 2025-02-28 18:57:53 +01:00
Jöran Karl
3fcaf16074 actions: SaveCmd: Print the error of SaveAs to the InfoBar 2025-02-28 18:57:53 +01:00
Jöran Karl
5c21241fc4 actions: SaveAs: Print the error of os.Stat() to the InfoBar 2025-02-28 18:57:53 +01:00
Jöran Karl
272a308275
Merge pull request #3663 from niten94/sh-separate-paramexp
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
`sh.yaml`: Match valid parameter expansions without braces
2025-02-27 19:03:30 +01:00
Jöran Karl
c93747926d
Merge pull request #3662 from JoeKar/fix/reload-settings
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
buffer: Fix `ReloadSettings(true)` for volatile `filetype`
2025-02-24 18:01:02 +01:00
niten94
0985d2cadd sh.yaml: Match parameter expansions with braces using \w 2025-02-23 11:39:11 +08:00
Jöran Karl
ddc6051b33 actions: Use SetOptionNative() instead of setting options directly
Setting options directly in (h.)Buf.Settings without calling SetOption() or
SetOptionNative() is generally not the best idea, since it may not
trigger the needed side effects.
In particular, after https://github.com/zyedidia/micro/pull/3343,
directly setting `diffgutter` and `ruler` causes them not being tracked as
locally overridden per buffer, so if we run the `reload` command,
it unexpectedly replaces them with the default ones.
2025-02-20 20:24:07 +01:00
Jöran Karl
2e94235905 buffer: Perform filetype callbacks on ReloadSettings()
In `ReloadSettings()` the `filetype` can change upon globbed path given by
the `settings.json` or by identifying a different `filetype` based on the
file name given or pattern present inside the file.
To prevent further recursion caused by checking the `filetype` again, its
processing stops here and isn't considered in `DoSetOptionNative()`
once again where the callbacks are usually triggered.
2025-02-20 20:24:05 +01:00
Jöran Karl
4a9058c3bd buffer: Move UpdatePathGlobLocals() before updating the filetype
Like in NewBuffer(), we need to update glob-based local settings
before updating the filetype, since the filetype itself may be among those
glob-based local settings.
2025-02-20 20:20:38 +01:00
Jöran Karl
982a4fe065 config: Prevent the update of filetype by UpdateFileTypeLocals()
This shall prevent unpredictable results caused by such a user configuration:

```
"ft:go" {
	"filetype": "c"
}
```
2025-02-20 20:18:36 +01:00
Jöran Karl
930fbea74d config: Split up InitLocalSettings() into two dedicated functions
* `UpdatePathGlobLocals()`
	* to apply the settings provided within e.g. "/etc/*": {}
* `UpdateFileTypeLocals()`
	* to apply the settings provided within e.g. "ft:shell": {}

We don't need to call `InitLocalSettings()` twice any longer.
2025-02-20 20:18:30 +01:00
Jöran Karl
00e568640c buffer: Fix ReloadSettings(true) for volatile filetype
We shall not overwrite a volatile set `filetype` provided as argument for micro:
`micro -filetype shell foo`
2025-02-17 20:30:20 +01:00
niten94
d992c606c5 status.lua: Move import lines to beginning of file 2025-02-15 23:17:33 +08:00
niten94
4abd966a99 sh.yaml: Match valid parameter expansions without braces
Match parameter expansions with valid name only in shell syntax file
when there are no braces.
2025-02-15 07:54:22 +08:00
matthias314
5a62a8ead4
match beginning and end of line correctly in FindNext and ReplaceRegex (#3575)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2025-02-09 15:19:43 +01:00
Jöran Karl
bf4156c490
Merge pull request #3657 from Andriamanitra/PR3656-continuation
Some checks are pending
Build and Test / test (1.19.x, macos-latest) (push) Waiting to run
Build and Test / test (1.19.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.19.x, windows-latest) (push) Waiting to run
Build and Test / test (1.23.x, macos-latest) (push) Waiting to run
Build and Test / test (1.23.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.23.x, windows-latest) (push) Waiting to run
plugin: linter: add ruff to default configuration
2025-02-08 16:19:59 +01:00
Andriamanitra
b9f1fc8da2 add missing linters to help linter 2025-02-07 23:56:25 +02:00
magneticminou
728526682e
plugin: linter: add ruff to documentation 2025-02-07 14:12:29 -03:00
magneticminou
c105c940fe
plugin: linter: change in ruff configuration
Use `--output-format concise` as suggested to get exact column of error

Co-authored-by: Mikko <Andriamanitra@users.noreply.github.com>
2025-02-06 19:28:47 -03:00
magneticminou
cdc9ab17f2
plugin: linter: add ruff to default configuration. 2025-02-06 17:05:04 -03:00
usfbih8u
b432bb7cfa
docs: remove duplicated line (#3647)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2025-02-01 09:41:25 +01:00
Jöran Karl
e4b0ad7107
Merge pull request #3620 from JoeKar/feature/cursor-overwrite-indicator
Some checks are pending
Build and Test / test (1.19.x, macos-latest) (push) Waiting to run
Build and Test / test (1.19.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.19.x, windows-latest) (push) Waiting to run
Build and Test / test (1.23.x, macos-latest) (push) Waiting to run
Build and Test / test (1.23.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.23.x, windows-latest) (push) Waiting to run
statusline: Provide `overwrite` mode indicator
2025-01-31 17:59:36 +01:00
Jöran Karl
57a6e81ddb statusline: Provide overwrite mode indicator 2025-01-30 20:19:37 +01:00
Jöran Karl
5ee7fb6014
Merge pull request #3576 from niten94/optmd-quote-sort
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
`options.md`: Add, sort entries and adjust formatting
2025-01-29 17:13:53 +01:00
niten94
7aa72b6a96 Sort options in settings.go 2025-01-29 16:06:35 +08:00
Neko Box Coder
dc18642985
Add missing resize in TabMove (#3619)
Some checks are pending
Build and Test / test (1.19.x, macos-latest) (push) Waiting to run
Build and Test / test (1.19.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.19.x, windows-latest) (push) Waiting to run
Build and Test / test (1.23.x, macos-latest) (push) Waiting to run
Build and Test / test (1.23.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.23.x, windows-latest) (push) Waiting to run
2025-01-28 21:04:05 +01:00
yz778
c02036e52f
Prompt to save or discard new files even with autosave enabled (#3626)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2025-01-25 20:24:31 +01:00
Neko Box Coder
698511c5b6
Fixing settings not being applied when saving as a new file (#3625)
Some checks are pending
Build and Test / test (1.19.x, macos-latest) (push) Waiting to run
Build and Test / test (1.19.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.19.x, windows-latest) (push) Waiting to run
Build and Test / test (1.23.x, macos-latest) (push) Waiting to run
Build and Test / test (1.23.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.23.x, windows-latest) (push) Waiting to run
2025-01-24 18:44:27 +01:00
Jöran Karl
c61670e86f buffer: Store the overwrite mode 2025-01-22 17:12:50 +01:00
Dmytro Maluka
6309136322 Adjust formatting of colorscheme option description 2025-01-21 17:26:42 +08:00
niten94
9e46a38536 Insert few parts in options.md in backticks 2025-01-21 17:23:44 +08:00
matthias314
f5debdf8fe
ignore quoted and escaped characters when splitting keybindings into actions (#3612)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2025-01-20 20:28:38 +01:00
niten94
4377e56e7e Add, sort options in list, default JSON in options.md 2025-01-19 21:34:38 +08:00
Antoine Beaubien
9b3f7ff240
Update options.md (#3615)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2025-01-19 13:07:48 +01:00
Jöran Karl
f49487dc3a
import: Use micro-editor/terminal instead of zyedidia/terminal (#3600)
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2025-01-14 18:20:37 +01:00
Jöran Karl
c77ed02778
Merge pull request #3601 from JoeKar/import/go-runewidth
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
import: Use `mattn/go-runewidth` instead of `zyedidia/go-runewidth`
2025-01-06 07:07:41 +01:00
Jöran Karl
d9956bde38 import: Bump mattn/go-runewidth to v0.0.16 2025-01-05 13:20:53 +01:00
Jöran Karl
ab6e170ec9 import: Use mattn/go-runewidth instead of zyedidia/go-runewidth 2025-01-05 13:19:39 +01:00
Jöran Karl
4d97076479
import: Use micro-editor/go-shellquote instead of zyedidia/go-shellquote (#3596)
Some checks are pending
Build and Test / test (1.19.x, macos-latest) (push) Waiting to run
Build and Test / test (1.19.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.19.x, windows-latest) (push) Waiting to run
Build and Test / test (1.23.x, macos-latest) (push) Waiting to run
Build and Test / test (1.23.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.23.x, windows-latest) (push) Waiting to run
2025-01-05 12:19:11 +01:00
Jöran Karl
a883c14c18
Merge pull request #3595 from JoeKar/import/json5
import: Use `micro-editor/json5` instead of `zyedidia/json5`
2025-01-05 12:13:21 +01:00
Jöran Karl
ce356c7957 Build: set 1.19 as minimum supported Go version 2025-01-04 16:01:21 +01:00
Jöran Karl
2ddf461ad8 import: Use micro-editor/json5 instead of zyedidia/json5 2025-01-04 16:01:20 +01:00
Jöran Karl
6600430e88
import: Use micro-editor/tcell (legacy) instead of zyedidia/tcell (#3593)
Some checks are pending
Build and Test / test (1.17.x, macos-latest) (push) Waiting to run
Build and Test / test (1.17.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.17.x, windows-latest) (push) Waiting to run
Build and Test / test (1.23.x, macos-latest) (push) Waiting to run
Build and Test / test (1.23.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.23.x, windows-latest) (push) Waiting to run
2025-01-04 15:55:46 +01:00
Dmytro Maluka
82467ba9f6
Merge pull request #3597 from matthias314/m3/gopher-bump
Some checks failed
Build and Test / test (1.17.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.17.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.17.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
bump gopher-lua and gopher-luar to current versions
2025-01-03 13:09:22 +01:00
Jöran Karl
99a27db4f7
Merge pull request #2962 from JoeKar/fix/linter-ft-relation
Some checks failed
Build and Test / test (1.17.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.17.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.17.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
plugin: Add new `onBufferOptionChanged` callback
2025-01-01 11:11:39 +01:00
niten94
58d38af8cd
Update go-isatty to v0.0.20 (#3561)
Some checks are pending
Build and Test / test (1.17.x, macos-latest) (push) Waiting to run
Build and Test / test (1.17.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.17.x, windows-latest) (push) Waiting to run
Build and Test / test (1.23.x, macos-latest) (push) Waiting to run
Build and Test / test (1.23.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.23.x, windows-latest) (push) Waiting to run
2024-12-31 13:32:34 +01:00
Jöran Karl
d1f54ea2a4 plugin: linter: Remove the forced C++14 standard to keep GCCs default 2024-12-31 13:27:38 +01:00
Jöran Karl
6b21fc5f92 plugin: linter: Invoke g++ for c++ instead of gcc 2024-12-31 13:27:38 +01:00
Jöran Karl
de84da068d plugin: linter: Move file type check into a dedicated function 2024-12-31 13:27:38 +01:00
Jöran Karl
771b84141f plugin: linter: Use new onBufferOptionChanged callback 2024-12-31 13:27:38 +01:00
Jöran Karl
415ceee46b plugin: Add new onBufferOptionChanged callback
This can become handy in the moment a plugin needs to react on e.g. changed
file type.
2024-12-31 13:27:35 +01:00
matthias314
aa24590070 bump gopher-luar to v1.0.11 2024-12-28 17:46:59 -05:00
matthias314
505aad8ba0 bump gopher-lua to v1.1.1 2024-12-27 17:59:12 -05:00
matthias314
2898f1590d
made FindNext and FindPrevious work with empty matches (#3572)
Some checks failed
Build and Test / test (1.17.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.17.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.17.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2024-12-17 18:52:44 +01:00
matthias314
aa0fefcaa1
skip empty match right after previous match in ReplaceCmd (#3566) 2024-12-17 18:44:48 +01:00
matthias314
8cdf68bbf6
skip save dialog on quit if buffer is shared (#3559)
Some checks failed
Build and Test / test (1.17.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.17.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.17.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
2024-12-09 19:42:19 +01:00
Jöran Karl
fb20818042
Merge pull request #3540 from JoeKar/fix/cursor-down
Some checks failed
Build and Test / test (1.17.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.17.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.17.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled
actions: Perform `Cursor(Page)Down` with selection like GUI editors do
2024-12-04 21:23:40 +01:00
Dmytro Maluka
71a26381c0
Fix unwanted view adjustment after page down (#3555)
Some checks are pending
Build and Test / test (1.17.x, macos-latest) (push) Waiting to run
Build and Test / test (1.17.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.17.x, windows-latest) (push) Waiting to run
Build and Test / test (1.23.x, macos-latest) (push) Waiting to run
Build and Test / test (1.23.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.23.x, windows-latest) (push) Waiting to run
Fix regression introduced while implementing nano-like page up/down in
commit b2dbcb3e: if the view is already at the end of the buffer and
the last line is even above the bottom, i.e. there are some empty
lines displayed below the last line (e.g. if we have scrolled past the
last line via the mouse wheel), pressing PageDown not just moves the
cursor to the last line but also unexpectedly adjusts the view so that
the last line is exactly at the bottom.
2024-12-03 21:07:30 +01:00
Jöran Karl
2c4754d484 actions: Prevent additional cursor move down on Cursor(Page)Down
This is needed to not move two lines below the last visual selection when it
has end behind the new line character.
2024-12-03 20:38:34 +01:00
matthias314
831e31d483
avoid creating nil callback for JobSpawn (#3554)
Some checks are pending
Build and Test / test (1.17.x, macos-latest) (push) Waiting to run
Build and Test / test (1.17.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.17.x, windows-latest) (push) Waiting to run
Build and Test / test (1.23.x, macos-latest) (push) Waiting to run
Build and Test / test (1.23.x, ubuntu-latest) (push) Waiting to run
Build and Test / test (1.23.x, windows-latest) (push) Waiting to run
2024-12-02 21:21:29 +01:00
Jöran Karl
50639015d7 cursor: Remove selection reduction by one character on Deselect() 2024-11-30 15:25:14 +01:00
Jöran Karl
aaf45a871f bufwindow: Don't highlight lines in ruler with active selection 2024-11-26 20:30:43 +01:00
Jöran Karl
3a16197da7 actions: On Cursor(Page)Down with selection of newline place cursor to start 2024-11-26 20:12:43 +01:00
med-ab
56c1f75bad
Add .cjs (common javascript) to javascript syntax definition (#3539) 2024-11-20 14:51:51 +01:00
Oleksandr Redko
b881bf5606
Remove unused internal or unexported functions (#3481) 2024-11-16 21:19:37 +01:00
Owen McGrath
c8eeb788cb
Update java.yaml (#3526) 2024-10-31 23:01:56 +01:00
Jöran Karl
aeabd5a7ba
Merge pull request #3518 from nimishjha/nano-like-pageup-pagedown
implement nano-like page up/page down functionality
2024-10-29 21:23:05 +01:00
Nimish Jha
b2dbcb3eab implement nano-like page up/page down functionality 2024-10-29 10:22:35 +11:00
Nimish Jha
eb880d8841 simplify code 2024-10-27 16:22:53 +11:00
Dmytro Maluka
1ead9ce4fd
Fix regression in CopyLine, CutLine, DeleteLine for last line (#3519)
Fix regression introduced in commit fdacb28962 ("CopyLine, CutLine,
DeleteLine: respect selection"): when CopyLine, CutLine or DeleteLine is
done in the last line of the buffer and this line is not empty, this
line gets selected but does not get copied/cut/deleted (and worse, it
remains selected).
2024-10-24 18:01:45 +02:00
Nimish Jha
b3227d6049
add actions: CursorToViewTop, CursorToViewCenter, CursorToViewBottom (#3506) 2024-10-23 07:25:33 +02:00
niten94
2c6dc32f5d
Set version as release when there are no commits ahead (#3515)
Print release version tag in tools/build-version.go even if the commit
being checked has a tag that is not a version number if there are no
commits ahead.
2024-10-22 22:07:30 +02:00
Creeper Lv
3cb8069e4a
Add target for Windows ARM64 in cross-compile.sh (#3512) 2024-10-21 19:33:14 +02:00
Dmytro Maluka
8c0e0fa2ed
Make textfilter work with multicursors (#3511)
As requested in [1] and [2], change the `textfilter` command behavior to
apply the filter to the selections of all cursors, not just the 1st one.

[1] https://github.com/zyedidia/micro/discussions/3505
[2] https://github.com/zyedidia/micro/discussions/3510
2024-10-20 21:27:19 +02:00
Dmytro Maluka
f293f983bd
Merge pull request #3503 from dmaluka/spawcursorup-logical-lines
Revert `SpawnMultiCursor{Up,Down}` honoring softwrap + overhaul `LastVisualX` usage
2024-10-20 21:26:59 +02:00
Jöran Karl
07f8cfbef1
Merge pull request #3502 from JoeKar/feature/help-split
action/command: Allow `-vsplit` & `-hsplit` as optional argument for `help`

Additionally the help, vsplit and hsplit command can now open multiple files like the tab command.
2024-10-20 20:17:53 +02:00
Jöran Karl
39b2b2639a action/command: Precise HelpCmd() documentation 2024-10-20 14:26:42 +02:00
Jöran Karl
47b84f75e1 config/settings: Add option helpsplit for permanent help split type
For downward compatibility the default split type for the `help` command
is set to be `hsplit`.
2024-10-20 14:26:42 +02:00
Jöran Karl
ff4c5c83f2 runtime/help: Align tab's documentation to vsplit 2024-10-20 14:26:42 +02:00
Jöran Karl
acabf2b492 action/command: Align vsplit & hsplit to tab's multiopen handling 2024-10-20 14:26:40 +02:00
Dmytro Maluka
1023c8d1be Document a few more undocumented colorscheme groups 2024-10-19 01:48:27 +02:00
Jöran Karl
2c62d4b70c action/command: Allow multiple help pages to be opened 2024-10-15 23:35:05 +02:00
Jöran Karl
26f0806915 action/command: Add optional flag -hsplit & -vsplit to help 2024-10-15 23:35:03 +02:00
Dmytro Maluka
e6ed161ca4 SpawnMultiCursorUp/Down: revert honoring softwrap
Commit 9fdea82542 ("Fix various issues with
`SpawnMultiCursor{Up,Down}`") changed SpawnMultiCursorUp/Down actions to
honor softwrap, i.e. spawn cursor in the next visual line within a
wrapped line, which may not be the next logical line in a buffer. That
was done for "consistency" with cursor movement actions CursorUp/Down
etc. But it seems there are no actual use cases for that, whereas at
least some users prefer spawning multicursor in the next logical line
regardless of the softwrap setting. So restore the old behavior.

Fixes #3499
2024-10-14 01:42:04 +02:00
Dmytro Maluka
134cd999c6 Reset LastVisualX on undo/redo
In cursor's Goto(), which is currently only used by undo and redo, we
restore remembered LastVisualX and LastWrappedVisualX values. But if
the window had been resized in the meantime, the LastWrappedVisualX
may not be valid anymore. So it may cause the cursor moving to
unexpected locations.

So for simplicity just reset these values on undo or redo, instead of
using remembered ones.
2024-10-14 01:42:04 +02:00
Dmytro Maluka
6214abba9a Overhaul LastVisualX and GetVisualX() usage
Restore the original meaning of LastVisualX before commit 6d13710d93
("Implement moving cursor up/down within a wrapped line"): last visual x
location of the cursor in a logical line in the buffer, not in a visual
line on the screen (in other words, taking tabs and wide characters into
account, but not taking softwrap into account). And add a separate
LastWrappedVisualX field, similar to LastVisualX but taking softwrap
into account as well.

This allows tracking last x position at the same time for both cases
when we care about softwrap and when we don't care about it. This can be
useful, for example, for implementing cursor up/down movement actions
that always move by logical lines, not by visual lines, even if softwrap
is enabled (in addition to our default CursorUp and CursorDown actions
that move by visual lines).

Also this fixes a minor bug: in InsertTab(), when `tabstospaces` is
enabled and we insert a tab, the amount of inserted spaces depends on
the visual line wrapping (i.e. on the window width), which is probably
not a good idea.
2024-10-14 01:41:35 +02:00
Dmytro Maluka
85afb6eb87 Use StoreVisualX() all over the code
Since we already have the StoreVisualX() helper, use it all over the
place instead of setting LastVisualX directly.

This will allow us to add more logic to StoreVisualX() add let this
extra logic apply everywhere automatically.
2024-10-13 17:46:34 +02:00
Jöran Karl
d60413f03c
Merge pull request #3495 from dmaluka/sudo-sigint-fix
Fix SIGINT killing micro when saving with sudo
2024-10-12 14:02:09 +02:00
Dmytro Maluka
af88b4d2a8 Fix error reporting when saving with sudo failed
When saving a file with sudo fails (e.g. if we set `sucmd` to a
non-existent binary, e.g. `set sucmd aaa`), we erroneously return
success instead of the error, as a result we report to the user that
that the file has been successfully saved. Fix it.
2024-10-06 17:08:25 +02:00
Dmytro Maluka
4baac3d3fb Fix SIGINT killing micro when saving with sudo
When we are saving a file with sudo, if we interrupt sudo via Ctrl-c,
it doesn't just kill sudo, it kills micro itself.

The cause is the same as in the issue #2612 for RunInteractiveShell()
which was fixed by #3357. So fix it the same way as in #3357.
2024-10-06 17:08:25 +02:00
theredcmdcraft
ac73f18191
Create nftables.yaml (#3325)
Created nftables syntax highlighting
2024-10-06 13:04:32 +02:00
Neko Box Coder
3b3fe63f19
Exposing replacement functions for deprecated IOUtil functions (#3393) 2024-09-22 22:08:32 +02:00
Oleksandr Redko
9cd1ce968d
tool/info-plist: decrease indentation and simplify (#3479) 2024-09-22 20:38:15 +02:00
Jöran Karl
71da59fd1c
Merge pull request #3466 from JoeKar/fix/linux-dynamic2static
Makefile: Make all builds explicitly fully static (disable CGO)
2024-09-19 17:27:16 +02:00
Jöran Karl
3f1e5ea6df README: Remove superflous whitespace 2024-09-18 19:10:46 +02:00
Jöran Karl
fcc7421bca Makefile: Fix native darwin/macOS builds with forced CGO 2024-09-18 19:08:50 +02:00
Jöran Karl
6722cc81de tools/cross-compile: Mark "Linux 64 fully static" to be same as "Linux 64"
It is kept for the next release only to support...
f90870e948/index.sh (L197-L204)
...and allow a fluent switch via:
https://github.com/benweissmann/getmic.ro/pull/40
2024-09-16 22:43:12 +02:00
Jöran Karl
90525a6a1d Makefile: Make all builds explicitly fully static by default (disable CGO) 2024-09-16 22:43:12 +02:00
Jöran Karl
6e46ae3090 Makefile: Remove "-s -w" from build-dbg target
This will keep the symbol table and the DWARF information.
2024-09-16 22:43:12 +02:00
Jöran Karl
4d2ddc7940 Makefile: Simplify build-tags build target 2024-09-16 22:43:12 +02:00
Massimo Mund
4f4a13a9a1
Implemented SkipMultiCursorBack as a counterpart to SkipMultiCursor (#3404) 2024-09-16 22:20:12 +02:00
Dmytro Maluka
9eaeb193d4
Merge pull request #3403 from masmu/refactor/tab-actions
Implemented new actions `FirstTab`, `LastTab`, `FirstSplit` and `LastSplit`
2024-09-16 22:19:36 +02:00
Dmytro Maluka
ca6012086b
Merge pull request #3335 from dmaluka/line-actions-cleanup
Improve and unify `CopyLine`, `CutLine`, `DeleteLine`, `DuplicateLine` actions
2024-09-16 22:19:05 +02:00
Oleksandr Redko
1539da7fdc
test: simplify cmd/micro tests (#3470) 2024-09-16 19:33:59 +02:00
Oleksandr Redko
a3211dce57
Build: set 1.17 as minimum supported Go version (#3461) 2024-09-16 19:21:43 +02:00
Massimo Mund
5f83661fee Fixes a bug where new BufPanes are not being inserted into the right array index.
When adding a new `BufPane` it is always being inserted last into `MainTab().Panes`.
This leads to a confusion when using the actions `PreviousSplit`, `NextSplit` as the previous/next split may not be the expected one.

How to reproduce:
- Launch micro and insert char "1"
- Open a new vsplit via the command `vsplit` and insert "2"
- Switch back to the left split (1) by using `PreviousSplit`
- Again open a new vsplit via command: `vsplit` and type char "3"
- Now switch between the 3 splits using `PreviousSplit`, `NextSplit`

Switching from most left split to the most right, the expected order would be 1, 3, 2 but actually is 1, 2, 3.
2024-09-15 16:36:00 +02:00
Massimo Mund
2e44db1ee9 Implemented new actions FirstTab, LastTab, FirstSplit and LastSplit and changed the default behavior of NextTab, PreviousTab, NextSplit, PreviousSplit to not walk in circles anymore 2024-09-15 16:35:22 +02:00
Oleksandr Redko
e6d4e37922
README: remove TOC in favor to GitHub's TOC (#3467) 2024-09-12 21:00:00 +02:00
Dmytro Maluka
d6d0b26041
Fix non-working raw escape bindings after restarting the screen (#3468)
When we temporarily disable the screen (e.g. during RunInteractiveShell)
and then enable it again, we reinitialize tcell.Screen from scratch, so
we need to register all previously registered raw escape sequences once
again. Otherwise raw escape bindings stop working, since the list of
raw escape sequences of this newly create tcell.Screen is empty.

Fixes #3392
2024-09-12 20:39:14 +02:00
Jonathan Berkeley
f22252e5ae
Mark quick install script as third-party (#3469)
Minor update to README that highlights the pipe install script is provided by a third-party.
2024-09-12 20:30:04 +02:00
Jöran Karl
8c52d2426d
Merge pull request #3458 from JoeKar/feature/empty-rules
Remove empty rules in regions
2024-09-09 18:48:21 +02:00
Jöran Karl
596da97626 syntax/syntax_converter: Remove empty rules in regions 2024-09-09 18:32:30 +02:00
Jöran Karl
f391b59be6 plugins/literate: Remove empty rules in regions 2024-09-09 18:32:30 +02:00
Jöran Karl
debef6e51b help/colors: Remove empty rules in regions 2024-09-09 18:32:30 +02:00
Jöran Karl
a9b513a28a syntax: Remove empty rules in regions 2024-09-09 18:32:30 +02:00
Jöran Karl
5554cd18e3 highlighter/parser: Switch creation of empty rules to struct literal
Co-authored-by: Dmytro Maluka <dmitrymaluka@gmail.com>
2024-09-09 18:30:39 +02:00
Jöran Karl
6e60dede36 highlighter/parser: Make nested rules optional
This allows us to remove the empty "rules: []" in various syntax definitions.
2024-09-09 18:28:33 +02:00
Oleksandr
5428b3fda2
Add Swift shebang to syntax (#3451)
The Swift compiler can be run in "interpreter" mode, so it can run Swift "scripts" if they have a proper shebang and no file extension.
2024-09-05 18:41:39 +02:00
James M Corey
2308bc5555
Fix rust syntax file to recognize byte strings and c strings. (#3452)
In rust, there are some prefixes that may be part of the string literal.
String literals of the form b"test" (and br##"test"## etc) are byte
strings (as opposed to unicode strings), and similarly, string literals
of the form c"test" are C zero-terminated strings.  Hence, added optional
prefixes to each of the string regular expressions so the prefix will be
recognized as part of the string.

Built and tested after fix.

Co-authored-by: James Corey <jc-git@neniam.net>
2024-09-05 18:41:17 +02:00
James M Corey
d8f7928b74
Add an OpenSCAD syntax file (#3410)
Update from PR feedback:
Coalesce multiple statement rules into one.
Coalesce multiple constant.number into one.

Update from more PR feedback:
Fix special variables (starting with $)--var must start with $,
i.e. x$y is not a valid special var, but you can have x=$y.

Compiled and tested again with latest changes.

Co-authored-by: James Corey <jc-git@neniam.net>
2024-09-04 18:49:55 +02:00
Jöran Karl
2b44fc3bbb
Makefile: Fetch tags with --force (#3448) 2024-08-31 18:08:16 +02:00
mystieneko
47fb91e333
add more css commands (#3436) 2024-08-31 13:03:00 +02:00
Mikko
cc67b801ce
Improve Haskell syntax highlighting (#3373)
* Improve Haskell syntax highlighting

* add syntax highlighting for binary literals
2024-08-31 12:59:40 +02:00
Juan Francisco Cantero Hurtado
f23c2b6115
Raku syntax: Add .rakutest/.nqp extensions. Rework filename regex. (#3406)
With @niten94, @JoeKar and @Andriamanitra.
2024-08-31 12:44:10 +02:00
Neko Box Coder
e6b20b2ce9
Adding SpawnCursorAtLoc for plugin to use (#3441) 2024-08-31 12:42:55 +02:00
Jöran Karl
968f5ba1ef
tools: Revert tgz to tar.gz in cross-compile.sh (#3446) 2024-08-28 23:50:23 +02:00
Jöran Karl
04c577049c metainfo: Release v2.0.14 2024-08-27 19:58:17 +02:00
Dmytro Maluka
bf6584739f help/keybindings: document CutLine behavior 2024-06-14 00:49:51 +02:00
Dmytro Maluka
68d6f43c63 CutLine: remove lastCutTime feature
The lastCutTime feature (reset the clipboard instead of appending to the
clipboard if the last CutLine was more than 10 seconds ago) was
implemented 8 years ago but was always buggy and never really worked,
until we have accidentally found and fixed the bug just now. No one ever
complained or noticed that, which means it is not a very useful feature.
Fixing it changes the existing behavior (essentially adds a new feature
which did not really exist before) and there is no reason to assume that
this new behavior will be welcome by users. So it's better to remove
this feature.
2024-06-12 03:16:36 +02:00
Dmytro Maluka
6f724bc424 DuplicateLine: respect selections
Similarly to CutLine, DeleteLine and CopyLine actions, if there is a
selection, duplicate not just the current line but all the lines covered
(fully or partially) by the selection.
2024-06-09 17:11:58 +02:00
Dmytro Maluka
25f71eec2d DuplicateLine: move selection duplication to separate Duplicate action
- Add a new Duplicate action which just duplicates the selection (and
  returns false if there is no selection).
- Change the behavior of the DuplicateLine action to only duplicate the
  current line, not the selection.
- Change the default action bound to Ctrl-d from DuplicateLine to
  Duplicate|DuplicateLine, so that the default behavior doesn't change.

This allows the user to rebind keybindings in a more flexible way, i.e.
to choose whether a key should duplicate just lines, or just selections,
or both, - in a similar fashion to Copy, Cut, Delete actions.
2024-06-09 17:11:58 +02:00
Dmytro Maluka
33a1bb120f CutLine: return if cliboard read failed
If we ever encounter this clipboard.Read() failure, return false
immediately. Otherwise, InfoBar.Error(err) will have no effect (it will
be immediately overwritten by InfoBar.Message()) so we won't even know
that there was an error.
2024-06-09 17:11:58 +02:00
Dmytro Maluka
04143c7a89 Make Cut, Copy, CopyLine don't mess with CutLine's multi line cuts
Weird behavior is observed e.g. if we cut some lines with CutLine, then
copy some selection with Copy, then cut some other lines with CutLine,
and then paste. The pasted cliboard contains not just the lines that
were cut at the last step, but also the selection that was copied before
that.

Fix that by resetting the CutLine's repeated line cuts whenever we
copy anything to the clipboard via any other action (Cut, Copy or
CopyLine).
2024-06-09 17:11:58 +02:00
Dmytro Maluka
e6825f0e08 CutLine: make infobar message more useful
Since CutLine may add lines to the clipboard instead of replacing the
clipboard, improve its info message to show how many lines are in the
clipboard in total, not just how many lines were added to it last time.
2024-06-09 17:11:58 +02:00
Dmytro Maluka
fdacb28962 CopyLine, CutLine, DeleteLine: respect selection
When there is a selection containing multiple lines, CutLine, DeleteLine
and CopyLine actions currently cut/delete/copy just the "current" line,
as usual. This behavior is at least confusing, since when there is a
selection, the cursor is not displayed, so the user doesn't know which
line is the current one.

So change the behavior. When there is a multi-line selection,
cut/delete/copy all lines covered by the selection, not just the current
line. Note that it will cut/delete/copy whole lines, not just the
selection itself, i.e. if the first and/or the last line of the
selection is only partially within the selection, we will
cut/delete/copy the entire first and last lines nonetheless.
2024-06-09 17:11:58 +02:00
Dmytro Maluka
9f7bdb109b Cosmetic change: move Cut above CutLine 2024-06-09 17:11:58 +02:00
Dmytro Maluka
c1bbd7b041 CutLine: cosmetic refactoring 2024-06-09 17:11:58 +02:00
Dmytro Maluka
a317aefd6d Reorganize Cut and CutLine actions
Change behavior of the Cut action: don't implicitly call CutLine if
there is no selection. Instead, make it return false in this case
and change the default Ctrl-x binding to Cut|CutLine, to make it clear,
explicit and in line with Copy and CopyLine actions.
2024-06-09 17:11:58 +02:00
Dmytro Maluka
830768b715 Reorganize Copy and CopyLine actions
Make Copy return false if there is no selection, and change the default
binding for Ctrl-c from CopyLine|Copy to Copy|CopyLine accordingly,
to make the semantics more meaningful: copying selection always fails
if there is no selection.
2024-06-09 12:19:34 +02:00
Dmytro Maluka
2860efbe3a CutLine: remove unneeded if check 2024-06-09 12:16:25 +02:00
Dmytro Maluka
52ed4315ff Make lastCutTime actually work
The CutLine action has a feature: if we execute it multiple times to cut
multiple lines, new cut lines are added to the previously cut lines in
the clipboard instead of replacing the clipboard, unless those
previously cut lines have been already pasted or the last cut was more
than 10 seconds ago. This last bit doesn't really work: newly cut lines
are appended to the clipboard regardless of when was the last cut.
So fix it.
2024-06-09 12:07:07 +02:00
Dmytro Maluka
8bc67569f9 Fix CopyLine at the last empty line of buffer
When the cursor is at the last line of buffer and it is an empty line,
CopyLine does not copy this line, which is correct, but it shows a bogus
"Copied line" message. Fix this by adding a check for that, same as in
CutLine and DeleteLine.
2024-06-09 11:44:44 +02:00
Dmytro Maluka
df8d5285bf Fix Cursor{Up,Down} after CopyLine
After executing the CopyLine action, moving cursor up or down
unexpectedly moves cursor to the beginning of the line, since its
LastVisualX value is lost in the selection/deselection manipulations.
Fix this by restoring the original LastVisualX.
2024-06-09 11:40:30 +02:00
Dmytro Maluka
19c69f9eaa Fix Cursor{Up,Down} after DeleteLine and CutLine
After executing CutLine or DeleteLine action, the cursor is at the
beginning of a line (as expected) but then moving the cursor up or down
moves it to an unexpected location in the middle of the next or previous
line. Fix this by updating the cursor's LastVisualX.
2024-06-09 11:39:23 +02:00
161 changed files with 2245 additions and 1408 deletions

View File

@ -7,7 +7,7 @@ jobs:
nightly: nightly:
strategy: strategy:
matrix: matrix:
go-version: [1.19.x] go-version: [1.23.x]
os: [ubuntu-latest] os: [ubuntu-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:

View File

@ -8,7 +8,7 @@ jobs:
release: release:
strategy: strategy:
matrix: matrix:
go-version: [1.19.x] go-version: [1.23.x]
os: [ubuntu-latest] os: [ubuntu-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:

View File

@ -4,7 +4,7 @@ jobs:
test: test:
strategy: strategy:
matrix: matrix:
go-version: [1.19.x] go-version: [1.19.x, 1.23.x]
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:

View File

@ -430,7 +430,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
github.com/gdamore/tcell/LICENSE github.com/gdamore/tcell/LICENSE
================ ================
github.com/zyedidia/tcell/LICENSE (fork) github.com/micro-editor/tcell/LICENSE (fork)
================ ================
@ -1048,7 +1048,7 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice
github.com/flynn/json5/LICENSE github.com/flynn/json5/LICENSE
================ ================
github.com/zyedidia/json5/LICENSE (fork) github.com/micro-editor/json5/LICENSE (fork)
================ ================
Decoder code based on package encoding/json from the Go language. Decoder code based on package encoding/json from the Go language.
@ -1108,7 +1108,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
github.com/james4k/terminal/LICENSE github.com/james4k/terminal/LICENSE
================ ================
github.com/zyedidia/terminal/LICENSE (fork) github.com/micro-editor/terminal/LICENSE (fork)
================ ================
Copyright (C) 2013 James Gray Copyright (C) 2013 James Gray

View File

@ -5,24 +5,31 @@ VERSION = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH
HASH = $(shell git rev-parse --short HEAD) HASH = $(shell git rev-parse --short HEAD)
DATE = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \ DATE = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
go run tools/build-date.go) go run tools/build-date.go)
ADDITIONAL_GO_LINKER_FLAGS = $(shell GOOS=$(shell go env GOHOSTOS) \
GOARCH=$(shell go env GOHOSTARCH) \
go run tools/info-plist.go "$(shell go env GOOS)" "$(VERSION)")
GOBIN ?= $(shell go env GOPATH)/bin GOBIN ?= $(shell go env GOPATH)/bin
GOVARS = -X github.com/zyedidia/micro/v2/internal/util.Version=$(VERSION) -X github.com/zyedidia/micro/v2/internal/util.CommitHash=$(HASH) -X 'github.com/zyedidia/micro/v2/internal/util.CompileDate=$(DATE)' GOVARS = -X github.com/zyedidia/micro/v2/internal/util.Version=$(VERSION) -X github.com/zyedidia/micro/v2/internal/util.CommitHash=$(HASH) -X 'github.com/zyedidia/micro/v2/internal/util.CompileDate=$(DATE)'
DEBUGVAR = -X github.com/zyedidia/micro/v2/internal/util.Debug=ON DEBUGVAR = -X github.com/zyedidia/micro/v2/internal/util.Debug=ON
VSCODE_TESTS_BASE_URL = 'https://raw.githubusercontent.com/microsoft/vscode/e6a45f4242ebddb7aa9a229f85555e8a3bd987e2/src/vs/editor/test/common/model/' VSCODE_TESTS_BASE_URL = 'https://raw.githubusercontent.com/microsoft/vscode/e6a45f4242ebddb7aa9a229f85555e8a3bd987e2/src/vs/editor/test/common/model/'
CGO_ENABLED := $(if $(CGO_ENABLED),$(CGO_ENABLED),0)
ADDITIONAL_GO_LINKER_FLAGS := ""
GOHOSTOS = $(shell go env GOHOSTOS)
ifeq ($(GOHOSTOS), darwin)
# Native darwin resp. macOS builds need external and dynamic linking
ADDITIONAL_GO_LINKER_FLAGS += $(shell GOOS=$(GOHOSTOS) \
GOARCH=$(shell go env GOHOSTARCH) \
go run tools/info-plist.go "$(shell go env GOOS)" "$(VERSION)")
CGO_ENABLED = 1
endif
build: generate build-quick build: generate build-quick
build-quick: build-quick:
go build -trimpath -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro CGO_ENABLED=$(CGO_ENABLED) go build -trimpath -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
build-dbg: build-dbg:
go build -trimpath -ldflags "-s -w $(ADDITIONAL_GO_LINKER_FLAGS) $(DEBUGVAR)" ./cmd/micro CGO_ENABLED=$(CGO_ENABLED) go build -trimpath -ldflags "$(ADDITIONAL_GO_LINKER_FLAGS) $(DEBUGVAR)" ./cmd/micro
build-tags: fetch-tags generate build-tags: fetch-tags build
go build -trimpath -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
build-all: build build-all: build
@ -32,7 +39,7 @@ install: generate
install-all: install install-all: install
fetch-tags: fetch-tags:
git fetch --tags git fetch --tags --force
generate: generate:
GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) go generate ./runtime GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) go generate ./runtime

View File

@ -21,22 +21,6 @@ To see more screenshots of micro, showcasing some of the default color schemes,
You can also check out the website for Micro at https://micro-editor.github.io. You can also check out the website for Micro at https://micro-editor.github.io.
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Prebuilt binaries](#pre-built-binaries)
- [Package Managers](#package-managers)
- [Building from source](#building-from-source)
- [Fully static binary](#fully-static-binary)
- [macOS terminal](#macos-terminal)
- [Linux clipboard support](#linux-clipboard-support)
- [Colors and syntax highlighting](#colors-and-syntax-highlighting)
- [Cygwin, Mingw, Plan9](#cygwin-mingw-plan9)
- [Usage](#usage)
- [Documentation and Help](#documentation-and-help)
- [Contributing](#contributing)
- - - - - -
## Features ## Features
@ -63,7 +47,7 @@ You can also check out the website for Micro at https://micro-editor.github.io.
- Syntax highlighting for over [130 languages](runtime/syntax). - Syntax highlighting for over [130 languages](runtime/syntax).
- Color scheme support. - Color scheme support.
- By default, micro comes with 16, 256, and true color themes. - By default, micro comes with 16, 256, and true color themes.
- True color support (set the `MICRO_TRUECOLOR` environment variable to 1 to enable it). - True color support.
- Copy and paste with the system clipboard. - Copy and paste with the system clipboard.
- Small and simple. - Small and simple.
- Easily configurable. - Easily configurable.
@ -88,7 +72,7 @@ Pre-built binaries are distributed in [releases](https://github.com/zyedidia/mic
To uninstall micro, simply remove the binary, and the configuration directory at `~/.config/micro`. To uninstall micro, simply remove the binary, and the configuration directory at `~/.config/micro`.
#### Quick-install script #### Third-party quick-install script
```bash ```bash
curl https://getmic.ro | bash curl https://getmic.ro | bash
@ -178,7 +162,7 @@ Without these tools installed, micro will use an internal clipboard for copy and
If your operating system does not have a binary release, 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.16 or greater and Go modules are enabled. Make sure that you have Go version 1.19 or greater and Go modules are enabled.
``` ```
git clone https://github.com/zyedidia/micro git clone https://github.com/zyedidia/micro
@ -196,16 +180,20 @@ You can install directly with `go get` (`go get github.com/zyedidia/micro/cmd/mi
recommended because it doesn't build micro with version information (necessary for the plugin manager), recommended because it doesn't build micro with version information (necessary for the plugin manager),
and doesn't disable debug mode. and doesn't disable debug mode.
### Fully static binary ### Fully static or dynamically linked binary
By default, the micro binary will dynamically link with core system libraries (this is generally By default, the micro binary is linked statically to increase the portability of the prebuilt binaries.
recommended for security and portability). However, there is a fully static prebuilt binary that This behavior can simply be overriden by providing `CGO_ENABLED=1` to the build target.
is provided for amd64 as `linux-static.tar.gz`, and to build a fully static binary from source, run
``` ```
CGO_ENABLED=0 make build CGO_ENABLED=1 make build
``` ```
Afterwards the micro binary will dynamically link with the present core system libraries.
**Note for Mac:**
Native macOS builds are done with `CGO_ENABLED=1` forced set to support adding the "Information Property List" in the linker step.
### macOS terminal ### macOS terminal
If you are using macOS, you should consider using [iTerm2](http://iterm2.com/) instead of the default terminal (Terminal.app). The iTerm2 terminal has much better mouse support as well as better handling of key events. For best keybinding behavior, choose `xterm defaults` under `Preferences->Profiles->Keys->Presets...`, and select `Esc+` for `Left Option Key` in the same menu. The newest versions also support true color. If you are using macOS, you should consider using [iTerm2](http://iterm2.com/) instead of the default terminal (Terminal.app). The iTerm2 terminal has much better mouse support as well as better handling of key events. For best keybinding behavior, choose `xterm defaults` under `Preferences->Profiles->Keys->Presets...`, and select `Esc+` for `Left Option Key` in the same menu. The newest versions also support true color.

View File

@ -3,8 +3,8 @@ package main
import ( import (
"bufio" "bufio"
"encoding/gob" "encoding/gob"
"errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
@ -12,6 +12,7 @@ import (
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/util"
) )
func shouldContinue() bool { func shouldContinue() bool {
@ -39,7 +40,16 @@ func CleanConfig() {
} }
fmt.Println("Cleaning default settings") fmt.Println("Cleaning default settings")
config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
settingsFile := filepath.Join(config.ConfigDir, "settings.json")
err := config.WriteSettings(settingsFile)
if err != nil {
if errors.Is(err, util.ErrOverwrite) {
fmt.Println(err.Error())
} else {
fmt.Println("Error writing settings.json file: " + err.Error())
}
}
// detect unused options // detect unused options
var unusedOptions []string var unusedOptions []string
@ -67,16 +77,20 @@ func CleanConfig() {
fmt.Printf("%s (value: %v)\n", s, config.GlobalSettings[s]) fmt.Printf("%s (value: %v)\n", s, config.GlobalSettings[s])
} }
fmt.Printf("These options will be removed from %s\n", filepath.Join(config.ConfigDir, "settings.json")) fmt.Printf("These options will be removed from %s\n", settingsFile)
if shouldContinue() { if shouldContinue() {
for _, s := range unusedOptions { for _, s := range unusedOptions {
delete(config.GlobalSettings, s) delete(config.GlobalSettings, s)
} }
err := config.OverwriteSettings(filepath.Join(config.ConfigDir, "settings.json")) err := config.OverwriteSettings(settingsFile)
if err != nil { if err != nil {
fmt.Println("Error writing settings.json file: " + err.Error()) if errors.Is(err, util.ErrOverwrite) {
fmt.Println(err.Error())
} else {
fmt.Println("Error overwriting settings.json file: " + err.Error())
}
} }
fmt.Println("Removed unused options") fmt.Println("Removed unused options")
@ -85,12 +99,13 @@ func CleanConfig() {
} }
// detect incorrectly formatted buffer/ files // detect incorrectly formatted buffer/ files
files, err := ioutil.ReadDir(filepath.Join(config.ConfigDir, "buffers")) buffersPath := filepath.Join(config.ConfigDir, "buffers")
files, err := os.ReadDir(buffersPath)
if err == nil { if err == nil {
var badFiles []string var badFiles []string
var buffer buffer.SerializedBuffer var buffer buffer.SerializedBuffer
for _, f := range files { for _, f := range files {
fname := filepath.Join(config.ConfigDir, "buffers", f.Name()) fname := filepath.Join(buffersPath, f.Name())
file, e := os.Open(fname) file, e := os.Open(fname)
if e == nil { if e == nil {
@ -105,9 +120,9 @@ func CleanConfig() {
} }
if len(badFiles) > 0 { if len(badFiles) > 0 {
fmt.Printf("Detected %d files with an invalid format in %s\n", len(badFiles), filepath.Join(config.ConfigDir, "buffers")) fmt.Printf("Detected %d files with an invalid format in %s\n", len(badFiles), buffersPath)
fmt.Println("These files store cursor and undo history.") fmt.Println("These files store cursor and undo history.")
fmt.Printf("Removing badly formatted files in %s\n", filepath.Join(config.ConfigDir, "buffers")) fmt.Printf("Removing badly formatted files in %s\n", buffersPath)
if shouldContinue() { if shouldContinue() {
removed := 0 removed := 0

View File

@ -18,7 +18,7 @@ func (NullWriter) Write(data []byte) (n int, err error) {
// InitLog sets up the debug log system for micro if it has been enabled by compile-time variables // InitLog sets up the debug log system for micro if it has been enabled by compile-time variables
func InitLog() { func InitLog() {
if util.Debug == "ON" { if util.Debug == "ON" {
f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, util.FileMode)
if err != nil { if err != nil {
log.Fatalf("error opening file: %v", err) log.Fatalf("error opening file: %v", err)
} }

View File

@ -4,10 +4,10 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"regexp" "regexp"
"runtime" "runtime"
"runtime/pprof" "runtime/pprof"
@ -18,6 +18,7 @@ import (
"github.com/go-errors/errors" "github.com/go-errors/errors"
isatty "github.com/mattn/go-isatty" isatty "github.com/mattn/go-isatty"
"github.com/micro-editor/tcell/v2"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/micro/v2/internal/action" "github.com/zyedidia/micro/v2/internal/action"
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
@ -26,7 +27,6 @@ import (
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell" "github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2"
) )
var ( var (
@ -99,7 +99,7 @@ func InitFlags() {
fmt.Println("Version:", util.Version) fmt.Println("Version:", util.Version)
fmt.Println("Commit hash:", util.CommitHash) fmt.Println("Commit hash:", util.CommitHash)
fmt.Println("Compiled on", util.CompileDate) fmt.Println("Compiled on", util.CompileDate)
os.Exit(0) exit(0)
} }
if *flagOptions { if *flagOptions {
@ -115,7 +115,7 @@ func InitFlags() {
fmt.Printf("-%s value\n", k) fmt.Printf("-%s value\n", k)
fmt.Printf(" \tDefault value: '%v'\n", v) fmt.Printf(" \tDefault value: '%v'\n", v)
} }
os.Exit(0) exit(0)
} }
if util.Debug == "OFF" && *flagDebug { if util.Debug == "OFF" && *flagDebug {
@ -136,7 +136,7 @@ func DoPluginFlags() {
CleanConfig() CleanConfig()
} }
os.Exit(0) exit(0)
} }
} }
@ -209,7 +209,7 @@ func LoadInput(args []string) []*buffer.Buffer {
// Option 2 // Option 2
// The input is not a terminal, so something is being piped in // The input is not a terminal, so something is being piped in
// and we should read from stdin // and we should read from stdin
input, err = ioutil.ReadAll(os.Stdin) input, err = io.ReadAll(os.Stdin)
if err != nil { if err != nil {
screen.TermMessage("Error reading from stdin: ", err) screen.TermMessage("Error reading from stdin: ", err)
input = []byte{} input = []byte{}
@ -223,12 +223,55 @@ func LoadInput(args []string) []*buffer.Buffer {
return buffers return buffers
} }
func checkBackup(name string) error {
target := filepath.Join(config.ConfigDir, name)
backup := util.AppendBackupSuffix(target)
if info, err := os.Stat(backup); err == nil {
input, err := os.ReadFile(backup)
if err == nil {
t := info.ModTime()
msg := fmt.Sprintf(buffer.BackupMsg, target, t.Format("Mon Jan _2 at 15:04, 2006"), backup)
choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true)
if choice%3 == 0 {
// recover
err := os.WriteFile(target, input, util.FileMode)
if err != nil {
return err
}
return os.Remove(backup)
} else if choice%3 == 1 {
// delete
return os.Remove(backup)
} else if choice%3 == 2 {
// abort
return errors.New("Aborted")
}
}
}
return nil
}
func exit(rc int) {
for _, b := range buffer.OpenBuffers {
if !b.Modified() {
b.Fini()
}
}
if screen.Screen != nil {
screen.Screen.Fini()
}
os.Exit(rc)
}
func main() { func main() {
defer func() { defer func() {
if util.Stdout.Len() > 0 { if util.Stdout.Len() > 0 {
fmt.Fprint(os.Stdout, util.Stdout.String()) fmt.Fprint(os.Stdout, util.Stdout.String())
} }
os.Exit(0) exit(0)
}() }()
var err error var err error
@ -256,6 +299,12 @@ func main() {
config.InitRuntimeFiles(true) config.InitRuntimeFiles(true)
config.InitPlugins() config.InitPlugins()
err = checkBackup("settings.json")
if err != nil {
screen.TermMessage(err)
exit(1)
}
err = config.ReadSettings() err = config.ReadSettings()
if err != nil { if err != nil {
screen.TermMessage(err) screen.TermMessage(err)
@ -288,7 +337,7 @@ func main() {
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
fmt.Println("Fatal: Micro could not initialize a Screen.") fmt.Println("Fatal: Micro could not initialize a Screen.")
os.Exit(1) exit(1)
} }
m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string)) m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string))
clipErr := clipboard.Initialize(m) clipErr := clipboard.Initialize(m)
@ -307,7 +356,7 @@ func main() {
for _, b := range buffer.OpenBuffers { for _, b := range buffer.OpenBuffers {
b.Backup() b.Backup()
} }
os.Exit(1) exit(1)
} }
}() }()
@ -316,6 +365,12 @@ func main() {
screen.TermMessage(err) screen.TermMessage(err)
} }
err = checkBackup("bindings.json")
if err != nil {
screen.TermMessage(err)
exit(1)
}
action.InitBindings() action.InitBindings()
action.InitCommands() action.InitCommands()
@ -434,24 +489,12 @@ func DoEvent() {
} }
case f := <-timerChan: case f := <-timerChan:
f() f()
case b := <-buffer.BackupCompleteChan:
b.RequestedBackup = false
case <-sighup: case <-sighup:
for _, b := range buffer.OpenBuffers { exit(0)
if !b.Modified() {
b.Fini()
}
}
os.Exit(0)
case <-util.Sigterm: case <-util.Sigterm:
for _, b := range buffer.OpenBuffers { exit(0)
if !b.Modified() {
b.Fini()
}
}
if screen.Screen != nil {
screen.Screen.Fini()
}
os.Exit(0)
} }
if e, ok := event.(*tcell.EventError); ok { if e, ok := event.(*tcell.EventError); ok {
@ -459,16 +502,7 @@ func DoEvent() {
if e.Err() == io.EOF { if e.Err() == io.EOF {
// shutdown due to terminal closing/becoming inaccessible // shutdown due to terminal closing/becoming inaccessible
for _, b := range buffer.OpenBuffers { exit(0)
if !b.Modified() {
b.Fini()
}
}
if screen.Screen != nil {
screen.Screen.Fini()
}
os.Exit(0)
} }
return return
} }

View File

@ -2,18 +2,17 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os" "os"
"testing" "testing"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/micro-editor/tcell/v2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zyedidia/micro/v2/internal/action" "github.com/zyedidia/micro/v2/internal/action"
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/tcell/v2"
) )
var tempDir string var tempDir string
@ -26,7 +25,7 @@ func init() {
func startup(args []string) (tcell.SimulationScreen, error) { func startup(args []string) (tcell.SimulationScreen, error) {
var err error var err error
tempDir, err = ioutil.TempDir("", "micro_test") tempDir, err = os.MkdirTemp("", "micro_test")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -164,20 +163,22 @@ func findBuffer(file string) *buffer.Buffer {
return buf return buf
} }
func createTestFile(name string, content string) (string, error) { func createTestFile(t *testing.T, content string) string {
testf, err := ioutil.TempFile("", name) f, err := os.CreateTemp(t.TempDir(), "")
if err != nil { if err != nil {
return "", err t.Fatal(err)
}
defer func() {
if err := f.Close(); err != nil {
t.Fatal(err)
}
}()
if _, err := f.WriteString(content); err != nil {
t.Fatal(err)
} }
if _, err := testf.Write([]byte(content)); err != nil { return f.Name()
return "", err
}
if err := testf.Close(); err != nil {
return "", err
}
return testf.Name(), nil
} }
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
@ -194,18 +195,12 @@ func TestMain(m *testing.M) {
} }
func TestSimpleEdit(t *testing.T) { func TestSimpleEdit(t *testing.T) {
file, err := createTestFile("micro_simple_edit_test", "base content") file := createTestFile(t, "base content")
if err != nil {
t.Error(err)
return
}
defer os.Remove(file)
openFile(file) openFile(file)
if findBuffer(file) == nil { if findBuffer(file) == nil {
t.Errorf("Could not find buffer %s", file) t.Fatalf("Could not find buffer %s", file)
return
} }
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone) injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
@ -223,28 +218,21 @@ func TestSimpleEdit(t *testing.T) {
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl) injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
data, err := ioutil.ReadFile(file) data, err := os.ReadFile(file)
if err != nil { if err != nil {
t.Error(err) t.Fatal(err)
return
} }
assert.Equal(t, "firstfoobar\nbase content\n", string(data)) assert.Equal(t, "firstfoobar\nbase content\n", string(data))
} }
func TestMouse(t *testing.T) { func TestMouse(t *testing.T) {
file, err := createTestFile("micro_mouse_test", "base content") file := createTestFile(t, "base content")
if err != nil {
t.Error(err)
return
}
defer os.Remove(file)
openFile(file) openFile(file)
if findBuffer(file) == nil { if findBuffer(file) == nil {
t.Errorf("Could not find buffer %s", file) t.Fatalf("Could not find buffer %s", file)
return
} }
// buffer: // buffer:
@ -275,10 +263,9 @@ func TestMouse(t *testing.T) {
// base content // base content
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl) injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
data, err := ioutil.ReadFile(file) data, err := os.ReadFile(file)
if err != nil { if err != nil {
t.Error(err) t.Fatal(err)
return
} }
assert.Equal(t, "firstline\nsecondline\nbase content\n", string(data)) assert.Equal(t, "firstline\nsecondline\nbase content\n", string(data))
@ -301,18 +288,12 @@ Ernleȝe test_string æðelen
` `
func TestSearchAndReplace(t *testing.T) { func TestSearchAndReplace(t *testing.T) {
file, err := createTestFile("micro_search_replace_test", srTestStart) file := createTestFile(t, srTestStart)
if err != nil {
t.Error(err)
return
}
defer os.Remove(file)
openFile(file) openFile(file)
if findBuffer(file) == nil { if findBuffer(file) == nil {
t.Errorf("Could not find buffer %s", file) t.Fatalf("Could not find buffer %s", file)
return
} }
injectKey(tcell.KeyCtrlE, rune(tcell.KeyCtrlE), tcell.ModCtrl) injectKey(tcell.KeyCtrlE, rune(tcell.KeyCtrlE), tcell.ModCtrl)
@ -321,10 +302,9 @@ func TestSearchAndReplace(t *testing.T) {
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl) injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
data, err := ioutil.ReadFile(file) data, err := os.ReadFile(file)
if err != nil { if err != nil {
t.Error(err) t.Fatal(err)
return
} }
assert.Equal(t, srTest2, string(data)) assert.Equal(t, srTest2, string(data))
@ -337,10 +317,9 @@ func TestSearchAndReplace(t *testing.T) {
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl) injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
data, err = ioutil.ReadFile(file) data, err = os.ReadFile(file)
if err != nil { if err != nil {
t.Error(err) t.Fatal(err)
return
} }
assert.Equal(t, srTest3, string(data)) assert.Equal(t, srTest3, string(data))

View File

@ -25,6 +25,7 @@
<category>TextEditor</category> <category>TextEditor</category>
</categories> </categories>
<releases> <releases>
<release version="2.0.14" date="2024-08-27"/>
<release version="2.0.13" date="2023-10-22"/> <release version="2.0.13" date="2023-10-22"/>
<release version="2.0.12" date="2023-09-06"/> <release version="2.0.12" date="2023-09-06"/>
<release version="2.0.11" date="2022-08-01"/> <release version="2.0.11" date="2022-08-01"/>

33
go.mod
View File

@ -5,26 +5,35 @@ require (
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0
github.com/go-errors/errors v1.0.1 github.com/go-errors/errors v1.0.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/go-isatty v0.0.11 github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-runewidth v0.0.7 github.com/mattn/go-runewidth v0.0.16
github.com/micro-editor/json5 v1.0.1-micro
github.com/micro-editor/tcell/v2 v2.0.11
github.com/micro-editor/terminal v0.0.0-20250324214352-e587e959c6b5
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/sergi/go-diff v1.1.0 github.com/sergi/go-diff v1.1.0
github.com/stretchr/testify v1.4.0 github.com/stretchr/testify v1.4.0
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb github.com/yuin/gopher-lua v1.1.1
github.com/zyedidia/clipper v0.1.1 github.com/zyedidia/clipper v0.1.1
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3 github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d golang.org/x/text v0.4.0
github.com/zyedidia/tcell/v2 v2.0.10 // indirect
github.com/zyedidia/terminal v0.0.0-20230315200948-4b3bcf6dddef
golang.org/x/text v0.3.8
gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v2 v2.2.8
layeh.com/gopher-luar v1.0.7 layeh.com/gopher-luar v1.0.11
) )
replace github.com/kballard/go-shellquote => github.com/zyedidia/go-shellquote v0.0.0-20200613203517-eccd813c0655 require (
github.com/creack/pty v1.1.18 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gdamore/encoding v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/zyedidia/poller v1.0.1 // indirect
golang.org/x/sys v0.28.0 // indirect
)
replace github.com/mattn/go-runewidth => github.com/zyedidia/go-runewidth v0.0.12 replace github.com/kballard/go-shellquote => github.com/micro-editor/go-shellquote v0.0.0-20250101105543-feb6c39314f5
replace layeh.com/gopher-luar => github.com/layeh/gopher-luar v1.0.7 replace layeh.com/gopher-luar v1.0.11 => github.com/layeh/gopher-luar v1.0.11
go 1.16 go 1.19

80
go.sum
View File

@ -19,84 +19,58 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/layeh/gopher-luar v1.0.7 h1:wnfZhYiJM748y1A4qYBfcFeMY9HWbdERny+ZL0f/jWc= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/layeh/gopher-luar v1.0.7/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk= github.com/layeh/gopher-luar v1.0.11 h1:ss6t9OtykOiETBScJylSMPhuYAtOmpH5rSX10/wCcis=
github.com/layeh/gopher-luar v1.0.11/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/micro-editor/go-shellquote v0.0.0-20250101105543-feb6c39314f5 h1:D7BPnsedXiKo/e8RTFX419/52ICNhU8UKPQGZ/0yiLc=
github.com/micro-editor/go-shellquote v0.0.0-20250101105543-feb6c39314f5/go.mod h1:zaPgW/fDiW4MUfEwxpC+GB/bhvX44NJaNHmRAC9auHQ=
github.com/micro-editor/json5 v1.0.1-micro h1:5Y4MuzhkmW0sQQNPvrIVevIOKi557qsznwjRr4iq1AI=
github.com/micro-editor/json5 v1.0.1-micro/go.mod h1:cmlPHZ1JKOXNse0/3zwwKj/GUpzAVkzx4lZDkpHl4q0=
github.com/micro-editor/tcell/v2 v2.0.11 h1:USjdpBSmbocx2yPARbY19KcUSj+ZerScrdmBqGjzoX4=
github.com/micro-editor/tcell/v2 v2.0.11/go.mod h1:kVYk6NOwYJrboL/7IA7cCupk4o2NzyF/0UMLjeEJN/s=
github.com/micro-editor/terminal v0.0.0-20250324214352-e587e959c6b5 h1:czSkYUNmHuWS2lv8VreufENEXZNOCGZcXd744YKf8yM=
github.com/micro-editor/terminal v0.0.0-20250324214352-e587e959c6b5/go.mod h1:OszIG7ockt4osicVHq6gI2QmV4PBDK6H5/Bj8GDGv4Q=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/xo/terminfo v0.0.0-20200218205459-454e5b68f9e8 h1:woqigIZtZUZxws1zZA99nAvuz2mQrxtWsuZSR9c8I/A=
github.com/xo/terminfo v0.0.0-20200218205459-454e5b68f9e8/go.mod h1:6Yhx5ZJl5942QrNRWLwITArVT9okUXc5c3brgWJMoDc= github.com/xo/terminfo v0.0.0-20200218205459-454e5b68f9e8/go.mod h1:6Yhx5ZJl5942QrNRWLwITArVT9okUXc5c3brgWJMoDc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zyedidia/clipper v0.1.1 h1:HBgguFNDq/QmSQKBnhy4sMKzILINr139VEgAhftOUTw= github.com/zyedidia/clipper v0.1.1 h1:HBgguFNDq/QmSQKBnhy4sMKzILINr139VEgAhftOUTw=
github.com/zyedidia/clipper v0.1.1/go.mod h1:7YApPNiiTZTXdKKZG92G50qj6mnWEX975Sdu65J7YpQ= github.com/zyedidia/clipper v0.1.1/go.mod h1:7YApPNiiTZTXdKKZG92G50qj6mnWEX975Sdu65J7YpQ=
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3 h1:oMHjjTLfGXVuyOQBYj5/td9WC0mw4g1xDBPovIqmHew= github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3 h1:oMHjjTLfGXVuyOQBYj5/td9WC0mw4g1xDBPovIqmHew=
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3/go.mod h1:YKbIYP//Eln8eDgAJGI3IDvR3s4Tv9Z9TGIOumiyQ5c= github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3/go.mod h1:YKbIYP//Eln8eDgAJGI3IDvR3s4Tv9Z9TGIOumiyQ5c=
github.com/zyedidia/go-runewidth v0.0.12 h1:aHWj8qL3aH7caRzoPBJXe1pEaZBXHpKtfTuiBo5p74Q=
github.com/zyedidia/go-runewidth v0.0.12/go.mod h1:vF8djYdLmG8BJaUZ4CznFYCJ3pFR8m4B4VinTvTTarU=
github.com/zyedidia/go-shellquote v0.0.0-20200613203517-eccd813c0655 h1:Z3RhH6hvcSx7eX6Q/pP6YVsgea/1eMDG99vtWwi3nK4=
github.com/zyedidia/go-shellquote v0.0.0-20200613203517-eccd813c0655/go.mod h1:1sTqqO+kcYzZp43M5VsJe1tns9IzlSeC9jB6c2+o/5Y=
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d h1:zmDMkh22zXOB7gz8jFaI4GpI7llsPgzm38/jG0UgxjE=
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d/go.mod h1:NDJSTTYWivnza6zkRapeX2/LwhKPEMQ7bJxqgDVT78I=
github.com/zyedidia/poller v1.0.1 h1:Tt9S3AxAjXwWGNiC2TUdRJkQDZSzCBNVQ4xXiQ7440s= github.com/zyedidia/poller v1.0.1 h1:Tt9S3AxAjXwWGNiC2TUdRJkQDZSzCBNVQ4xXiQ7440s=
github.com/zyedidia/poller v1.0.1/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE= github.com/zyedidia/poller v1.0.1/go.mod h1:vZXJOHGDcuK08GXhF6IAY0ZFd2WcgOR5DOTp84Uk5eE=
github.com/zyedidia/tcell/v2 v2.0.9 h1:FxXRkE62N0GPHES7EMLtp2rteYqC9r1kVid8vJN1kOE=
github.com/zyedidia/tcell/v2 v2.0.9/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
github.com/zyedidia/tcell/v2 v2.0.10-0.20221007181625-f562052bccb8 h1:53ULv4mmLyQDnqbjVxanckP57WSreWHwTmlLJrJEutY=
github.com/zyedidia/tcell/v2 v2.0.10-0.20221007181625-f562052bccb8/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
github.com/zyedidia/tcell/v2 v2.0.10-0.20230320201625-54f6acdada4a h1:W4TWa++Wk6uRGxZoxr2nPX1TpIEl+Wxv0mTtocG4TYc=
github.com/zyedidia/tcell/v2 v2.0.10-0.20230320201625-54f6acdada4a/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
github.com/zyedidia/tcell/v2 v2.0.10-0.20230831153116-061c5b2c7260 h1:SCAmAacT5BxZsmOFdFy5zwwi6nj1MjA60gydjKdTgXo=
github.com/zyedidia/tcell/v2 v2.0.10-0.20230831153116-061c5b2c7260/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
github.com/zyedidia/tcell/v2 v2.0.10 h1:6fbbYAx/DYc9A//4jU1OeBrxtc9qJxYCZXCtGQbtTWU=
github.com/zyedidia/tcell/v2 v2.0.10/go.mod h1:i4NNlquIQXFeNecrOgxDQQJdu+7LmTi3g62asvmwUws=
github.com/zyedidia/terminal v0.0.0-20230315200948-4b3bcf6dddef h1:LeB4Qs0Tss4r/Qh8pfsTTqagDYHysfKJLYzAH3MVfu0=
github.com/zyedidia/terminal v0.0.0-20230315200948-4b3bcf6dddef/go.mod h1:zeb8MJdcCObFKVvur3n2B4BANIPuo2Q8r4iiNs9Enx0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=

View File

@ -11,6 +11,7 @@ import (
"time" "time"
shellquote "github.com/kballard/go-shellquote" shellquote "github.com/kballard/go-shellquote"
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/clipboard" "github.com/zyedidia/micro/v2/internal/clipboard"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
@ -18,7 +19,6 @@ import (
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell" "github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2"
) )
// ScrollUp is not an action // ScrollUp is not an action
@ -46,6 +46,14 @@ func (h *BufPane) ScrollAdjust() {
h.SetView(v) h.SetView(v)
} }
// ScrollReachedEnd returns true if the view is at the end of the buffer,
// i.e. the last line of the buffer is in the view.
func (h *BufPane) ScrollReachedEnd() bool {
v := h.GetView()
end := h.SLocFromLoc(h.Buf.End())
return h.Diff(v.StartLine, end) < h.BufView().Height
}
// MousePress is the event that should happen when a normal click happens // MousePress is the event that should happen when a normal click happens
// This is almost always bound to left click // This is almost always bound to left click
func (h *BufPane) MousePress(e *tcell.EventMouse) bool { func (h *BufPane) MousePress(e *tcell.EventMouse) bool {
@ -65,12 +73,12 @@ func (h *BufPane) MousePress(e *tcell.EventMouse) bool {
h.Cursor.Loc = mouseLoc h.Cursor.Loc = mouseLoc
} }
if time.Since(h.lastClickTime)/time.Millisecond < config.DoubleClickThreshold && (mouseLoc.X == h.lastLoc.X && mouseLoc.Y == h.lastLoc.Y) { if time.Since(h.lastClickTime)/time.Millisecond < config.DoubleClickThreshold && (mouseLoc.X == h.lastLoc.X && mouseLoc.Y == h.lastLoc.Y) {
if h.doubleClick { if h.DoubleClick {
// Triple click // Triple click
h.lastClickTime = time.Now() h.lastClickTime = time.Now()
h.tripleClick = true h.TripleClick = true
h.doubleClick = false h.DoubleClick = false
h.Cursor.SelectLine() h.Cursor.SelectLine()
h.Cursor.CopySelection(clipboard.PrimaryReg) h.Cursor.CopySelection(clipboard.PrimaryReg)
@ -78,15 +86,15 @@ func (h *BufPane) MousePress(e *tcell.EventMouse) bool {
// Double click // Double click
h.lastClickTime = time.Now() h.lastClickTime = time.Now()
h.doubleClick = true h.DoubleClick = true
h.tripleClick = false h.TripleClick = false
h.Cursor.SelectWord() h.Cursor.SelectWord()
h.Cursor.CopySelection(clipboard.PrimaryReg) h.Cursor.CopySelection(clipboard.PrimaryReg)
} }
} else { } else {
h.doubleClick = false h.DoubleClick = false
h.tripleClick = false h.TripleClick = false
h.lastClickTime = time.Now() h.lastClickTime = time.Now()
h.Cursor.OrigSelection[0] = h.Cursor.Loc h.Cursor.OrigSelection[0] = h.Cursor.Loc
@ -108,9 +116,9 @@ func (h *BufPane) MouseDrag(e *tcell.EventMouse) bool {
} }
h.Cursor.Loc = h.LocFromVisual(buffer.Loc{mx, my}) h.Cursor.Loc = h.LocFromVisual(buffer.Loc{mx, my})
if h.tripleClick { if h.TripleClick {
h.Cursor.AddLineToSelection() h.Cursor.AddLineToSelection()
} else if h.doubleClick { } else if h.DoubleClick {
h.Cursor.AddWordToSelection() h.Cursor.AddWordToSelection()
} else { } else {
h.Cursor.SelectTo(h.Cursor.Loc) h.Cursor.SelectTo(h.Cursor.Loc)
@ -127,7 +135,7 @@ func (h *BufPane) MouseRelease(e *tcell.EventMouse) bool {
// that doesn't support mouse motion events. But when the mouse click is // that doesn't support mouse motion events. But when the mouse click is
// within the scroll margin, that would cause a scroll and selection // within the scroll margin, that would cause a scroll and selection
// even for a simple mouse click, which is not good. // even for a simple mouse click, which is not good.
// if !h.doubleClick && !h.tripleClick { // if !h.DoubleClick && !h.TripleClick {
// mx, my := e.Position() // mx, my := e.Position()
// h.Cursor.Loc = h.LocFromVisual(buffer.Loc{mx, my}) // h.Cursor.Loc = h.LocFromVisual(buffer.Loc{mx, my})
// h.Cursor.SetSelectionEnd(h.Cursor.Loc) // h.Cursor.SetSelectionEnd(h.Cursor.Loc)
@ -145,7 +153,7 @@ func (h *BufPane) ScrollUpAction() bool {
return true return true
} }
// ScrollDownAction scrolls the view up // ScrollDownAction scrolls the view down
func (h *BufPane) ScrollDownAction() bool { func (h *BufPane) ScrollDownAction() bool {
h.ScrollDown(util.IntOpt(h.Buf.Settings["scrollspeed"])) h.ScrollDown(util.IntOpt(h.Buf.Settings["scrollspeed"]))
return true return true
@ -160,6 +168,52 @@ func (h *BufPane) Center() bool {
return true return true
} }
// CursorToViewTop moves the cursor to the top of the view,
// offset by scrollmargin unless at the beginning or end of the file
func (h *BufPane) CursorToViewTop() bool {
v := h.GetView()
h.Buf.ClearCursors()
scrollmargin := int(h.Buf.Settings["scrollmargin"].(float64))
bStart := display.SLoc{0, 0}
if v.StartLine == bStart {
scrollmargin = 0
}
h.Cursor.GotoLoc(h.LocFromVLoc(display.VLoc{
SLoc: h.Scroll(v.StartLine, scrollmargin),
VisualX: 0,
}))
return true
}
// CursorToViewCenter moves the cursor to the center of the view
func (h *BufPane) CursorToViewCenter() bool {
v := h.GetView()
h.Buf.ClearCursors()
h.Cursor.GotoLoc(h.LocFromVLoc(display.VLoc{
SLoc: h.Scroll(v.StartLine, h.BufView().Height/2),
VisualX: 0,
}))
return true
}
// CursorToViewBottom moves the cursor to the bottom of the view,
// offset by scrollmargin unless at the beginning or end of the file
func (h *BufPane) CursorToViewBottom() bool {
v := h.GetView()
h.Buf.ClearCursors()
scrollmargin := int(h.Buf.Settings["scrollmargin"].(float64))
bEnd := h.SLocFromLoc(h.Buf.End())
lastLine := h.Scroll(v.StartLine, h.BufView().Height-1)
if lastLine == bEnd {
scrollmargin = 0
}
h.Cursor.GotoLoc(h.LocFromVLoc(display.VLoc{
SLoc: h.Scroll(lastLine, -scrollmargin),
VisualX: 0,
}))
return true
}
// MoveCursorUp is not an action // MoveCursorUp is not an action
func (h *BufPane) MoveCursorUp(n int) { func (h *BufPane) MoveCursorUp(n int) {
if !h.Buf.Settings["softwrap"].(bool) { if !h.Buf.Settings["softwrap"].(bool) {
@ -170,10 +224,10 @@ func (h *BufPane) MoveCursorUp(n int) {
if sloc == vloc.SLoc { if sloc == vloc.SLoc {
// we are at the beginning of buffer // we are at the beginning of buffer
h.Cursor.Loc = h.Buf.Start() h.Cursor.Loc = h.Buf.Start()
h.Cursor.LastVisualX = 0 h.Cursor.StoreVisualX()
} else { } else {
vloc.SLoc = sloc vloc.SLoc = sloc
vloc.VisualX = h.Cursor.LastVisualX vloc.VisualX = h.Cursor.LastWrappedVisualX
h.Cursor.Loc = h.LocFromVLoc(vloc) h.Cursor.Loc = h.LocFromVLoc(vloc)
} }
} }
@ -189,11 +243,10 @@ func (h *BufPane) MoveCursorDown(n int) {
if sloc == vloc.SLoc { if sloc == vloc.SLoc {
// we are at the end of buffer // we are at the end of buffer
h.Cursor.Loc = h.Buf.End() h.Cursor.Loc = h.Buf.End()
vloc = h.VLocFromLoc(h.Cursor.Loc) h.Cursor.StoreVisualX()
h.Cursor.LastVisualX = vloc.VisualX
} else { } else {
vloc.SLoc = sloc vloc.SLoc = sloc
vloc.VisualX = h.Cursor.LastVisualX vloc.VisualX = h.Cursor.LastWrappedVisualX
h.Cursor.Loc = h.LocFromVLoc(vloc) h.Cursor.Loc = h.LocFromVLoc(vloc)
} }
} }
@ -209,8 +262,13 @@ func (h *BufPane) CursorUp() bool {
// CursorDown moves the cursor down // CursorDown moves the cursor down
func (h *BufPane) CursorDown() bool { func (h *BufPane) CursorDown() bool {
selectionEndNewline := h.Cursor.HasSelection() && h.Cursor.CurSelection[1].X == 0
h.Cursor.Deselect(false) h.Cursor.Deselect(false)
h.MoveCursorDown(1) if selectionEndNewline {
h.Cursor.Start()
} else {
h.MoveCursorDown(1)
}
h.Relocate() h.Relocate()
return true return true
} }
@ -244,7 +302,6 @@ func (h *BufPane) CursorLeft() bool {
func (h *BufPane) CursorRight() bool { func (h *BufPane) CursorRight() bool {
if h.Cursor.HasSelection() { if h.Cursor.HasSelection() {
h.Cursor.Deselect(false) h.Cursor.Deselect(false)
h.Cursor.Right()
} else { } else {
tabstospaces := h.Buf.Settings["tabstospaces"].(bool) tabstospaces := h.Buf.Settings["tabstospaces"].(bool)
tabmovement := h.Buf.Settings["tabmovement"].(bool) tabmovement := h.Buf.Settings["tabmovement"].(bool)
@ -657,7 +714,7 @@ func (h *BufPane) InsertNewline() bool {
h.Buf.Remove(buffer.Loc{X: 0, Y: h.Cursor.Y - 1}, buffer.Loc{X: util.CharacterCount(line), Y: h.Cursor.Y - 1}) h.Buf.Remove(buffer.Loc{X: 0, Y: h.Cursor.Y - 1}, buffer.Loc{X: util.CharacterCount(line), Y: h.Cursor.Y - 1})
} }
} }
h.Cursor.LastVisualX = h.Cursor.GetVisualX() h.Cursor.StoreVisualX()
h.Relocate() h.Relocate()
return true return true
} }
@ -687,7 +744,7 @@ func (h *BufPane) Backspace() bool {
h.Buf.Remove(loc.Move(-1, h.Buf), loc) h.Buf.Remove(loc.Move(-1, h.Buf), loc)
} }
} }
h.Cursor.LastVisualX = h.Cursor.GetVisualX() h.Cursor.StoreVisualX()
h.Relocate() h.Relocate()
return true return true
} }
@ -854,6 +911,11 @@ func (h *BufPane) Autocomplete() bool {
return false return false
} }
if b.HasSuggestions {
b.CycleAutocomplete(true)
return true
}
if h.Cursor.X == 0 { if h.Cursor.X == 0 {
return false return false
} }
@ -864,10 +926,6 @@ func (h *BufPane) Autocomplete() bool {
return false return false
} }
if b.HasSuggestions {
b.CycleAutocomplete(true)
return true
}
return b.Autocomplete(buffer.BufferComplete) return b.Autocomplete(buffer.BufferComplete)
} }
@ -889,7 +947,7 @@ func (h *BufPane) InsertTab() bool {
b := h.Buf b := h.Buf
indent := b.IndentString(util.IntOpt(b.Settings["tabsize"])) indent := b.IndentString(util.IntOpt(b.Settings["tabsize"]))
tabBytes := len(indent) tabBytes := len(indent)
bytesUntilIndent := tabBytes - (h.Cursor.GetVisualX() % tabBytes) bytesUntilIndent := tabBytes - (h.Cursor.GetVisualX(false) % tabBytes)
b.Insert(h.Cursor.Loc, indent[:bytesUntilIndent]) b.Insert(h.Cursor.Loc, indent[:bytesUntilIndent])
h.Relocate() h.Relocate()
return true return true
@ -946,6 +1004,9 @@ func (h *BufPane) SaveAsCB(action string, callback func()) bool {
h.completeAction(action) h.completeAction(action)
return return
} }
} else {
InfoBar.Error(err)
return
} }
} else { } else {
InfoBar.YNPrompt( InfoBar.YNPrompt(
@ -982,8 +1043,6 @@ func (h *BufPane) saveBufToFile(filename string, action string, callback func())
if err != nil { if err != nil {
InfoBar.Error(err) InfoBar.Error(err)
} else { } else {
h.Buf.Path = filename
h.Buf.SetName(filename)
InfoBar.Message("Saved " + filename) InfoBar.Message("Saved " + filename)
if callback != nil { if callback != nil {
callback() callback()
@ -1008,8 +1067,6 @@ func (h *BufPane) saveBufToFile(filename string, action string, callback func())
InfoBar.Error(err) InfoBar.Error(err)
} }
} else { } else {
h.Buf.Path = filename
h.Buf.SetName(filename)
InfoBar.Message("Saved " + filename) InfoBar.Message("Saved " + filename)
if callback != nil { if callback != nil {
callback() callback()
@ -1154,6 +1211,14 @@ func (h *BufPane) FindNext() bool {
match, found, err := h.Buf.FindNext(h.Buf.LastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, true, h.Buf.LastSearchRegex) match, found, err := h.Buf.FindNext(h.Buf.LastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, true, h.Buf.LastSearchRegex)
if err != nil { if err != nil {
InfoBar.Error(err) InfoBar.Error(err)
} else if found && searchLoc == match[0] && match[0] == match[1] {
// skip empty match at present cursor location
if searchLoc == h.Buf.End() {
searchLoc = h.Buf.Start()
} else {
searchLoc = searchLoc.Move(1, h.Buf)
}
match, found, _ = h.Buf.FindNext(h.Buf.LastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, true, h.Buf.LastSearchRegex)
} }
if found { if found {
h.Cursor.SetSelectionStart(match[0]) h.Cursor.SetSelectionStart(match[0])
@ -1183,6 +1248,14 @@ func (h *BufPane) FindPrevious() bool {
match, found, err := h.Buf.FindNext(h.Buf.LastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, false, h.Buf.LastSearchRegex) match, found, err := h.Buf.FindNext(h.Buf.LastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, false, h.Buf.LastSearchRegex)
if err != nil { if err != nil {
InfoBar.Error(err) InfoBar.Error(err)
} else if found && searchLoc == match[0] && match[0] == match[1] {
// skip empty match at present cursor location
if searchLoc == h.Buf.Start() {
searchLoc = h.Buf.End()
} else {
searchLoc = searchLoc.Move(-1, h.Buf)
}
match, found, _ = h.Buf.FindNext(h.Buf.LastSearch, h.Buf.Start(), h.Buf.End(), searchLoc, false, h.Buf.LastSearchRegex)
} }
if found { if found {
h.Cursor.SetSelectionStart(match[0]) h.Cursor.SetSelectionStart(match[0])
@ -1238,101 +1311,189 @@ func (h *BufPane) Redo() bool {
return true return true
} }
func (h *BufPane) selectLines() int {
if h.Cursor.HasSelection() {
start := h.Cursor.CurSelection[0]
end := h.Cursor.CurSelection[1]
if start.GreaterThan(end) {
start, end = end, start
}
if end.X == 0 {
end = end.Move(-1, h.Buf)
}
h.Cursor.Deselect(true)
h.Cursor.SetSelectionStart(buffer.Loc{0, start.Y})
h.Cursor.SetSelectionEnd(buffer.Loc{0, end.Y + 1})
} else {
h.Cursor.SelectLine()
}
nlines := h.Cursor.CurSelection[1].Y - h.Cursor.CurSelection[0].Y
if nlines == 0 && h.Cursor.HasSelection() {
// selected last line and it is not empty
nlines++
}
return nlines
}
// Copy the selection to the system clipboard // Copy the selection to the system clipboard
func (h *BufPane) Copy() bool { func (h *BufPane) Copy() bool {
if h.Cursor.HasSelection() {
h.Cursor.CopySelection(clipboard.ClipboardReg)
h.freshClip = true
InfoBar.Message("Copied selection")
}
h.Relocate()
return true
}
// CopyLine copies the current line to the clipboard
func (h *BufPane) CopyLine() bool {
if h.Cursor.HasSelection() {
return false
}
origLoc := h.Cursor.Loc
h.Cursor.SelectLine()
h.Cursor.CopySelection(clipboard.ClipboardReg)
h.freshClip = true
InfoBar.Message("Copied line")
h.Cursor.Deselect(true)
h.Cursor.Loc = origLoc
h.Relocate()
return true
}
// CutLine cuts the current line to the clipboard
func (h *BufPane) CutLine() bool {
h.Cursor.SelectLine()
if !h.Cursor.HasSelection() { if !h.Cursor.HasSelection() {
return false return false
} }
if h.freshClip { h.Cursor.CopySelection(clipboard.ClipboardReg)
if h.Cursor.HasSelection() { h.freshClip = false
if clip, err := clipboard.Read(clipboard.ClipboardReg); err != nil { InfoBar.Message("Copied selection")
InfoBar.Error(err) h.Relocate()
} else { return true
clipboard.WriteMulti(clip+string(h.Cursor.GetSelection()), clipboard.ClipboardReg, h.Cursor.Num, h.Buf.NumCursors()) }
}
} // CopyLine copies the current line to the clipboard. If there is a selection,
} else if time.Since(h.lastCutTime)/time.Second > 10*time.Second || !h.freshClip { // CopyLine copies all the lines that are (fully or partially) in the selection.
h.Copy() func (h *BufPane) CopyLine() bool {
origLoc := h.Cursor.Loc
origLastVisualX := h.Cursor.LastVisualX
origLastWrappedVisualX := h.Cursor.LastWrappedVisualX
origSelection := h.Cursor.CurSelection
nlines := h.selectLines()
if nlines == 0 {
return false
} }
h.freshClip = true h.Cursor.CopySelection(clipboard.ClipboardReg)
h.lastCutTime = time.Now() h.freshClip = false
h.Cursor.DeleteSelection() if nlines > 1 {
h.Cursor.ResetSelection() InfoBar.Message(fmt.Sprintf("Copied %d lines", nlines))
InfoBar.Message("Cut line") } else {
InfoBar.Message("Copied line")
}
h.Cursor.Loc = origLoc
h.Cursor.LastVisualX = origLastVisualX
h.Cursor.LastWrappedVisualX = origLastWrappedVisualX
h.Cursor.CurSelection = origSelection
h.Relocate() h.Relocate()
return true return true
} }
// Cut the selection to the system clipboard // Cut the selection to the system clipboard
func (h *BufPane) Cut() bool { func (h *BufPane) Cut() bool {
if h.Cursor.HasSelection() { if !h.Cursor.HasSelection() {
h.Cursor.CopySelection(clipboard.ClipboardReg) return false
h.Cursor.DeleteSelection()
h.Cursor.ResetSelection()
h.freshClip = true
InfoBar.Message("Cut selection")
h.Relocate()
return true
} }
return h.CutLine() h.Cursor.CopySelection(clipboard.ClipboardReg)
} h.Cursor.DeleteSelection()
h.Cursor.ResetSelection()
h.freshClip = false
InfoBar.Message("Cut selection")
// DuplicateLine duplicates the current line or selection
func (h *BufPane) DuplicateLine() bool {
var infoMessage = "Duplicated line"
if h.Cursor.HasSelection() {
infoMessage = "Duplicated selection"
h.Buf.Insert(h.Cursor.CurSelection[1], string(h.Cursor.GetSelection()))
} else {
h.Cursor.End()
h.Buf.Insert(h.Cursor.Loc, "\n"+string(h.Buf.LineBytes(h.Cursor.Y)))
// h.Cursor.Right()
}
InfoBar.Message(infoMessage)
h.Relocate() h.Relocate()
return true return true
} }
// DeleteLine deletes the current line // CutLine cuts the current line to the clipboard. If there is a selection,
func (h *BufPane) DeleteLine() bool { // CutLine cuts all the lines that are (fully or partially) in the selection.
h.Cursor.SelectLine() func (h *BufPane) CutLine() bool {
nlines := h.selectLines()
if nlines == 0 {
return false
}
totalLines := nlines
if h.freshClip {
if clip, err := clipboard.Read(clipboard.ClipboardReg); err != nil {
InfoBar.Error(err)
return false
} else {
clipboard.WriteMulti(clip+string(h.Cursor.GetSelection()), clipboard.ClipboardReg, h.Cursor.Num, h.Buf.NumCursors())
totalLines = strings.Count(clip, "\n") + nlines
}
} else {
h.Cursor.CopySelection(clipboard.ClipboardReg)
}
h.freshClip = true
h.Cursor.DeleteSelection()
h.Cursor.ResetSelection()
h.Cursor.StoreVisualX()
if totalLines > 1 {
InfoBar.Message(fmt.Sprintf("Cut %d lines", totalLines))
} else {
InfoBar.Message("Cut line")
}
h.Relocate()
return true
}
// Duplicate the selection
func (h *BufPane) Duplicate() bool {
if !h.Cursor.HasSelection() { if !h.Cursor.HasSelection() {
return false return false
} }
h.Buf.Insert(h.Cursor.CurSelection[1], string(h.Cursor.GetSelection()))
InfoBar.Message("Duplicated selection")
h.Relocate()
return true
}
// DuplicateLine duplicates the current line. If there is a selection, DuplicateLine
// duplicates all the lines that are (fully or partially) in the selection.
func (h *BufPane) DuplicateLine() bool {
if h.Cursor.HasSelection() {
origLoc := h.Cursor.Loc
origLastVisualX := h.Cursor.LastVisualX
origLastWrappedVisualX := h.Cursor.LastWrappedVisualX
origSelection := h.Cursor.CurSelection
start := h.Cursor.CurSelection[0]
end := h.Cursor.CurSelection[1]
if start.GreaterThan(end) {
start, end = end, start
}
if end.X == 0 {
end = end.Move(-1, h.Buf)
}
h.Cursor.Deselect(true)
h.Cursor.Loc = end
h.Cursor.End()
for y := start.Y; y <= end.Y; y++ {
h.Buf.Insert(h.Cursor.Loc, "\n"+string(h.Buf.LineBytes(y)))
}
h.Cursor.Loc = origLoc
h.Cursor.LastVisualX = origLastVisualX
h.Cursor.LastWrappedVisualX = origLastWrappedVisualX
h.Cursor.CurSelection = origSelection
if start.Y < end.Y {
InfoBar.Message(fmt.Sprintf("Duplicated %d lines", end.Y-start.Y+1))
} else {
InfoBar.Message("Duplicated line")
}
} else {
h.Cursor.End()
h.Buf.Insert(h.Cursor.Loc, "\n"+string(h.Buf.LineBytes(h.Cursor.Y)))
InfoBar.Message("Duplicated line")
}
h.Relocate()
return true
}
// DeleteLine deletes the current line. If there is a selection, DeleteLine
// deletes all the lines that are (fully or partially) in the selection.
func (h *BufPane) DeleteLine() bool {
nlines := h.selectLines()
if nlines == 0 {
return false
}
h.Cursor.DeleteSelection() h.Cursor.DeleteSelection()
h.Cursor.ResetSelection() h.Cursor.ResetSelection()
InfoBar.Message("Deleted line") h.Cursor.StoreVisualX()
if nlines > 1 {
InfoBar.Message(fmt.Sprintf("Deleted %d lines", nlines))
} else {
InfoBar.Message("Deleted line")
}
h.Relocate() h.Relocate()
return true return true
} }
@ -1536,63 +1697,84 @@ func (h *BufPane) End() bool {
// PageUp scrolls the view up a page // PageUp scrolls the view up a page
func (h *BufPane) PageUp() bool { func (h *BufPane) PageUp() bool {
h.ScrollUp(h.BufView().Height) pageOverlap := int(h.Buf.Settings["pageoverlap"].(float64))
h.ScrollUp(h.BufView().Height - pageOverlap)
return true return true
} }
// PageDown scrolls the view down a page // PageDown scrolls the view down a page
func (h *BufPane) PageDown() bool { func (h *BufPane) PageDown() bool {
h.ScrollDown(h.BufView().Height) pageOverlap := int(h.Buf.Settings["pageoverlap"].(float64))
h.ScrollDown(h.BufView().Height - pageOverlap)
h.ScrollAdjust() h.ScrollAdjust()
return true return true
} }
// SelectPageUp selects up one page // SelectPageUp selects up one page
func (h *BufPane) SelectPageUp() bool { func (h *BufPane) SelectPageUp() bool {
pageOverlap := int(h.Buf.Settings["pageoverlap"].(float64))
scrollAmount := h.BufView().Height - pageOverlap
if !h.Cursor.HasSelection() { if !h.Cursor.HasSelection() {
h.Cursor.OrigSelection[0] = h.Cursor.Loc h.Cursor.OrigSelection[0] = h.Cursor.Loc
} }
h.MoveCursorUp(h.BufView().Height) h.MoveCursorUp(scrollAmount)
h.Cursor.SelectTo(h.Cursor.Loc) h.Cursor.SelectTo(h.Cursor.Loc)
if h.Cursor.Num == 0 {
h.ScrollUp(scrollAmount)
}
h.Relocate() h.Relocate()
return true return true
} }
// SelectPageDown selects down one page // SelectPageDown selects down one page
func (h *BufPane) SelectPageDown() bool { func (h *BufPane) SelectPageDown() bool {
pageOverlap := int(h.Buf.Settings["pageoverlap"].(float64))
scrollAmount := h.BufView().Height - pageOverlap
if !h.Cursor.HasSelection() { if !h.Cursor.HasSelection() {
h.Cursor.OrigSelection[0] = h.Cursor.Loc h.Cursor.OrigSelection[0] = h.Cursor.Loc
} }
h.MoveCursorDown(h.BufView().Height) h.MoveCursorDown(scrollAmount)
h.Cursor.SelectTo(h.Cursor.Loc) h.Cursor.SelectTo(h.Cursor.Loc)
if h.Cursor.Num == 0 && !h.ScrollReachedEnd() {
h.ScrollDown(scrollAmount)
h.ScrollAdjust()
}
h.Relocate() h.Relocate()
return true return true
} }
// CursorPageUp places the cursor a page up // CursorPageUp places the cursor a page up,
// moving the view to keep cursor at the same relative position in the view
func (h *BufPane) CursorPageUp() bool { func (h *BufPane) CursorPageUp() bool {
h.Cursor.Deselect(true) h.Cursor.Deselect(true)
pageOverlap := int(h.Buf.Settings["pageoverlap"].(float64))
if h.Cursor.HasSelection() { scrollAmount := h.BufView().Height - pageOverlap
h.Cursor.Loc = h.Cursor.CurSelection[0] h.MoveCursorUp(scrollAmount)
h.Cursor.ResetSelection() if h.Cursor.Num == 0 {
h.Cursor.StoreVisualX() h.ScrollUp(scrollAmount)
} }
h.MoveCursorUp(h.BufView().Height)
h.Relocate() h.Relocate()
return true return true
} }
// CursorPageDown places the cursor a page up // CursorPageDown places the cursor a page down,
// moving the view to keep cursor at the same relative position in the view
func (h *BufPane) CursorPageDown() bool { func (h *BufPane) CursorPageDown() bool {
selectionEndNewline := h.Cursor.HasSelection() && h.Cursor.CurSelection[1].X == 0
h.Cursor.Deselect(false) h.Cursor.Deselect(false)
pageOverlap := int(h.Buf.Settings["pageoverlap"].(float64))
if h.Cursor.HasSelection() { scrollAmount := h.BufView().Height - pageOverlap
h.Cursor.Loc = h.Cursor.CurSelection[1] if selectionEndNewline {
h.Cursor.ResetSelection() scrollAmount--
h.Cursor.StoreVisualX() }
h.MoveCursorDown(scrollAmount)
if h.Cursor.Num == 0 && !h.ScrollReachedEnd() {
h.ScrollDown(scrollAmount)
h.ScrollAdjust()
}
if selectionEndNewline {
h.Cursor.Start()
} }
h.MoveCursorDown(h.BufView().Height)
h.Relocate() h.Relocate()
return true return true
} }
@ -1612,12 +1794,12 @@ func (h *BufPane) HalfPageDown() bool {
// ToggleDiffGutter turns the diff gutter off and on // ToggleDiffGutter turns the diff gutter off and on
func (h *BufPane) ToggleDiffGutter() bool { func (h *BufPane) ToggleDiffGutter() bool {
if !h.Buf.Settings["diffgutter"].(bool) { diffgutter := !h.Buf.Settings["diffgutter"].(bool)
h.Buf.Settings["diffgutter"] = true h.Buf.SetOptionNative("diffgutter", diffgutter)
if diffgutter {
h.Buf.UpdateDiff() h.Buf.UpdateDiff()
InfoBar.Message("Enabled diff gutter") InfoBar.Message("Enabled diff gutter")
} else { } else {
h.Buf.Settings["diffgutter"] = false
InfoBar.Message("Disabled diff gutter") InfoBar.Message("Disabled diff gutter")
} }
return true return true
@ -1625,11 +1807,11 @@ func (h *BufPane) ToggleDiffGutter() bool {
// ToggleRuler turns line numbers off and on // ToggleRuler turns line numbers off and on
func (h *BufPane) ToggleRuler() bool { func (h *BufPane) ToggleRuler() bool {
if !h.Buf.Settings["ruler"].(bool) { ruler := !h.Buf.Settings["ruler"].(bool)
h.Buf.Settings["ruler"] = true h.Buf.SetOptionNative("ruler", ruler)
if ruler {
InfoBar.Message("Enabled ruler") InfoBar.Message("Enabled ruler")
} else { } else {
h.Buf.Settings["ruler"] = false
InfoBar.Message("Disabled ruler") InfoBar.Message("Disabled ruler")
} }
return true return true
@ -1645,7 +1827,8 @@ func (h *BufPane) ToggleHelp() bool {
if h.Buf.Type == buffer.BTHelp { if h.Buf.Type == buffer.BTHelp {
h.Quit() h.Quit()
} else { } else {
h.openHelp("help") hsplit := config.GlobalSettings["helpsplit"] == "hsplit"
h.openHelp("help", hsplit, false)
} }
return true return true
} }
@ -1681,7 +1864,7 @@ func (h *BufPane) CommandMode() bool {
// ToggleOverwriteMode lets the user toggle the text overwrite mode // ToggleOverwriteMode lets the user toggle the text overwrite mode
func (h *BufPane) ToggleOverwriteMode() bool { func (h *BufPane) ToggleOverwriteMode() bool {
h.isOverwriteMode = !h.isOverwriteMode h.Buf.OverwriteMode = !h.Buf.OverwriteMode
return true return true
} }
@ -1708,11 +1891,11 @@ func (h *BufPane) ClearInfo() bool {
return true return true
} }
// ForceQuit closes the current tab or view even if there are unsaved changes // ForceQuit closes the tab or view even if there are unsaved changes
// (no prompt) // (no prompt)
func (h *BufPane) ForceQuit() bool { func (h *BufPane) ForceQuit() bool {
h.Buf.Close() h.Buf.Close()
if len(MainTab().Panes) > 1 { if len(h.tab.Panes) > 1 {
h.Unsplit() h.Unsplit()
} else if len(Tabs.List) > 1 { } else if len(Tabs.List) > 1 {
Tabs.RemoveTab(h.splitID) Tabs.RemoveTab(h.splitID)
@ -1724,23 +1907,29 @@ func (h *BufPane) ForceQuit() bool {
return true return true
} }
// closePrompt displays a prompt to save the buffer before closing it to proceed
// with a different action or command
func (h *BufPane) closePrompt(action string, callback func()) {
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
if !canceled && !yes {
callback()
} else if !canceled && yes {
h.SaveCB(action, callback)
}
})
}
// Quit this will close the current tab or view that is open // Quit this will close the current tab or view that is open
func (h *BufPane) Quit() bool { func (h *BufPane) Quit() bool {
if h.Buf.Modified() { if h.Buf.Modified() && !h.Buf.Shared() {
if config.GlobalSettings["autosave"].(float64) > 0 { if config.GlobalSettings["autosave"].(float64) > 0 && h.Buf.Path != "" {
// autosave on means we automatically save when quitting // autosave on means we automatically save when quitting
h.SaveCB("Quit", func() { h.SaveCB("Quit", func() {
h.ForceQuit() h.ForceQuit()
}) })
} else { } else {
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) { h.closePrompt("Quit", func() {
if !canceled && !yes { h.ForceQuit()
h.ForceQuit()
} else if !canceled && yes {
h.SaveCB("Quit", func() {
h.ForceQuit()
})
}
}) })
} }
} else { } else {
@ -1793,27 +1982,38 @@ func (h *BufPane) AddTab() bool {
// PreviousTab switches to the previous tab in the tab list // PreviousTab switches to the previous tab in the tab list
func (h *BufPane) PreviousTab() bool { func (h *BufPane) PreviousTab() bool {
tabsLen := len(Tabs.List) if Tabs.Active() == 0 {
if tabsLen == 1 {
return false return false
} }
Tabs.SetActive(Tabs.Active() - 1)
a := Tabs.Active() + tabsLen
Tabs.SetActive((a - 1) % tabsLen)
return true return true
} }
// NextTab switches to the next tab in the tab list // NextTab switches to the next tab in the tab list
func (h *BufPane) NextTab() bool { func (h *BufPane) NextTab() bool {
tabsLen := len(Tabs.List) if Tabs.Active() == len(Tabs.List)-1 {
if tabsLen == 1 {
return false return false
} }
Tabs.SetActive(Tabs.Active() + 1)
return true
}
a := Tabs.Active() // FirstTab switches to the first tab in the tab list
Tabs.SetActive((a + 1) % tabsLen) func (h *BufPane) FirstTab() bool {
if Tabs.Active() == 0 {
return false
}
Tabs.SetActive(0)
return true
}
// LastTab switches to the last tab in the tab list
func (h *BufPane) LastTab() bool {
lastTabIndex := len(Tabs.List) - 1
if Tabs.Active() == lastTabIndex {
return false
}
Tabs.SetActive(lastTabIndex)
return true return true
} }
@ -1848,36 +2048,38 @@ func (h *BufPane) Unsplit() bool {
// NextSplit changes the view to the next split // NextSplit changes the view to the next split
func (h *BufPane) NextSplit() bool { func (h *BufPane) NextSplit() bool {
if len(h.tab.Panes) == 1 { if h.tab.active == len(h.tab.Panes)-1 {
return false return false
} }
h.tab.SetActive(h.tab.active + 1)
a := h.tab.active
if a < len(h.tab.Panes)-1 {
a++
} else {
a = 0
}
h.tab.SetActive(a)
return true return true
} }
// PreviousSplit changes the view to the previous split // PreviousSplit changes the view to the previous split
func (h *BufPane) PreviousSplit() bool { func (h *BufPane) PreviousSplit() bool {
if len(h.tab.Panes) == 1 { if h.tab.active == 0 {
return false return false
} }
h.tab.SetActive(h.tab.active - 1)
return true
}
a := h.tab.active // FirstSplit changes the view to the first split
if a > 0 { func (h *BufPane) FirstSplit() bool {
a-- if h.tab.active == 0 {
} else { return false
a = len(h.tab.Panes) - 1
} }
h.tab.SetActive(a) h.tab.SetActive(0)
return true
}
// LastSplit changes the view to the last split
func (h *BufPane) LastSplit() bool {
lastPaneIdx := len(h.tab.Panes) - 1
if h.tab.active == lastPaneIdx {
return false
}
h.tab.SetActive(lastPaneIdx)
return true return true
} }
@ -1955,38 +2157,31 @@ func (h *BufPane) SpawnMultiCursor() bool {
return true return true
} }
// SpawnCursorAtLoc spawns a new cursor at a location and merges the cursors
func (h *BufPane) SpawnCursorAtLoc(loc buffer.Loc) *buffer.Cursor {
c := buffer.NewCursor(h.Buf, loc)
h.Buf.AddCursor(c)
h.Buf.MergeCursors()
return c
}
// SpawnMultiCursorUpN is not an action // SpawnMultiCursorUpN is not an action
func (h *BufPane) SpawnMultiCursorUpN(n int) bool { func (h *BufPane) SpawnMultiCursorUpN(n int) bool {
lastC := h.Buf.GetCursor(h.Buf.NumCursors() - 1) lastC := h.Buf.GetCursor(h.Buf.NumCursors() - 1)
var c *buffer.Cursor if n > 0 && lastC.Y == 0 {
if !h.Buf.Settings["softwrap"].(bool) { return false
if n > 0 && lastC.Y == 0 {
return false
}
if n < 0 && lastC.Y+1 == h.Buf.LinesNum() {
return false
}
h.Buf.DeselectCursors()
c = buffer.NewCursor(h.Buf, buffer.Loc{lastC.X, lastC.Y - n})
c.LastVisualX = lastC.LastVisualX
c.X = c.GetCharPosInLine(h.Buf.LineBytes(c.Y), c.LastVisualX)
c.Relocate()
} else {
vloc := h.VLocFromLoc(lastC.Loc)
sloc := h.Scroll(vloc.SLoc, -n)
if sloc == vloc.SLoc {
return false
}
h.Buf.DeselectCursors()
vloc.SLoc = sloc
vloc.VisualX = lastC.LastVisualX
c = buffer.NewCursor(h.Buf, h.LocFromVLoc(vloc))
c.LastVisualX = lastC.LastVisualX
} }
if n < 0 && lastC.Y+1 == h.Buf.LinesNum() {
return false
}
h.Buf.DeselectCursors()
c := buffer.NewCursor(h.Buf, buffer.Loc{lastC.X, lastC.Y - n})
c.LastVisualX = lastC.LastVisualX
c.LastWrappedVisualX = lastC.LastWrappedVisualX
c.X = c.GetCharPosInLine(h.Buf.LineBytes(c.Y), c.LastVisualX)
c.Relocate()
h.Buf.AddCursor(c) h.Buf.AddCursor(c)
h.Buf.SetCurCursor(h.Buf.NumCursors() - 1) h.Buf.SetCurCursor(h.Buf.NumCursors() - 1)
@ -2068,14 +2263,16 @@ func (h *BufPane) MouseMultiCursor(e *tcell.EventMouse) bool {
return true return true
} }
// SkipMultiCursor moves the current multiple cursor to the next available position func (h *BufPane) skipMultiCursor(forward bool) bool {
func (h *BufPane) SkipMultiCursor() bool {
lastC := h.Buf.GetCursor(h.Buf.NumCursors() - 1) lastC := h.Buf.GetCursor(h.Buf.NumCursors() - 1)
if !lastC.HasSelection() { if !lastC.HasSelection() {
return false return false
} }
sel := lastC.GetSelection() sel := lastC.GetSelection()
searchStart := lastC.CurSelection[1] searchStart := lastC.CurSelection[1]
if !forward {
searchStart = lastC.CurSelection[0]
}
search := string(sel) search := string(sel)
search = regexp.QuoteMeta(search) search = regexp.QuoteMeta(search)
@ -2083,7 +2280,7 @@ func (h *BufPane) SkipMultiCursor() bool {
search = "\\b" + search + "\\b" search = "\\b" + search + "\\b"
} }
match, found, err := h.Buf.FindNext(search, h.Buf.Start(), h.Buf.End(), searchStart, true, true) match, found, err := h.Buf.FindNext(search, h.Buf.Start(), h.Buf.End(), searchStart, forward, true)
if err != nil { if err != nil {
InfoBar.Error(err) InfoBar.Error(err)
} }
@ -2103,6 +2300,16 @@ func (h *BufPane) SkipMultiCursor() bool {
return true return true
} }
// SkipMultiCursor moves the current multiple cursor to the next available position
func (h *BufPane) SkipMultiCursor() bool {
return h.skipMultiCursor(true)
}
// SkipMultiCursorBack moves the current multiple cursor to the previous available position
func (h *BufPane) SkipMultiCursorBack() bool {
return h.skipMultiCursor(false)
}
// RemoveMultiCursor removes the latest multiple cursor // RemoveMultiCursor removes the latest multiple cursor
func (h *BufPane) RemoveMultiCursor() bool { func (h *BufPane) RemoveMultiCursor() bool {
if h.Buf.NumCursors() > 1 { if h.Buf.NumCursors() > 1 {

View File

@ -1,4 +1,4 @@
// +build plan9 nacl windows //go:build plan9 || nacl || windows
package action package action

View File

@ -1,4 +1,4 @@
// +build linux darwin dragonfly solaris openbsd netbsd freebsd //go:build linux || darwin || dragonfly || solaris || openbsd || netbsd || freebsd
package action package action

View File

@ -4,17 +4,18 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"unicode" "unicode"
"github.com/zyedidia/json5" "github.com/micro-editor/json5"
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/tcell/v2" "github.com/zyedidia/micro/v2/internal/util"
) )
var Binder = map[string]func(e Event, action string){ var Binder = map[string]func(e Event, action string){
@ -23,9 +24,13 @@ var Binder = map[string]func(e Event, action string){
"terminal": TermMapEvent, "terminal": TermMapEvent,
} }
func writeFile(name string, txt []byte) error {
return util.SafeWrite(name, txt, false)
}
func createBindingsIfNotExist(fname string) { func createBindingsIfNotExist(fname string) {
if _, e := os.Stat(fname); os.IsNotExist(e) { if _, e := os.Stat(fname); errors.Is(e, fs.ErrNotExist) {
ioutil.WriteFile(fname, []byte("{}"), 0644) writeFile(fname, []byte("{}"))
} }
} }
@ -37,7 +42,7 @@ func InitBindings() {
createBindingsIfNotExist(filename) createBindingsIfNotExist(filename)
if _, e := os.Stat(filename); e == nil { if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename) input, err := os.ReadFile(filename)
if err != nil { if err != nil {
screen.TermMessage("Error reading bindings.json file: " + err.Error()) screen.TermMessage("Error reading bindings.json file: " + err.Error())
return return
@ -89,7 +94,7 @@ func BindKey(k, v string, bind func(e Event, a string)) {
} }
if strings.HasPrefix(k, "\x1b") { if strings.HasPrefix(k, "\x1b") {
screen.Screen.RegisterRawSeq(k) screen.RegisterRawSeq(k)
} }
bind(event, v) bind(event, v)
@ -265,7 +270,7 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) {
filename := filepath.Join(config.ConfigDir, "bindings.json") filename := filepath.Join(config.ConfigDir, "bindings.json")
createBindingsIfNotExist(filename) createBindingsIfNotExist(filename)
if _, e = os.Stat(filename); e == nil { if _, e = os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename) input, err := os.ReadFile(filename)
if err != nil { if err != nil {
return false, errors.New("Error reading bindings.json file: " + err.Error()) return false, errors.New("Error reading bindings.json file: " + err.Error())
} }
@ -304,7 +309,8 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) {
BindKey(k, v, Binder["buffer"]) BindKey(k, v, Binder["buffer"])
txt, _ := json.MarshalIndent(parsed, "", " ") txt, _ := json.MarshalIndent(parsed, "", " ")
return true, ioutil.WriteFile(filename, append(txt, '\n'), 0644) txt = append(txt, '\n')
return true, writeFile(filename, txt)
} }
return false, e return false, e
} }
@ -317,7 +323,7 @@ func UnbindKey(k string) error {
filename := filepath.Join(config.ConfigDir, "bindings.json") filename := filepath.Join(config.ConfigDir, "bindings.json")
createBindingsIfNotExist(filename) createBindingsIfNotExist(filename)
if _, e = os.Stat(filename); e == nil { if _, e = os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename) input, err := os.ReadFile(filename)
if err != nil { if err != nil {
return errors.New("Error reading bindings.json file: " + err.Error()) return errors.New("Error reading bindings.json file: " + err.Error())
} }
@ -342,7 +348,7 @@ func UnbindKey(k string) error {
} }
if strings.HasPrefix(k, "\x1b") { if strings.HasPrefix(k, "\x1b") {
screen.Screen.UnregisterRawSeq(k) screen.UnregisterRawSeq(k)
} }
defaults := DefaultBindings("buffer") defaults := DefaultBindings("buffer")
@ -354,7 +360,8 @@ func UnbindKey(k string) error {
} }
txt, _ := json.MarshalIndent(parsed, "", " ") txt, _ := json.MarshalIndent(parsed, "", " ")
return ioutil.WriteFile(filename, append(txt, '\n'), 0644) txt = append(txt, '\n')
return writeFile(filename, txt)
} }
return e return e
} }

View File

@ -6,6 +6,7 @@ import (
luar "layeh.com/gopher-luar" luar "layeh.com/gopher-luar"
"github.com/micro-editor/tcell/v2"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
@ -13,7 +14,6 @@ import (
ulua "github.com/zyedidia/micro/v2/internal/lua" ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2"
) )
type BufAction interface{} type BufAction interface{}
@ -100,9 +100,7 @@ func BufMapEvent(k Event, action string) {
break break
} }
// TODO: fix problem when complex bindings have these idx := util.IndexAnyUnquoted(action, "&|,")
// characters (escape them?)
idx := strings.IndexAny(action, "&|,")
a := action a := action
if idx >= 0 { if idx >= 0 {
a = action[:idx] a = action[:idx]
@ -226,26 +224,21 @@ type BufPane struct {
// (possibly multiple) buttons were pressed previously. // (possibly multiple) buttons were pressed previously.
mousePressed map[MouseEvent]bool mousePressed map[MouseEvent]bool
// We need to keep track of insert key press toggle
isOverwriteMode bool
// This stores when the last click was // This stores when the last click was
// This is useful for detecting double and triple clicks // This is useful for detecting double and triple clicks
lastClickTime time.Time lastClickTime time.Time
lastLoc buffer.Loc lastLoc buffer.Loc
// lastCutTime stores when the last ctrl+k was issued. // freshClip returns true if one or more lines have been cut to the clipboard
// It is used for clearing the clipboard to replace it with fresh cut lines. // and have never been pasted yet.
lastCutTime time.Time
// freshClip returns true if the clipboard has never been pasted.
freshClip bool freshClip bool
// Was the last mouse event actually a double click? // Was the last mouse event actually a double click?
// Useful for detecting triple clicks -- if a double click is detected // Useful for detecting triple clicks -- if a double click is detected
// but the last mouse event was actually a double click, it's a triple click // but the last mouse event was actually a double click, it's a triple click
doubleClick bool DoubleClick bool
// Same here, just to keep track for mouse move events // Same here, just to keep track for mouse move events
tripleClick bool TripleClick bool
// Should the current multiple cursor selection search based on word or // Should the current multiple cursor selection search based on word or
// based on selection (false for selection, true for word) // based on selection (false for selection, true for word)
@ -363,9 +356,6 @@ func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
// Set mouseReleased to true because we assume the mouse is not being // Set mouseReleased to true because we assume the mouse is not being
// pressed when the editor is opened // pressed when the editor is opened
h.resetMouse() h.resetMouse()
// Set isOverwriteMode to false, because we assume we are in the default
// mode when editor is opened
h.isOverwriteMode = false
h.lastClickTime = time.Time{} h.lastClickTime = time.Time{}
} }
@ -644,7 +634,7 @@ func (h *BufPane) DoRuneInsert(r rune) {
c.ResetSelection() c.ResetSelection()
} }
if h.isOverwriteMode { if h.Buf.OverwriteMode {
next := c.Loc next := c.Loc
next.X++ next.X++
h.Buf.Replace(c.Loc, next, string(r)) h.Buf.Replace(c.Loc, next, string(r))
@ -662,20 +652,28 @@ func (h *BufPane) DoRuneInsert(r rune) {
// VSplitIndex opens the given buffer in a vertical split on the given side. // VSplitIndex opens the given buffer in a vertical split on the given side.
func (h *BufPane) VSplitIndex(buf *buffer.Buffer, right bool) *BufPane { func (h *BufPane) VSplitIndex(buf *buffer.Buffer, right bool) *BufPane {
e := NewBufPaneFromBuf(buf, h.tab) e := NewBufPaneFromBuf(buf, h.tab)
e.splitID = MainTab().GetNode(h.splitID).VSplit(right) e.splitID = h.tab.GetNode(h.splitID).VSplit(right)
MainTab().Panes = append(MainTab().Panes, e) currentPaneIdx := h.tab.GetPane(h.splitID)
MainTab().Resize() if right {
MainTab().SetActive(len(MainTab().Panes) - 1) currentPaneIdx++
}
h.tab.AddPane(e, currentPaneIdx)
h.tab.Resize()
h.tab.SetActive(currentPaneIdx)
return e return e
} }
// HSplitIndex opens the given buffer in a horizontal split on the given side. // HSplitIndex opens the given buffer in a horizontal split on the given side.
func (h *BufPane) HSplitIndex(buf *buffer.Buffer, bottom bool) *BufPane { func (h *BufPane) HSplitIndex(buf *buffer.Buffer, bottom bool) *BufPane {
e := NewBufPaneFromBuf(buf, h.tab) e := NewBufPaneFromBuf(buf, h.tab)
e.splitID = MainTab().GetNode(h.splitID).HSplit(bottom) e.splitID = h.tab.GetNode(h.splitID).HSplit(bottom)
MainTab().Panes = append(MainTab().Panes, e) currentPaneIdx := h.tab.GetPane(h.splitID)
MainTab().Resize() if bottom {
MainTab().SetActive(len(MainTab().Panes) - 1) currentPaneIdx++
}
h.tab.AddPane(e, currentPaneIdx)
h.tab.Resize()
h.tab.SetActive(currentPaneIdx)
return e return e
} }
@ -733,6 +731,9 @@ var BufKeyActions = map[string]BufKeyAction{
"CursorRight": (*BufPane).CursorRight, "CursorRight": (*BufPane).CursorRight,
"CursorStart": (*BufPane).CursorStart, "CursorStart": (*BufPane).CursorStart,
"CursorEnd": (*BufPane).CursorEnd, "CursorEnd": (*BufPane).CursorEnd,
"CursorToViewTop": (*BufPane).CursorToViewTop,
"CursorToViewCenter": (*BufPane).CursorToViewCenter,
"CursorToViewBottom": (*BufPane).CursorToViewBottom,
"SelectToStart": (*BufPane).SelectToStart, "SelectToStart": (*BufPane).SelectToStart,
"SelectToEnd": (*BufPane).SelectToEnd, "SelectToEnd": (*BufPane).SelectToEnd,
"SelectUp": (*BufPane).SelectUp, "SelectUp": (*BufPane).SelectUp,
@ -780,6 +781,7 @@ var BufKeyActions = map[string]BufKeyAction{
"CopyLine": (*BufPane).CopyLine, "CopyLine": (*BufPane).CopyLine,
"Cut": (*BufPane).Cut, "Cut": (*BufPane).Cut,
"CutLine": (*BufPane).CutLine, "CutLine": (*BufPane).CutLine,
"Duplicate": (*BufPane).Duplicate,
"DuplicateLine": (*BufPane).DuplicateLine, "DuplicateLine": (*BufPane).DuplicateLine,
"DeleteLine": (*BufPane).DeleteLine, "DeleteLine": (*BufPane).DeleteLine,
"MoveLinesUp": (*BufPane).MoveLinesUp, "MoveLinesUp": (*BufPane).MoveLinesUp,
@ -824,8 +826,12 @@ var BufKeyActions = map[string]BufKeyAction{
"AddTab": (*BufPane).AddTab, "AddTab": (*BufPane).AddTab,
"PreviousTab": (*BufPane).PreviousTab, "PreviousTab": (*BufPane).PreviousTab,
"NextTab": (*BufPane).NextTab, "NextTab": (*BufPane).NextTab,
"FirstTab": (*BufPane).FirstTab,
"LastTab": (*BufPane).LastTab,
"NextSplit": (*BufPane).NextSplit, "NextSplit": (*BufPane).NextSplit,
"PreviousSplit": (*BufPane).PreviousSplit, "PreviousSplit": (*BufPane).PreviousSplit,
"FirstSplit": (*BufPane).FirstSplit,
"LastSplit": (*BufPane).LastSplit,
"Unsplit": (*BufPane).Unsplit, "Unsplit": (*BufPane).Unsplit,
"VSplit": (*BufPane).VSplitAction, "VSplit": (*BufPane).VSplitAction,
"HSplit": (*BufPane).HSplitAction, "HSplit": (*BufPane).HSplitAction,
@ -841,6 +847,7 @@ var BufKeyActions = map[string]BufKeyAction{
"RemoveMultiCursor": (*BufPane).RemoveMultiCursor, "RemoveMultiCursor": (*BufPane).RemoveMultiCursor,
"RemoveAllMultiCursors": (*BufPane).RemoveAllMultiCursors, "RemoveAllMultiCursors": (*BufPane).RemoveAllMultiCursors,
"SkipMultiCursor": (*BufPane).SkipMultiCursor, "SkipMultiCursor": (*BufPane).SkipMultiCursor,
"SkipMultiCursorBack": (*BufPane).SkipMultiCursorBack,
"JumpToMatchingBrace": (*BufPane).JumpToMatchingBrace, "JumpToMatchingBrace": (*BufPane).JumpToMatchingBrace,
"JumpLine": (*BufPane).JumpLine, "JumpLine": (*BufPane).JumpLine,
"Deselect": (*BufPane).Deselect, "Deselect": (*BufPane).Deselect,
@ -907,6 +914,7 @@ var MultiActions = map[string]bool{
"Copy": true, "Copy": true,
"Cut": true, "Cut": true,
"CutLine": true, "CutLine": true,
"Duplicate": true,
"DuplicateLine": true, "DuplicateLine": true,
"DeleteLine": true, "DeleteLine": true,
"MoveLinesUp": true, "MoveLinesUp": true,

View File

@ -139,23 +139,25 @@ func (h *BufPane) TextFilterCmd(args []string) {
InfoBar.Error("usage: textfilter arguments") InfoBar.Error("usage: textfilter arguments")
return return
} }
sel := h.Cursor.GetSelection() for _, c := range h.Buf.GetCursors() {
if len(sel) == 0 { sel := c.GetSelection()
h.Cursor.SelectWord() if len(sel) == 0 {
sel = h.Cursor.GetSelection() c.SelectWord()
sel = c.GetSelection()
}
var bout, berr bytes.Buffer
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = strings.NewReader(string(sel))
cmd.Stderr = &berr
cmd.Stdout = &bout
err := cmd.Run()
if err != nil {
InfoBar.Error(err.Error() + " " + berr.String())
return
}
c.DeleteSelection()
h.Buf.Insert(c.Loc, bout.String())
} }
var bout, berr bytes.Buffer
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = strings.NewReader(string(sel))
cmd.Stderr = &berr
cmd.Stdout = &bout
err := cmd.Run()
if err != nil {
InfoBar.Error(err.Error() + " " + berr.String())
return
}
h.Cursor.DeleteSelection()
h.Buf.Insert(h.Cursor.Loc, bout.String())
} }
// TabMoveCmd moves the current tab to a given index (starts at 1). The // TabMoveCmd moves the current tab to a given index (starts at 1). The
@ -203,6 +205,7 @@ func (h *BufPane) TabMoveCmd(args []string) {
Tabs.List = append(Tabs.List, nil) Tabs.List = append(Tabs.List, nil)
copy(Tabs.List[idxTo+1:], Tabs.List[idxTo:]) copy(Tabs.List[idxTo+1:], Tabs.List[idxTo:])
Tabs.List[idxTo] = activeTab Tabs.List[idxTo] = activeTab
Tabs.Resize()
Tabs.UpdateNames() Tabs.UpdateNames()
Tabs.SetActive(idxTo) Tabs.SetActive(idxTo)
// InfoBar.Message(fmt.Sprintf("Moved tab from slot %d to %d", idxFrom+1, idxTo+1)) // InfoBar.Message(fmt.Sprintf("Moved tab from slot %d to %d", idxFrom+1, idxTo+1))
@ -305,15 +308,8 @@ func (h *BufPane) OpenCmd(args []string) {
} }
h.OpenBuffer(b) h.OpenBuffer(b)
} }
if h.Buf.Modified() { if h.Buf.Modified() && !h.Buf.Shared() {
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) { h.closePrompt("Save", open)
if !canceled && !yes {
open()
} else if !canceled && yes {
h.Save()
open()
}
})
} else { } else {
open() open()
} }
@ -428,7 +424,7 @@ func (h *BufPane) ReopenCmd(args []string) {
} }
} }
func (h *BufPane) openHelp(page string) error { func (h *BufPane) openHelp(page string, hsplit bool, forceSplit bool) error {
if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil { if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil {
return errors.New(fmt.Sprintf("Unable to load help text for %s: %v", page, err)) return errors.New(fmt.Sprintf("Unable to load help text for %s: %v", page, err))
} else { } else {
@ -437,33 +433,74 @@ func (h *BufPane) openHelp(page string) error {
helpBuffer.SetOptionNative("hltaberrors", false) helpBuffer.SetOptionNative("hltaberrors", false)
helpBuffer.SetOptionNative("hltrailingws", false) helpBuffer.SetOptionNative("hltrailingws", false)
if h.Buf.Type == buffer.BTHelp { if h.Buf.Type == buffer.BTHelp && !forceSplit {
h.OpenBuffer(helpBuffer) h.OpenBuffer(helpBuffer)
} else { } else if hsplit {
h.HSplitBuf(helpBuffer) h.HSplitBuf(helpBuffer)
} else {
h.VSplitBuf(helpBuffer)
} }
} }
return nil return nil
} }
// HelpCmd tries to open the given help page in a horizontal split // HelpCmd tries to open the given help page according to the split type
// configured with the "helpsplit" option. It can be overriden by the optional
// arguments "-vpslit" or "-hsplit". In case more than one help page is given
// as argument then it opens all of them with the defined split type.
func (h *BufPane) HelpCmd(args []string) { func (h *BufPane) HelpCmd(args []string) {
hsplit := config.GlobalSettings["helpsplit"] == "hsplit"
if len(args) < 1 { if len(args) < 1 {
// Open the default help if the user just typed "> help" // Open the default help if the user just typed "> help"
h.openHelp("help") h.openHelp("help", hsplit, false)
} else { } else {
if config.FindRuntimeFile(config.RTHelp, args[0]) != nil { var topics []string
err := h.openHelp(args[0]) forceSplit := false
if err != nil { const errSplit = "hsplit and vsplit are not allowed at the same time"
InfoBar.Error(err) for _, arg := range args {
switch arg {
case "-vsplit":
if forceSplit {
InfoBar.Error(errSplit)
return
}
hsplit = false
forceSplit = true
case "-hsplit":
if forceSplit {
InfoBar.Error(errSplit)
return
}
hsplit = true
forceSplit = true
default:
topics = append(topics, arg)
}
}
if len(topics) < 1 {
// Do the same as without arg
h.openHelp("help", hsplit, forceSplit)
return
}
if len(topics) > 1 {
forceSplit = true
}
for _, topic := range topics {
if config.FindRuntimeFile(config.RTHelp, topic) != nil {
err := h.openHelp(topic, hsplit, forceSplit)
if err != nil {
InfoBar.Error(err)
}
} else {
InfoBar.Error("Sorry, no help for ", topic)
} }
} else {
InfoBar.Error("Sorry, no help for ", args[0])
} }
} }
} }
// VSplitCmd opens a vertical split with file given in the first argument // VSplitCmd opens one or more vertical splits with the files given as arguments
// If no file is given, it opens an empty buffer in a new split // If no file is given, it opens an empty buffer in a new split
func (h *BufPane) VSplitCmd(args []string) { func (h *BufPane) VSplitCmd(args []string) {
if len(args) == 0 { if len(args) == 0 {
@ -472,16 +509,18 @@ func (h *BufPane) VSplitCmd(args []string) {
return return
} }
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault) for _, a := range args {
if err != nil { buf, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
InfoBar.Error(err) if err != nil {
return InfoBar.Error(err)
} return
}
h.VSplitBuf(buf) h.VSplitBuf(buf)
}
} }
// HSplitCmd opens a horizontal split with file given in the first argument // HSplitCmd opens one or more horizontal splits with the files given as arguments
// If no file is given, it opens an empty buffer in a new split // If no file is given, it opens an empty buffer in a new split
func (h *BufPane) HSplitCmd(args []string) { func (h *BufPane) HSplitCmd(args []string) {
if len(args) == 0 { if len(args) == 0 {
@ -490,13 +529,15 @@ func (h *BufPane) HSplitCmd(args []string) {
return return
} }
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault) for _, a := range args {
if err != nil { buf, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
InfoBar.Error(err) if err != nil {
return InfoBar.Error(err)
} return
}
h.HSplitBuf(buf) h.HSplitBuf(buf)
}
} }
// EvalCmd evaluates a lua expression // EvalCmd evaluates a lua expression
@ -504,7 +545,8 @@ func (h *BufPane) EvalCmd(args []string) {
InfoBar.Error("Eval unsupported") InfoBar.Error("Eval unsupported")
} }
// NewTabCmd opens the given file in a new tab // NewTabCmd opens one or more tabs with the files given as arguments
// If no file is given, it opens an empty buffer in a new tab
func (h *BufPane) NewTabCmd(args []string) { func (h *BufPane) NewTabCmd(args []string) {
width, height := screen.Screen.Size() width, height := screen.Screen.Size()
iOffset := config.GetInfoBarOffset() iOffset := config.GetInfoBarOffset()
@ -609,7 +651,16 @@ func SetGlobalOptionNative(option string, nativeValue interface{}) error {
delete(b.LocalSettings, option) delete(b.LocalSettings, option)
} }
return config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json")) err := config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
if err != nil {
if errors.Is(err, util.ErrOverwrite) {
screen.TermMessage(err)
err = errors.Unwrap(err)
}
return err
}
return nil
} }
func SetGlobalOption(option, value string) error { func SetGlobalOption(option, value string) error {
@ -734,7 +785,11 @@ func (h *BufPane) BindCmd(args []string) {
_, err := TryBindKey(parseKeyArg(args[0]), args[1], true) _, err := TryBindKey(parseKeyArg(args[0]), args[1], true)
if err != nil { if err != nil {
InfoBar.Error(err) if errors.Is(err, util.ErrOverwrite) {
screen.TermMessage(err)
} else {
InfoBar.Error(err)
}
} }
} }
@ -747,7 +802,11 @@ func (h *BufPane) UnbindCmd(args []string) {
err := UnbindKey(parseKeyArg(args[0])) err := UnbindKey(parseKeyArg(args[0]))
if err != nil { if err != nil {
InfoBar.Error(err) if errors.Is(err, util.ErrOverwrite) {
screen.TermMessage(err)
} else {
InfoBar.Error(err)
}
} }
} }
@ -841,7 +900,7 @@ func (h *BufPane) SaveCmd(args []string) {
if len(args) == 0 { if len(args) == 0 {
h.Save() h.Save()
} else { } else {
h.Buf.SaveAs(args[0]) h.saveBufToFile(args[0], "SaveAs", nil)
} }
} }
@ -902,10 +961,12 @@ func (h *BufPane) ReplaceCmd(args []string) {
nreplaced := 0 nreplaced := 0
start := h.Buf.Start() start := h.Buf.Start()
end := h.Buf.End() end := h.Buf.End()
searchLoc := h.Cursor.Loc
selection := h.Cursor.HasSelection() selection := h.Cursor.HasSelection()
if selection { if selection {
start = h.Cursor.CurSelection[0] start = h.Cursor.CurSelection[0]
end = h.Cursor.CurSelection[1] end = h.Cursor.CurSelection[1]
searchLoc = start // otherwise me might start at the end
} }
if all { if all {
nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace, !noRegex) nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace, !noRegex)
@ -914,7 +975,7 @@ func (h *BufPane) ReplaceCmd(args []string) {
return l.GreaterEqual(start) && l.LessEqual(end) return l.GreaterEqual(start) && l.LessEqual(end)
} }
searchLoc := h.Cursor.Loc lastMatchEnd := buffer.Loc{-1, -1}
var doReplacement func() var doReplacement func()
doReplacement = func() { doReplacement = func() {
locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, true) locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, true)
@ -929,6 +990,18 @@ func (h *BufPane) ReplaceCmd(args []string) {
return return
} }
if lastMatchEnd == locs[1] {
// skip empty match right after previous match
if searchLoc == end {
searchLoc = start
lastMatchEnd = buffer.Loc{-1, -1}
} else {
searchLoc = searchLoc.Move(1, h.Buf)
}
doReplacement()
return
}
h.Cursor.SetSelectionStart(locs[0]) h.Cursor.SetSelectionStart(locs[0])
h.Cursor.SetSelectionEnd(locs[1]) h.Cursor.SetSelectionEnd(locs[1])
h.GotoLoc(locs[0]) h.GotoLoc(locs[0])
@ -954,6 +1027,7 @@ func (h *BufPane) ReplaceCmd(args []string) {
h.Buf.RelocateCursors() h.Buf.RelocateCursors()
return return
} }
lastMatchEnd = searchLoc
doReplacement() doReplacement()
}) })
} }
@ -985,10 +1059,42 @@ func (h *BufPane) ReplaceAllCmd(args []string) {
h.ReplaceCmd(append(args, "-a")) h.ReplaceCmd(append(args, "-a"))
} }
func (h *BufPane) openTerm(args []string, newtab bool) {
t := new(shell.Terminal)
err := t.Start(args, false, true, nil, nil)
if err != nil {
InfoBar.Error(err)
return
}
pane := 0
id := h.ID()
if newtab {
h.AddTab()
id = MainTab().Panes[pane].ID()
} else {
for i, p := range MainTab().Panes {
if p.IsActive() {
pane = i
id = p.ID()
p.Close()
break
}
}
}
v := h.GetView()
tp, err := NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
if err != nil {
InfoBar.Error(err)
return
}
MainTab().Panes[pane] = tp
MainTab().SetActive(pane)
}
// TermCmd opens a terminal in the current view // TermCmd opens a terminal in the current view
func (h *BufPane) TermCmd(args []string) { func (h *BufPane) TermCmd(args []string) {
ps := h.tab.Panes
if !TermEmuSupported { if !TermEmuSupported {
InfoBar.Error("Terminal emulator not supported on this system") InfoBar.Error("Terminal emulator not supported on this system")
return return
@ -1003,56 +1109,19 @@ func (h *BufPane) TermCmd(args []string) {
args = []string{sh} args = []string{sh}
} }
term := func(i int, newtab bool) {
t := new(shell.Terminal)
err := t.Start(args, false, true, nil, nil)
if err != nil {
InfoBar.Error(err)
return
}
id := h.ID()
if newtab {
h.AddTab()
i = 0
id = MainTab().Panes[0].ID()
} else {
MainTab().Panes[i].Close()
}
v := h.GetView()
tp, err := NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
if err != nil {
InfoBar.Error(err)
return
}
MainTab().Panes[i] = tp
MainTab().SetActive(i)
}
// If there is only one open file we make a new tab instead of overwriting it // If there is only one open file we make a new tab instead of overwriting it
newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1 newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1
if newtab { if newtab {
term(0, true) h.openTerm(args, true)
return return
} }
for i, p := range ps { if h.Buf.Modified() && !h.Buf.Shared() {
if p.ID() == h.ID() { h.closePrompt("Save", func() {
if h.Buf.Modified() { h.openTerm(args, false)
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) { })
if !canceled && !yes { } else {
term(i, false) h.openTerm(args, false)
} else if !canceled && yes {
h.Save()
term(i, false)
}
})
} else {
term(i, false)
}
}
} }
} }

View File

@ -3,7 +3,7 @@ package action
var termdefaults = map[string]string{ var termdefaults = map[string]string{
"<Ctrl-q><Ctrl-q>": "Exit", "<Ctrl-q><Ctrl-q>": "Exit",
"<Ctrl-e><Ctrl-e>": "CommandMode", "<Ctrl-e><Ctrl-e>": "CommandMode",
"<Ctrl-w><Ctrl-w>": "NextSplit", "<Ctrl-w><Ctrl-w>": "NextSplit|FirstSplit",
} }
// DefaultBindings returns a map containing micro's default keybindings // DefaultBindings returns a map containing micro's default keybindings

View File

@ -45,23 +45,23 @@ var bufdefaults = map[string]string{
"Alt-]": "DiffNext|CursorEnd", "Alt-]": "DiffNext|CursorEnd",
"Ctrl-z": "Undo", "Ctrl-z": "Undo",
"Ctrl-y": "Redo", "Ctrl-y": "Redo",
"Ctrl-c": "CopyLine|Copy", "Ctrl-c": "Copy|CopyLine",
"Ctrl-x": "Cut", "Ctrl-x": "Cut|CutLine",
"Ctrl-k": "CutLine", "Ctrl-k": "CutLine",
"Ctrl-d": "DuplicateLine", "Ctrl-d": "Duplicate|DuplicateLine",
"Ctrl-v": "Paste", "Ctrl-v": "Paste",
"Ctrl-a": "SelectAll", "Ctrl-a": "SelectAll",
"Ctrl-t": "AddTab", "Ctrl-t": "AddTab",
"Alt-,": "PreviousTab", "Alt-,": "PreviousTab|LastTab",
"Alt-.": "NextTab", "Alt-.": "NextTab|FirstTab",
"Home": "StartOfTextToggle", "Home": "StartOfTextToggle",
"End": "EndOfLine", "End": "EndOfLine",
"CtrlHome": "CursorStart", "CtrlHome": "CursorStart",
"CtrlEnd": "CursorEnd", "CtrlEnd": "CursorEnd",
"PageUp": "CursorPageUp", "PageUp": "CursorPageUp",
"PageDown": "CursorPageDown", "PageDown": "CursorPageDown",
"CtrlPageUp": "PreviousTab", "CtrlPageUp": "PreviousTab|LastTab",
"CtrlPageDown": "NextTab", "CtrlPageDown": "NextTab|FirstTab",
"ShiftPageUp": "SelectPageUp", "ShiftPageUp": "SelectPageUp",
"ShiftPageDown": "SelectPageDown", "ShiftPageDown": "SelectPageDown",
"Ctrl-g": "ToggleHelp", "Ctrl-g": "ToggleHelp",
@ -72,7 +72,7 @@ var bufdefaults = map[string]string{
"Ctrl-b": "ShellMode", "Ctrl-b": "ShellMode",
"Ctrl-q": "Quit", "Ctrl-q": "Quit",
"Ctrl-e": "CommandMode", "Ctrl-e": "CommandMode",
"Ctrl-w": "NextSplit", "Ctrl-w": "NextSplit|FirstSplit",
"Ctrl-u": "ToggleMacro", "Ctrl-u": "ToggleMacro",
"Ctrl-j": "PlayMacro", "Ctrl-j": "PlayMacro",
"Insert": "ToggleOverwriteMode", "Insert": "ToggleOverwriteMode",
@ -146,8 +146,8 @@ var infodefaults = map[string]string{
"Backtab": "CycleAutocompleteBack", "Backtab": "CycleAutocompleteBack",
"Ctrl-z": "Undo", "Ctrl-z": "Undo",
"Ctrl-y": "Redo", "Ctrl-y": "Redo",
"Ctrl-c": "CopyLine|Copy", "Ctrl-c": "Copy|CopyLine",
"Ctrl-x": "Cut", "Ctrl-x": "Cut|CutLine",
"Ctrl-k": "CutLine", "Ctrl-k": "CutLine",
"Ctrl-v": "Paste", "Ctrl-v": "Paste",
"Home": "StartOfTextToggle", "Home": "StartOfTextToggle",

View File

@ -48,23 +48,23 @@ var bufdefaults = map[string]string{
"Alt-]": "DiffNext|CursorEnd", "Alt-]": "DiffNext|CursorEnd",
"Ctrl-z": "Undo", "Ctrl-z": "Undo",
"Ctrl-y": "Redo", "Ctrl-y": "Redo",
"Ctrl-c": "CopyLine|Copy", "Ctrl-c": "Copy|CopyLine",
"Ctrl-x": "Cut", "Ctrl-x": "Cut|CutLine",
"Ctrl-k": "CutLine", "Ctrl-k": "CutLine",
"Ctrl-d": "DuplicateLine", "Ctrl-d": "Duplicate|DuplicateLine",
"Ctrl-v": "Paste", "Ctrl-v": "Paste",
"Ctrl-a": "SelectAll", "Ctrl-a": "SelectAll",
"Ctrl-t": "AddTab", "Ctrl-t": "AddTab",
"Alt-,": "PreviousTab", "Alt-,": "PreviousTab|LastTab",
"Alt-.": "NextTab", "Alt-.": "NextTab|FirstTab",
"Home": "StartOfTextToggle", "Home": "StartOfTextToggle",
"End": "EndOfLine", "End": "EndOfLine",
"CtrlHome": "CursorStart", "CtrlHome": "CursorStart",
"CtrlEnd": "CursorEnd", "CtrlEnd": "CursorEnd",
"PageUp": "CursorPageUp", "PageUp": "CursorPageUp",
"PageDown": "CursorPageDown", "PageDown": "CursorPageDown",
"CtrlPageUp": "PreviousTab", "CtrlPageUp": "PreviousTab|LastTab",
"CtrlPageDown": "NextTab", "CtrlPageDown": "NextTab|FirstTab",
"ShiftPageUp": "SelectPageUp", "ShiftPageUp": "SelectPageUp",
"ShiftPageDown": "SelectPageDown", "ShiftPageDown": "SelectPageDown",
"Ctrl-g": "ToggleHelp", "Ctrl-g": "ToggleHelp",
@ -75,7 +75,7 @@ var bufdefaults = map[string]string{
"Ctrl-b": "ShellMode", "Ctrl-b": "ShellMode",
"Ctrl-q": "Quit", "Ctrl-q": "Quit",
"Ctrl-e": "CommandMode", "Ctrl-e": "CommandMode",
"Ctrl-w": "NextSplit", "Ctrl-w": "NextSplit|FirstSplit",
"Ctrl-u": "ToggleMacro", "Ctrl-u": "ToggleMacro",
"Ctrl-j": "PlayMacro", "Ctrl-j": "PlayMacro",
"Insert": "ToggleOverwriteMode", "Insert": "ToggleOverwriteMode",
@ -149,8 +149,8 @@ var infodefaults = map[string]string{
"Backtab": "CycleAutocompleteBack", "Backtab": "CycleAutocompleteBack",
"Ctrl-z": "Undo", "Ctrl-z": "Undo",
"Ctrl-y": "Redo", "Ctrl-y": "Redo",
"Ctrl-c": "CopyLine|Copy", "Ctrl-c": "Copy|CopyLine",
"Ctrl-x": "Cut", "Ctrl-x": "Cut|CutLine",
"Ctrl-k": "CutLine", "Ctrl-k": "CutLine",
"Ctrl-v": "Paste", "Ctrl-v": "Paste",
"Home": "StartOfTextToggle", "Home": "StartOfTextToggle",

View File

@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
type Event interface { type Event interface {

View File

@ -135,15 +135,6 @@ headerLoop:
return chosen, suggestions return chosen, suggestions
} }
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
// OptionComplete autocompletes options // OptionComplete autocompletes options
func OptionComplete(b *buffer.Buffer) ([]string, []string) { func OptionComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor() c := b.GetActiveCursor()

View File

@ -3,12 +3,12 @@ package action
import ( import (
"bytes" "bytes"
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/display" "github.com/zyedidia/micro/v2/internal/display"
"github.com/zyedidia/micro/v2/internal/info" "github.com/zyedidia/micro/v2/internal/info"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2"
) )
type InfoKeyAction func(*InfoPane) type InfoKeyAction func(*InfoPane)

View File

@ -3,7 +3,7 @@ package action
import ( import (
"bytes" "bytes"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
type PaneKeyAction func(Pane) bool type PaneKeyAction func(Pane) bool

View File

@ -4,9 +4,9 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/display" "github.com/zyedidia/micro/v2/internal/display"
"github.com/zyedidia/tcell/v2"
) )
type RawPane struct { type RawPane struct {

View File

@ -3,13 +3,13 @@ package action
import ( import (
luar "layeh.com/gopher-luar" luar "layeh.com/gopher-luar"
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/display" "github.com/zyedidia/micro/v2/internal/display"
ulua "github.com/zyedidia/micro/v2/internal/lua" ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/views" "github.com/zyedidia/micro/v2/internal/views"
"github.com/zyedidia/tcell/v2"
) )
// The TabList is a list of tabs and a window to display the tab bar // The TabList is a list of tabs and a window to display the tab bar
@ -211,7 +211,7 @@ func InitTabs(bufs []*buffer.Buffer) {
for _, b := range bufs[1:] { for _, b := range bufs[1:] {
if multiopen == "vsplit" { if multiopen == "vsplit" {
MainTab().CurPane().VSplitBuf(b) MainTab().CurPane().VSplitBuf(b)
} else { // default hsplit } else { // default hsplit
MainTab().CurPane().HSplitBuf(b) MainTab().CurPane().HSplitBuf(b)
} }
} }
@ -349,6 +349,16 @@ func (t *Tab) SetActive(i int) {
} }
} }
// AddPane adds a pane at a given index
func (t *Tab) AddPane(pane Pane, i int) {
if len(t.Panes) == i {
t.Panes = append(t.Panes, pane)
return
}
t.Panes = append(t.Panes[:i+1], t.Panes[i:]...)
t.Panes[i] = pane
}
// GetPane returns the pane with the given split index // GetPane returns the pane with the given split index
func (t *Tab) GetPane(splitid uint64) int { func (t *Tab) GetPane(splitid uint64) int {
for i, p := range t.Panes { for i, p := range t.Panes {

View File

@ -1,4 +1,4 @@
// +build linux darwin dragonfly openbsd_amd64 freebsd //go:build linux || darwin || dragonfly || solaris || openbsd || netbsd || freebsd
package action package action

View File

@ -1,4 +1,4 @@
// +build !linux,!darwin,!freebsd,!dragonfly,!openbsd_amd64 //go:build plan9 || nacl || windows
package action package action

View File

@ -4,13 +4,13 @@ import (
"errors" "errors"
"runtime" "runtime"
"github.com/micro-editor/tcell/v2"
"github.com/micro-editor/terminal"
"github.com/zyedidia/micro/v2/internal/clipboard" "github.com/zyedidia/micro/v2/internal/clipboard"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/display" "github.com/zyedidia/micro/v2/internal/display"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell" "github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/tcell/v2"
"github.com/zyedidia/terminal"
) )
type TermKeyAction func(*TermPane) type TermKeyAction func(*TermPane)

View File

@ -2,7 +2,7 @@ package buffer
import ( import (
"bytes" "bytes"
"io/ioutil" "io/fs"
"os" "os"
"sort" "sort"
"strings" "strings"
@ -109,15 +109,15 @@ func FileComplete(b *Buffer) ([]string, []string) {
sep := string(os.PathSeparator) sep := string(os.PathSeparator)
dirs := strings.Split(input, sep) dirs := strings.Split(input, sep)
var files []os.FileInfo var files []fs.DirEntry
var err error var err error
if len(dirs) > 1 { if len(dirs) > 1 {
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
directories, _ = util.ReplaceHome(directories) directories, _ = util.ReplaceHome(directories)
files, err = ioutil.ReadDir(directories) files, err = os.ReadDir(directories)
} else { } else {
files, err = ioutil.ReadDir(".") files, err = os.ReadDir(".")
} }
if err != nil { if err != nil {

View File

@ -1,24 +1,26 @@
package buffer package buffer
import ( import (
"errors"
"fmt" "fmt"
"io" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"sync/atomic"
"time"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"golang.org/x/text/encoding"
) )
const backupMsg = `A backup was detected for this file. This likely means that micro const BackupMsg = `A backup was detected for:
crashed while editing this file, or another instance of micro is currently
editing this file.
The backup was created on %s, and the file is %s
This likely means that micro crashed while editing this file,
or another instance of micro is currently editing this file,
or an error occurred while saving this file so it may be corrupted.
The backup was created on %s and its path is:
%s %s
@ -30,90 +32,80 @@ The backup was created on %s, and the file is
Options: [r]ecover, [i]gnore, [a]bort: ` Options: [r]ecover, [i]gnore, [a]bort: `
var backupRequestChan chan *Buffer const backupSeconds = 8
func backupThread() { var BackupCompleteChan chan *Buffer
for {
time.Sleep(time.Second * 8)
for len(backupRequestChan) > 0 {
b := <-backupRequestChan
bfini := atomic.LoadInt32(&(b.fini)) != 0
if !bfini {
b.Backup()
}
}
}
}
func init() { func init() {
backupRequestChan = make(chan *Buffer, 10) BackupCompleteChan = make(chan *Buffer, 10)
go backupThread()
} }
func (b *Buffer) RequestBackup() { func (b *Buffer) RequestBackup() {
if !b.requestedBackup { if !b.RequestedBackup {
select { select {
case backupRequestChan <- b: case backupRequestChan <- b:
default: default:
// channel is full // channel is full
} }
b.requestedBackup = true b.RequestedBackup = true
} }
} }
// Backup saves the current buffer to ConfigDir/backups func (b *Buffer) backupDir() string {
backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string))
if backupdir == "" || err != nil {
backupdir = filepath.Join(config.ConfigDir, "backups")
}
return backupdir
}
func (b *Buffer) keepBackup() bool {
return b.forceKeepBackup || b.Settings["permbackup"].(bool)
}
// Backup saves the current buffer to the backups directory
func (b *Buffer) Backup() error { func (b *Buffer) Backup() error {
if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault { if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault {
return nil return nil
} }
backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string)) backupdir := b.backupDir()
if backupdir == "" || err != nil { if _, err := os.Stat(backupdir); errors.Is(err, fs.ErrNotExist) {
backupdir = filepath.Join(config.ConfigDir, "backups")
}
if _, err := os.Stat(backupdir); os.IsNotExist(err) {
os.Mkdir(backupdir, os.ModePerm) os.Mkdir(backupdir, os.ModePerm)
} }
name := filepath.Join(backupdir, util.EscapePath(b.AbsPath)) name := util.DetermineEscapePath(backupdir, b.AbsPath)
if _, err := os.Stat(name); errors.Is(err, fs.ErrNotExist) {
err = overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) { _, err = b.overwriteFile(name)
if len(b.lines) == 0 { if err == nil {
return BackupCompleteChan <- b
} }
return err
}
// end of line tmp := util.AppendBackupSuffix(name)
eol := []byte{'\n'} _, err := b.overwriteFile(tmp)
if err != nil {
os.Remove(tmp)
return err
}
err = os.Rename(tmp, name)
if err != nil {
os.Remove(tmp)
return err
}
// write lines BackupCompleteChan <- b
if _, e = file.Write(b.lines[0].data); e != nil {
return
}
for _, l := range b.lines[1:] {
if _, e = file.Write(eol); e != nil {
return
}
if _, e = file.Write(l.data); e != nil {
return
}
}
return
}, false)
b.requestedBackup = false
return err return err
} }
// RemoveBackup removes any backup file associated with this buffer // RemoveBackup removes any backup file associated with this buffer
func (b *Buffer) RemoveBackup() { func (b *Buffer) RemoveBackup() {
if !b.Settings["backup"].(bool) || b.Settings["permbackup"].(bool) || b.Path == "" || b.Type != BTDefault { if !b.Settings["backup"].(bool) || b.keepBackup() || b.Path == "" || b.Type != BTDefault {
return return
} }
f := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath)) f := util.DetermineEscapePath(b.backupDir(), b.AbsPath)
os.Remove(f) os.Remove(f)
} }
@ -121,13 +113,13 @@ func (b *Buffer) RemoveBackup() {
// Returns true if a backup was applied // Returns true if a backup was applied
func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) { func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) {
if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault { if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault {
backupfile := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath)) backupfile := util.DetermineEscapePath(b.backupDir(), b.AbsPath)
if info, err := os.Stat(backupfile); err == nil { if info, err := os.Stat(backupfile); err == nil {
backup, err := os.Open(backupfile) backup, err := os.Open(backupfile)
if err == nil { if err == nil {
defer backup.Close() defer backup.Close()
t := info.ModTime() t := info.ModTime()
msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), util.EscapePath(b.AbsPath)) msg := fmt.Sprintf(BackupMsg, b.Path, t.Format("Mon Jan _2 at 15:04, 2006"), backupfile)
choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true) choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true)
if choice%3 == 0 { if choice%3 == 0 {

View File

@ -7,9 +7,8 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/fs"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -25,13 +24,12 @@ import (
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/micro/v2/pkg/highlight" "github.com/zyedidia/micro/v2/pkg/highlight"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/htmlindex" "golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/encoding/unicode" "golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform" "golang.org/x/text/transform"
) )
const backupTime = 8000
var ( var (
// OpenBuffers is a list of the currently open buffers // OpenBuffers is a list of the currently open buffers
OpenBuffers []*Buffer OpenBuffers []*Buffer
@ -89,6 +87,8 @@ type SharedBuffer struct {
// LocalSettings customized by the user for this buffer only // LocalSettings customized by the user for this buffer only
LocalSettings map[string]bool LocalSettings map[string]bool
encoding encoding.Encoding
Suggestions []string Suggestions []string
Completions []string Completions []string
CurSuggestion int CurSuggestion int
@ -101,7 +101,8 @@ type SharedBuffer struct {
diffLock sync.RWMutex diffLock sync.RWMutex
diff map[int]DiffStatus diff map[int]DiffStatus
requestedBackup bool RequestedBackup bool
forceKeepBackup bool
// ReloadDisabled allows the user to disable reloads if they // ReloadDisabled allows the user to disable reloads if they
// are viewing a file that is constantly changing // are viewing a file that is constantly changing
@ -209,6 +210,11 @@ type Buffer struct {
LastSearchRegex bool LastSearchRegex bool
// HighlightSearch enables highlighting all instances of the last successful search // HighlightSearch enables highlighting all instances of the last successful search
HighlightSearch bool HighlightSearch bool
// OverwriteMode indicates that we are in overwrite mode (toggled by
// Insert key by default) i.e. that typing a character shall replace the
// character under the cursor instead of inserting a character before it.
OverwriteMode bool
} }
// NewBufferFromFileAtLoc opens a new buffer with a given cursor location // NewBufferFromFileAtLoc opens a new buffer with a given cursor location
@ -232,17 +238,20 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
return nil, err return nil, err
} }
f, err := os.OpenFile(filename, os.O_WRONLY, 0)
readonly := os.IsPermission(err)
f.Close()
fileInfo, serr := os.Stat(filename) fileInfo, serr := os.Stat(filename)
if serr != nil && !os.IsNotExist(serr) { if serr != nil && !errors.Is(serr, fs.ErrNotExist) {
return nil, serr return nil, serr
} }
if serr == nil && fileInfo.IsDir() { if serr == nil && fileInfo.IsDir() {
return nil, errors.New("Error: " + filename + " is a directory and cannot be opened") return nil, errors.New("Error: " + filename + " is a directory and cannot be opened")
} }
if serr == nil && !fileInfo.Mode().IsRegular() {
return nil, errors.New("Error: " + filename + " is not a regular file and cannot be opened")
}
f, err := os.OpenFile(filename, os.O_WRONLY, 0)
readonly := errors.Is(err, fs.ErrPermission)
f.Close()
file, err := os.Open(filename) file, err := os.Open(filename)
if err == nil { if err == nil {
@ -250,7 +259,7 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
} }
var buf *Buffer var buf *Buffer
if os.IsNotExist(err) { if errors.Is(err, fs.ErrNotExist) {
// File does not exist -- create an empty buffer with that name // File does not exist -- create an empty buffer with that name
buf = NewBufferFromString("", filename, btype) buf = NewBufferFromString("", filename, btype)
} else if err != nil { } else if err != nil {
@ -320,30 +329,19 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
b.AbsPath = absPath b.AbsPath = absPath
b.Path = path b.Path = path
// this is a little messy since we need to know some settings to read
// the file properly, but some settings depend on the filetype, which
// we don't know until reading the file. We first read the settings
// into a local variable and then use that to determine the encoding,
// readonly, and fileformat necessary for reading the file and
// assigning the filetype.
settings := config.DefaultCommonSettings()
b.Settings = config.DefaultCommonSettings() b.Settings = config.DefaultCommonSettings()
b.LocalSettings = make(map[string]bool) b.LocalSettings = make(map[string]bool)
for k, v := range config.GlobalSettings { for k, v := range config.GlobalSettings {
if _, ok := config.DefaultGlobalOnlySettings[k]; !ok { if _, ok := config.DefaultGlobalOnlySettings[k]; !ok {
// make sure setting is not global-only // make sure setting is not global-only
settings[k] = v
b.Settings[k] = v b.Settings[k] = v
} }
} }
config.InitLocalSettings(settings, absPath) config.UpdatePathGlobLocals(b.Settings, absPath)
b.Settings["readonly"] = settings["readonly"]
b.Settings["filetype"] = settings["filetype"]
b.Settings["syntax"] = settings["syntax"]
enc, err := htmlindex.Get(settings["encoding"].(string)) b.encoding, err = htmlindex.Get(b.Settings["encoding"].(string))
if err != nil { if err != nil {
enc = unicode.UTF8 b.encoding = unicode.UTF8
b.Settings["encoding"] = "utf-8" b.Settings["encoding"] = "utf-8"
} }
@ -354,14 +352,14 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
return NewBufferFromString("", "", btype) return NewBufferFromString("", "", btype)
} }
if !hasBackup { if !hasBackup {
reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder())) reader := bufio.NewReader(transform.NewReader(r, b.encoding.NewDecoder()))
var ff FileFormat = FFAuto var ff FileFormat = FFAuto
if size == 0 { if size == 0 {
// for empty files, use the fileformat setting instead of // for empty files, use the fileformat setting instead of
// autodetection // autodetection
switch settings["fileformat"] { switch b.Settings["fileformat"] {
case "unix": case "unix":
ff = FFUnix ff = FFUnix
case "dos": case "dos":
@ -392,10 +390,10 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
} }
b.UpdateRules() b.UpdateRules()
// init local settings again now that we know the filetype // we know the filetype now, so update per-filetype settings
config.InitLocalSettings(b.Settings, b.Path) config.UpdateFileTypeLocals(b.Settings, b.Settings["filetype"].(string))
if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); os.IsNotExist(err) { if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); errors.Is(err, fs.ErrNotExist) {
os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm) os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm)
} }
@ -480,7 +478,7 @@ func (b *Buffer) GetName() string {
name = b.Path name = b.Path
} }
if b.Settings["basename"].(bool) { if b.Settings["basename"].(bool) {
return path.Base(name) return filepath.Base(name)
} }
return name return name
} }
@ -546,7 +544,7 @@ func (b *Buffer) ReOpen() error {
} }
reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder())) reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder()))
data, err := ioutil.ReadAll(reader) data, err := io.ReadAll(reader)
txt := string(data) txt := string(data)
if err != nil { if err != nil {
@ -621,6 +619,16 @@ func (b *Buffer) WordAt(loc Loc) []byte {
return b.Substr(start, end) return b.Substr(start, end)
} }
// Shared returns if there are other buffers with the same file as this buffer
func (b *Buffer) Shared() bool {
for _, buf := range OpenBuffers {
if buf != b && buf.SharedBuffer == b.SharedBuffer {
return true
}
}
return false
}
// Modified returns if this buffer has been modified since // Modified returns if this buffer has been modified since
// being opened // being opened
func (b *Buffer) Modified() bool { func (b *Buffer) Modified() bool {

View File

@ -20,8 +20,14 @@ type Cursor struct {
buf *Buffer buf *Buffer
Loc Loc
// Last cursor x position // Last visual x position of the cursor. Used in cursor up/down movements
// for remembering the original x position when moving to a line that is
// shorter than current x position.
LastVisualX int LastVisualX int
// Similar to LastVisualX but takes softwrapping into account, i.e. last
// visual x position in a visual (wrapped) line on the screen, which may be
// different from the line in the buffer.
LastWrappedVisualX int
// The current selection as a range of character numbers (inclusive) // The current selection as a range of character numbers (inclusive)
CurSelection [2]Loc CurSelection [2]Loc
@ -61,8 +67,9 @@ func (c *Cursor) Buf() *Buffer {
// Goto puts the cursor at the given cursor's location and gives // Goto puts the cursor at the given cursor's location and gives
// the current cursor its selection too // the current cursor its selection too
func (c *Cursor) Goto(b Cursor) { func (c *Cursor) Goto(b Cursor) {
c.X, c.Y, c.LastVisualX = b.X, b.Y, b.LastVisualX c.X, c.Y = b.X, b.Y
c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
c.StoreVisualX()
} }
// GotoLoc puts the cursor at the given cursor's location and gives // GotoLoc puts the cursor at the given cursor's location and gives
@ -73,8 +80,8 @@ func (c *Cursor) GotoLoc(l Loc) {
} }
// GetVisualX returns the x value of the cursor in visual spaces // GetVisualX returns the x value of the cursor in visual spaces
func (c *Cursor) GetVisualX() int { func (c *Cursor) GetVisualX(wrap bool) int {
if c.buf.GetVisualX != nil { if wrap && c.buf.GetVisualX != nil {
return c.buf.GetVisualX(c.Loc) return c.buf.GetVisualX(c.Loc)
} }
@ -100,7 +107,7 @@ func (c *Cursor) GetCharPosInLine(b []byte, visualPos int) int {
// Start moves the cursor to the start of the line it is on // Start moves the cursor to the start of the line it is on
func (c *Cursor) Start() { func (c *Cursor) Start() {
c.X = 0 c.X = 0
c.LastVisualX = c.GetVisualX() c.StoreVisualX()
} }
// StartOfText moves the cursor to the first non-whitespace rune of // StartOfText moves the cursor to the first non-whitespace rune of
@ -131,7 +138,7 @@ func (c *Cursor) IsStartOfText() bool {
// End moves the cursor to the end of the line it is on // End moves the cursor to the end of the line it is on
func (c *Cursor) End() { func (c *Cursor) End() {
c.X = util.CharacterCount(c.buf.LineBytes(c.Y)) c.X = util.CharacterCount(c.buf.LineBytes(c.Y))
c.LastVisualX = c.GetVisualX() c.StoreVisualX()
} }
// CopySelection copies the user's selection to either "primary" // CopySelection copies the user's selection to either "primary"
@ -186,7 +193,7 @@ func (c *Cursor) Deselect(start bool) {
if start { if start {
c.Loc = c.CurSelection[0] c.Loc = c.CurSelection[0]
} else { } else {
c.Loc = c.CurSelection[1].Move(-1, c.buf) c.Loc = c.CurSelection[1]
} }
c.ResetSelection() c.ResetSelection()
c.StoreVisualX() c.StoreVisualX()
@ -615,5 +622,6 @@ func (c *Cursor) RuneUnder(x int) rune {
} }
func (c *Cursor) StoreVisualX() { func (c *Cursor) StoreVisualX() {
c.LastVisualX = c.GetVisualX() c.LastVisualX = c.GetVisualX(false)
c.LastWrappedVisualX = c.GetVisualX(true)
} }

View File

@ -104,7 +104,7 @@ func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) {
c.OrigSelection[0] = move(c.OrigSelection[0]) c.OrigSelection[0] = move(c.OrigSelection[0])
c.OrigSelection[1] = move(c.OrigSelection[1]) c.OrigSelection[1] = move(c.OrigSelection[1])
c.Relocate() c.Relocate()
c.LastVisualX = c.GetVisualX() c.StoreVisualX()
} }
if useUndo { if useUndo {

View File

@ -285,11 +285,6 @@ func (la *LineArray) deleteLines(y1, y2 int) {
la.lines = la.lines[:y1+copy(la.lines[y1:], la.lines[y2+1:])] la.lines = la.lines[:y1+copy(la.lines[y1:], la.lines[y2+1:])]
} }
// DeleteByte deletes the byte at a position
func (la *LineArray) deleteByte(pos Loc) {
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X+copy(la.lines[pos.Y].data[pos.X:], la.lines[pos.Y].data[pos.X+1:])]
}
// Substr returns the string representation between two locations // Substr returns the string representation between two locations
func (la *LineArray) Substr(start, end Loc) []byte { func (la *LineArray) Substr(start, end Loc) []byte {
startX := runeToByteIndex(start.X, la.lines[start.Y].data) startX := runeToByteIndex(start.X, la.lines[start.Y].data)

View File

@ -47,6 +47,16 @@ func (l Loc) LessEqual(b Loc) bool {
return l == b return l == b
} }
// Clamp clamps a loc between start and end
func (l Loc) Clamp(start, end Loc) Loc {
if l.GreaterEqual(end) {
return end
} else if l.LessThan(start) {
return start
}
return l
}
// The following functions require a buffer to know where newlines are // The following functions require a buffer to know where newlines are
// Diff returns the distance between two locations // Diff returns the distance between two locations
@ -139,10 +149,5 @@ func ByteOffset(pos Loc, buf *Buffer) int {
// clamps a loc within a buffer // clamps a loc within a buffer
func clamp(pos Loc, la *LineArray) Loc { func clamp(pos Loc, la *LineArray) Loc {
if pos.GreaterEqual(la.End()) { return pos.Clamp(la.Start(), la.End())
return la.End()
} else if pos.LessThan(la.Start()) {
return la.Start()
}
return pos
} }

View File

@ -1,8 +1,8 @@
package buffer package buffer
import ( import (
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/tcell/v2"
) )
type MsgType int type MsgType int

View File

@ -5,18 +5,19 @@ import (
"bytes" "bytes"
"errors" "errors"
"io" "io"
"io/fs"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sync/atomic"
"time"
"unicode" "unicode"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/transform" "golang.org/x/text/transform"
) )
@ -24,68 +25,178 @@ import (
// because hashing is too slow // because hashing is too slow
const LargeFileThreshold = 50000 const LargeFileThreshold = 50000
// overwriteFile opens the given file for writing, truncating if one exists, and then calls type wrappedFile struct {
// the supplied function with the file as io.Writer object, also making sure the file is writeCloser io.WriteCloser
// closed afterwards. withSudo bool
func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, withSudo bool) (err error) { screenb bool
cmd *exec.Cmd
sigChan chan os.Signal
}
type saveResponse struct {
size int
err error
}
type saveRequest struct {
buf *Buffer
path string
withSudo bool
newFile bool
saveResponseChan chan saveResponse
}
var saveRequestChan chan saveRequest
var backupRequestChan chan *Buffer
func init() {
saveRequestChan = make(chan saveRequest, 10)
backupRequestChan = make(chan *Buffer, 10)
go func() {
duration := backupSeconds * float64(time.Second)
backupTicker := time.NewTicker(time.Duration(duration))
for {
select {
case sr := <-saveRequestChan:
size, err := sr.buf.safeWrite(sr.path, sr.withSudo, sr.newFile)
sr.saveResponseChan <- saveResponse{size, err}
case <-backupTicker.C:
for len(backupRequestChan) > 0 {
b := <-backupRequestChan
bfini := atomic.LoadInt32(&(b.fini)) != 0
if !bfini {
b.Backup()
}
}
}
}
}()
}
func openFile(name string, withSudo bool) (wrappedFile, error) {
var err error
var writeCloser io.WriteCloser var writeCloser io.WriteCloser
var screenb bool var screenb bool
var cmd *exec.Cmd var cmd *exec.Cmd
var sigChan chan os.Signal
if withSudo { if withSudo {
cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name) cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name)
writeCloser, err = cmd.StdinPipe()
if writeCloser, err = cmd.StdinPipe(); err != nil { if err != nil {
return return wrappedFile{}, err
} }
c := make(chan os.Signal, 1) sigChan = make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt) signal.Reset(os.Interrupt)
go func() { signal.Notify(sigChan, os.Interrupt)
<-c
cmd.Process.Kill()
}()
screenb = screen.TempFini() screenb = screen.TempFini()
// need to start the process now, otherwise when we flush the file // need to start the process now, otherwise when we flush the file
// contents to its stdin it might hang because the kernel's pipe size // contents to its stdin it might hang because the kernel's pipe size
// is too small to handle the full file contents all at once // is too small to handle the full file contents all at once
if e := cmd.Start(); e != nil && err == nil { err = cmd.Start()
if err != nil {
screen.TempStart(screenb) screen.TempStart(screenb)
return err
signal.Notify(util.Sigterm, os.Interrupt)
signal.Stop(sigChan)
return wrappedFile{}, err
} }
} else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666); err != nil { } else {
return writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, util.FileMode)
} if err != nil {
return wrappedFile{}, err
w := bufio.NewWriter(transform.NewWriter(writeCloser, enc.NewEncoder()))
err = fn(w)
if err2 := w.Flush(); err2 != nil && err == nil {
err = err2
}
// Call Sync() on the file to make sure the content is safely on disk.
// Does not work with sudo as we don't have direct access to the file.
if !withSudo {
f := writeCloser.(*os.File)
if err2 := f.Sync(); err2 != nil && err == nil {
err = err2
} }
} }
if err2 := writeCloser.Close(); err2 != nil && err == nil {
err = err2 return wrappedFile{writeCloser, withSudo, screenb, cmd, sigChan}, nil
}
func (wf wrappedFile) Write(b *Buffer) (int, error) {
file := bufio.NewWriter(transform.NewWriter(wf.writeCloser, b.encoding.NewEncoder()))
b.Lock()
defer b.Unlock()
if len(b.lines) == 0 {
return 0, nil
} }
if withSudo { // end of line
var eol []byte
if b.Endings == FFDos {
eol = []byte{'\r', '\n'}
} else {
eol = []byte{'\n'}
}
if !wf.withSudo {
f := wf.writeCloser.(*os.File)
err := f.Truncate(0)
if err != nil {
return 0, err
}
}
// write lines
size, err := file.Write(b.lines[0].data)
if err != nil {
return 0, err
}
for _, l := range b.lines[1:] {
if _, err = file.Write(eol); err != nil {
return 0, err
}
if _, err = file.Write(l.data); err != nil {
return 0, err
}
size += len(eol) + len(l.data)
}
err = file.Flush()
if err == nil && !wf.withSudo {
// Call Sync() on the file to make sure the content is safely on disk.
f := wf.writeCloser.(*os.File)
err = f.Sync()
}
return size, err
}
func (wf wrappedFile) Close() error {
err := wf.writeCloser.Close()
if wf.withSudo {
// wait for dd to finish and restart the screen if we used sudo // wait for dd to finish and restart the screen if we used sudo
err := cmd.Wait() err := wf.cmd.Wait()
screen.TempStart(screenb) screen.TempStart(wf.screenb)
signal.Notify(util.Sigterm, os.Interrupt)
signal.Stop(wf.sigChan)
if err != nil { if err != nil {
return err return err
} }
} }
return err
}
return func (b *Buffer) overwriteFile(name string) (int, error) {
file, err := openFile(name, false)
if err != nil {
return 0, err
}
size, err := file.Write(b)
err2 := file.Close()
if err2 != nil && err == nil {
err = err2
}
return size, err
} }
// Save saves the buffer to its default path // Save saves the buffer to its default path
@ -146,19 +257,35 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
} }
} }
// Update the last time this file was updated after saving filename, err = util.ReplaceHome(filename)
defer func() { if err != nil {
b.ModTime, _ = util.GetModTime(filename) return err
err = b.Serialize() }
}()
// Removes any tilde and replaces with the absolute path to home newFile := false
absFilename, _ := util.ReplaceHome(filename) fileInfo, err := os.Stat(filename)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
newFile = true
}
if err == nil && fileInfo.IsDir() {
return errors.New("Error: " + filename + " is a directory and cannot be saved")
}
if err == nil && !fileInfo.Mode().IsRegular() {
return errors.New("Error: " + filename + " is not a regular file and cannot be saved")
}
absFilename, err := filepath.Abs(filename)
if err != nil {
return err
}
// Get the leading path to the file | "." is returned if there's no leading path provided // Get the leading path to the file | "." is returned if there's no leading path provided
if dirname := filepath.Dir(absFilename); dirname != "." { if dirname := filepath.Dir(absFilename); dirname != "." {
// Check if the parent dirs don't exist // Check if the parent dirs don't exist
if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) { if _, statErr := os.Stat(dirname); errors.Is(statErr, fs.ErrNotExist) {
// Prompt to make sure they want to create the dirs that are missing // Prompt to make sure they want to create the dirs that are missing
if b.Settings["mkparents"].(bool) { if b.Settings["mkparents"].(bool) {
// Create all leading dir(s) since they don't exist // Create all leading dir(s) since they don't exist
@ -172,49 +299,22 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
} }
} }
var fileSize int saveResponseChan := make(chan saveResponse)
saveRequestChan <- saveRequest{b, absFilename, withSudo, newFile, saveResponseChan}
enc, err := htmlindex.Get(b.Settings["encoding"].(string)) result := <-saveResponseChan
err = result.err
if err != nil { if err != nil {
return err if errors.Is(err, util.ErrOverwrite) {
} screen.TermMessage(err)
err = errors.Unwrap(err)
fwriter := func(file io.Writer) (e error) { b.UpdateModTime()
if len(b.lines) == 0 {
return
} }
// end of line
var eol []byte
if b.Endings == FFDos {
eol = []byte{'\r', '\n'}
} else {
eol = []byte{'\n'}
}
// write lines
if fileSize, e = file.Write(b.lines[0].data); e != nil {
return
}
for _, l := range b.lines[1:] {
if _, e = file.Write(eol); e != nil {
return
}
if _, e = file.Write(l.data); e != nil {
return
}
fileSize += len(eol) + len(l.data)
}
return
}
if err = overwriteFile(absFilename, enc, fwriter, withSudo); err != nil {
return err return err
} }
if !b.Settings["fastdirty"].(bool) { if !b.Settings["fastdirty"].(bool) {
if fileSize > LargeFileThreshold { if result.size > LargeFileThreshold {
// For large files 'fastdirty' needs to be on // For large files 'fastdirty' needs to be on
b.Settings["fastdirty"] = true b.Settings["fastdirty"] = true
} else { } else {
@ -222,10 +322,70 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
} }
} }
newPath := b.Path != filename
b.Path = filename b.Path = filename
absPath, _ := filepath.Abs(filename) b.AbsPath = absFilename
b.AbsPath = absPath
b.isModified = false b.isModified = false
b.UpdateRules() b.UpdateModTime()
if newPath {
// need to update glob-based and filetype-based settings
b.ReloadSettings(true)
}
err = b.Serialize()
return err return err
} }
// safeWrite writes the buffer to a file in a "safe" way, preventing loss of the
// contents of the file if it fails to write the new contents.
// This means that the file is not overwritten directly but by writing to the
// backup file first.
func (b *Buffer) safeWrite(path string, withSudo bool, newFile bool) (int, error) {
file, err := openFile(path, withSudo)
if err != nil {
return 0, err
}
defer func() {
if newFile && err != nil {
os.Remove(path)
}
}()
backupDir := b.backupDir()
if _, err := os.Stat(backupDir); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return 0, err
}
if err = os.Mkdir(backupDir, os.ModePerm); err != nil {
return 0, err
}
}
backupName := util.DetermineEscapePath(backupDir, path)
_, err = b.overwriteFile(backupName)
if err != nil {
os.Remove(backupName)
return 0, err
}
b.forceKeepBackup = true
size, err := file.Write(b)
if err != nil {
err = util.OverwriteError{err, backupName}
return size, err
}
b.forceKeepBackup = false
if !b.keepBackup() {
os.Remove(backupName)
}
err2 := file.Close()
if err2 != nil && err == nil {
err = err2
}
return size, err
}

View File

@ -2,10 +2,56 @@ package buffer
import ( import (
"regexp" "regexp"
"unicode/utf8"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
) )
// We want "^" and "$" to match only the beginning/end of a line, not the
// beginning/end of the search region if it is in the middle of a line.
// In that case we use padded regexps to require a rune before or after
// the match. (This also affects other empty-string patters like "\\b".)
// The following two flags indicate the padding used.
const (
padStart = 1 << iota
padEnd
)
func findLineParams(b *Buffer, start, end Loc, i int, r *regexp.Regexp) ([]byte, int, int, *regexp.Regexp) {
l := b.LineBytes(i)
charpos := 0
padMode := 0
if i == end.Y {
nchars := util.CharacterCount(l)
end.X = util.Clamp(end.X, 0, nchars)
if end.X < nchars {
l = util.SliceStart(l, end.X+1)
padMode |= padEnd
}
}
if i == start.Y {
nchars := util.CharacterCount(l)
start.X = util.Clamp(start.X, 0, nchars)
if start.X > 0 {
charpos = start.X - 1
l = util.SliceEnd(l, charpos)
padMode |= padStart
}
}
if padMode == padStart {
r = regexp.MustCompile(".(?:" + r.String() + ")")
} else if padMode == padEnd {
r = regexp.MustCompile("(?:" + r.String() + ").")
} else if padMode == padStart|padEnd {
r = regexp.MustCompile(".(?:" + r.String() + ").")
}
return l, charpos, padMode, r
}
func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) { func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
lastcn := util.CharacterCount(b.LineBytes(b.LinesNum() - 1)) lastcn := util.CharacterCount(b.LineBytes(b.LinesNum() - 1))
if start.Y > b.LinesNum()-1 { if start.Y > b.LinesNum()-1 {
@ -22,30 +68,19 @@ func (b *Buffer) findDown(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
} }
for i := start.Y; i <= end.Y; i++ { for i := start.Y; i <= end.Y; i++ {
l := b.LineBytes(i) l, charpos, padMode, rPadded := findLineParams(b, start, end, i, r)
charpos := 0
if i == start.Y && start.Y == end.Y { match := rPadded.FindIndex(l)
nchars := util.CharacterCount(l)
start.X = util.Clamp(start.X, 0, nchars)
end.X = util.Clamp(end.X, 0, nchars)
l = util.SliceStart(l, end.X)
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == start.Y {
nchars := util.CharacterCount(l)
start.X = util.Clamp(start.X, 0, nchars)
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == end.Y {
nchars := util.CharacterCount(l)
end.X = util.Clamp(end.X, 0, nchars)
l = util.SliceStart(l, end.X)
}
match := r.FindIndex(l)
if match != nil { if match != nil {
if padMode&padStart != 0 {
_, size := utf8.DecodeRune(l[match[0]:])
match[0] += size
}
if padMode&padEnd != 0 {
_, size := utf8.DecodeLastRune(l[:match[1]])
match[1] -= size
}
start := Loc{charpos + util.RunePos(l, match[0]), i} start := Loc{charpos + util.RunePos(l, match[0]), i}
end := Loc{charpos + util.RunePos(l, match[1]), i} end := Loc{charpos + util.RunePos(l, match[1]), i}
return [2]Loc{start, end}, true return [2]Loc{start, end}, true
@ -70,39 +105,39 @@ func (b *Buffer) findUp(r *regexp.Regexp, start, end Loc) ([2]Loc, bool) {
} }
for i := end.Y; i >= start.Y; i-- { for i := end.Y; i >= start.Y; i-- {
l := b.LineBytes(i) charCount := util.CharacterCount(b.LineBytes(i))
charpos := 0 from := Loc{0, i}.Clamp(start, end)
to := Loc{charCount, i}.Clamp(start, end)
if i == start.Y && start.Y == end.Y {
nchars := util.CharacterCount(l)
start.X = util.Clamp(start.X, 0, nchars)
end.X = util.Clamp(end.X, 0, nchars)
l = util.SliceStart(l, end.X)
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == start.Y {
nchars := util.CharacterCount(l)
start.X = util.Clamp(start.X, 0, nchars)
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == end.Y {
nchars := util.CharacterCount(l)
end.X = util.Clamp(end.X, 0, nchars)
l = util.SliceStart(l, end.X)
}
allMatches := r.FindAllIndex(l, -1)
allMatches := b.findAll(r, from, to)
if allMatches != nil { if allMatches != nil {
match := allMatches[len(allMatches)-1] match := allMatches[len(allMatches)-1]
start := Loc{charpos + util.RunePos(l, match[0]), i} return [2]Loc{match[0], match[1]}, true
end := Loc{charpos + util.RunePos(l, match[1]), i}
return [2]Loc{start, end}, true
} }
} }
return [2]Loc{}, false return [2]Loc{}, false
} }
func (b *Buffer) findAll(r *regexp.Regexp, start, end Loc) [][2]Loc {
var matches [][2]Loc
loc := start
for {
match, found := b.findDown(r, loc, end)
if !found {
break
}
matches = append(matches, match)
if match[0] != match[1] {
loc = match[1]
} else if match[1] != end {
loc = match[1].Move(1, b)
} else {
break
}
}
return matches
}
// FindNext finds the next occurrence of a given string in the buffer // FindNext finds the next occurrence of a given string in the buffer
// It returns the start and end location of the match (if found) and // It returns the start and end location of the match (if found) and
// a boolean indicating if it was found // a boolean indicating if it was found
@ -146,53 +181,58 @@ func (b *Buffer) FindNext(s string, start, end, from Loc, down bool, useRegex bo
} }
// ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area // ReplaceRegex replaces all occurrences of 'search' with 'replace' in the given area
// and returns the number of replacements made and the number of runes // and returns the number of replacements made and the number of characters
// added or removed on the last line of the range // added or removed on the last line of the range
func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte, captureGroups bool) (int, int) { func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte, captureGroups bool) (int, int) {
if start.GreaterThan(end) { if start.GreaterThan(end) {
start, end = end, start start, end = end, start
} }
netrunes := 0 charsEnd := util.CharacterCount(b.LineBytes(end.Y))
found := 0 found := 0
var deltas []Delta var deltas []Delta
for i := start.Y; i <= end.Y; i++ { for i := start.Y; i <= end.Y; i++ {
l := b.lines[i].data l := b.LineBytes(i)
charpos := 0 charCount := util.CharacterCount(l)
if (i == start.Y && start.X > 0) || (i == end.Y && end.X < charCount) {
// This replacement code works in general, but it creates a separate
// modification for each match. We only use it for the first and last
// lines, which may use padded regexps
if start.Y == end.Y && i == start.Y { from := Loc{0, i}.Clamp(start, end)
l = util.SliceStart(l, end.X) to := Loc{charCount, i}.Clamp(start, end)
l = util.SliceEnd(l, start.X) matches := b.findAll(search, from, to)
charpos = start.X found += len(matches)
} else if i == start.Y {
l = util.SliceEnd(l, start.X) for j := len(matches) - 1; j >= 0; j-- {
charpos = start.X // if we counted upwards, the different deltas would interfere
} else if i == end.Y { match := matches[j]
l = util.SliceStart(l, end.X) var newText []byte
} if captureGroups {
newText := search.ReplaceAllFunc(l, func(in []byte) []byte { newText = search.ReplaceAll(b.Substr(match[0], match[1]), replace)
var result []byte } else {
if captureGroups { newText = replace
for _, submatches := range search.FindAllSubmatchIndex(in, -1) {
result = search.Expand(result, replace, in, submatches)
} }
} else { deltas = append(deltas, Delta{newText, match[0], match[1]})
result = replace
} }
found++ } else {
if i == end.Y { newLine := search.ReplaceAllFunc(l, func(in []byte) []byte {
netrunes += util.CharacterCount(result) - util.CharacterCount(in) found++
} var result []byte
return result if captureGroups {
}) match := search.FindSubmatchIndex(in)
result = search.Expand(result, replace, in, match)
from := Loc{charpos, i} } else {
to := Loc{charpos + util.CharacterCount(l), i} result = replace
}
deltas = append(deltas, Delta{newText, from, to}) return result
})
deltas = append(deltas, Delta{newLine, Loc{0, i}, Loc{charCount, i}})
}
} }
b.MultipleReplace(deltas) b.MultipleReplace(deltas)
return found, netrunes return found, util.CharacterCount(b.LineBytes(end.Y)) - charsEnd
} }

View File

@ -1,15 +1,13 @@
package buffer package buffer
import ( import (
"bytes"
"encoding/gob" "encoding/gob"
"errors" "errors"
"io"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
"golang.org/x/text/encoding"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
) )
@ -31,16 +29,18 @@ func (b *Buffer) Serialize() error {
return nil return nil
} }
name := filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath)) var buf bytes.Buffer
err := gob.NewEncoder(&buf).Encode(SerializedBuffer{
return overwriteFile(name, encoding.Nop, func(file io.Writer) error { b.EventHandler,
err := gob.NewEncoder(file).Encode(SerializedBuffer{ b.GetActiveCursor().Loc,
b.EventHandler, b.ModTime,
b.GetActiveCursor().Loc, })
b.ModTime, if err != nil {
})
return err return err
}, false) }
name := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath)
return util.SafeWrite(name, buf.Bytes(), true)
} }
// Unserialize loads the buffer info from config.ConfigDir/buffers // Unserialize loads the buffer info from config.ConfigDir/buffers
@ -50,7 +50,7 @@ func (b *Buffer) Unserialize() error {
if b.Path == "" { if b.Path == "" {
return nil return nil
} }
file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath))) file, err := os.Open(util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath))
if err == nil { if err == nil {
defer file.Close() defer file.Close()
var buffer SerializedBuffer var buffer SerializedBuffer

View File

@ -5,13 +5,22 @@ import (
"reflect" "reflect"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/encoding/unicode"
luar "layeh.com/gopher-luar"
) )
func (b *Buffer) ReloadSettings(reloadFiletype bool) { func (b *Buffer) ReloadSettings(reloadFiletype bool) {
settings := config.ParsedSettings() settings := config.ParsedSettings()
config.UpdatePathGlobLocals(settings, b.AbsPath)
if _, ok := b.LocalSettings["filetype"]; !ok && reloadFiletype { oldFiletype := b.Settings["filetype"].(string)
_, local := b.LocalSettings["filetype"]
_, volatile := config.VolatileSettings["filetype"]
if reloadFiletype && !local && !volatile {
// need to update filetype before updating other settings based on it // need to update filetype before updating other settings based on it
b.Settings["filetype"] = "unknown" b.Settings["filetype"] = "unknown"
if v, ok := settings["filetype"]; ok { if v, ok := settings["filetype"]; ok {
@ -21,9 +30,14 @@ func (b *Buffer) ReloadSettings(reloadFiletype bool) {
// update syntax rules, which will also update filetype if needed // update syntax rules, which will also update filetype if needed
b.UpdateRules() b.UpdateRules()
settings["filetype"] = b.Settings["filetype"]
config.InitLocalSettings(settings, b.Path) curFiletype := b.Settings["filetype"].(string)
if oldFiletype != curFiletype {
b.doCallbacks("filetype", oldFiletype, curFiletype)
}
config.UpdateFileTypeLocals(settings, curFiletype)
for k, v := range config.DefaultCommonSettings() { for k, v := range config.DefaultCommonSettings() {
if k == "filetype" { if k == "filetype" {
// prevent recursion // prevent recursion
@ -46,7 +60,8 @@ func (b *Buffer) ReloadSettings(reloadFiletype bool) {
} }
func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) { func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) {
if reflect.DeepEqual(b.Settings[option], nativeValue) { oldValue := b.Settings[option]
if reflect.DeepEqual(oldValue, nativeValue) {
return return
} }
@ -84,6 +99,12 @@ func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) {
b.UpdateRules() b.UpdateRules()
} }
} else if option == "encoding" { } else if option == "encoding" {
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
if err != nil {
enc = unicode.UTF8
b.Settings["encoding"] = "utf-8"
}
b.encoding = enc
b.isModified = true b.isModified = true
} else if option == "readonly" && b.Type.Kind == BTDefault.Kind { } else if option == "readonly" && b.Type.Kind == BTDefault.Kind {
b.Type.Readonly = nativeValue.(bool) b.Type.Readonly = nativeValue.(bool)
@ -114,9 +135,7 @@ func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) {
} }
} }
if b.OptionCallback != nil { b.doCallbacks(option, oldValue, nativeValue)
b.OptionCallback(option, nativeValue)
}
} }
func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error { func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
@ -143,3 +162,15 @@ func (b *Buffer) SetOption(option, value string) error {
return b.SetOptionNative(option, nativeValue) return b.SetOptionNative(option, nativeValue)
} }
func (b *Buffer) doCallbacks(option string, oldValue interface{}, newValue interface{}) {
if b.OptionCallback != nil {
b.OptionCallback(option, newValue)
}
if err := config.RunPluginFn("onBufferOptionChanged",
luar.New(ulua.L, b), luar.New(ulua.L, option),
luar.New(ulua.L, oldValue), luar.New(ulua.L, newValue)); err != nil {
screen.TermMessage(err)
}
}

View File

@ -4,8 +4,8 @@ import (
"errors" "errors"
"time" "time"
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/tcell/v2"
) )
type terminalClipboard struct{} type terminalClipboard struct{}

View File

@ -6,7 +6,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
// DefStyle is Micro's default style // DefStyle is Micro's default style

View File

@ -3,8 +3,8 @@ package config
import ( import (
"testing" "testing"
"github.com/micro-editor/tcell/v2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zyedidia/tcell/v2"
) )
func TestSimpleStringToStyle(t *testing.T) { func TestSimpleStringToStyle(t *testing.T) {

View File

@ -71,7 +71,7 @@ type Plugin struct {
Info *PluginInfo // json file containing info Info *PluginInfo // json file containing info
Srcs []RuntimeFile // lua files Srcs []RuntimeFile // lua files
Loaded bool Loaded bool
Default bool // pre-installed plugin Default bool // pre-installed plugin
} }
// IsLoaded returns if a plugin is enabled // IsLoaded returns if a plugin is enabled
@ -143,15 +143,3 @@ func FindPlugin(name string) *Plugin {
} }
return pl return pl
} }
// FindAnyPlugin does not require the plugin to be enabled
func FindAnyPlugin(name string) *Plugin {
var pl *Plugin
for _, p := range Plugins {
if p.Name == name {
pl = p
break
}
}
return pl
}

View File

@ -5,7 +5,6 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -14,8 +13,8 @@ import (
"sync" "sync"
"github.com/blang/semver" "github.com/blang/semver"
"github.com/micro-editor/json5"
lua "github.com/yuin/gopher-lua" lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/json5"
ulua "github.com/zyedidia/micro/v2/internal/lua" ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
) )
@ -396,7 +395,7 @@ func (pv *PluginVersion) DownloadAndInstall(out io.Writer) error {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return err
} }

View File

@ -5,7 +5,7 @@ import (
"github.com/blang/semver" "github.com/blang/semver"
"github.com/zyedidia/json5" "github.com/micro-editor/json5"
) )
func TestDependencyResolving(t *testing.T) { func TestDependencyResolving(t *testing.T) {

View File

@ -2,10 +2,8 @@ package config
import ( import (
"errors" "errors"
"io/ioutil"
"log" "log"
"os" "os"
"path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
@ -62,12 +60,6 @@ type realFile string
// some asset file // some asset file
type assetFile string type assetFile string
// some file on filesystem but with a different name
type namedFile struct {
realFile
name string
}
// a file with the data stored in memory // a file with the data stored in memory
type memoryFile struct { type memoryFile struct {
name string name string
@ -87,22 +79,18 @@ func (rf realFile) Name() string {
} }
func (rf realFile) Data() ([]byte, error) { func (rf realFile) Data() ([]byte, error) {
return ioutil.ReadFile(string(rf)) return os.ReadFile(string(rf))
} }
func (af assetFile) Name() string { func (af assetFile) Name() string {
fn := path.Base(string(af)) fn := filepath.Base(string(af))
return fn[:len(fn)-len(path.Ext(fn))] return fn[:len(fn)-len(filepath.Ext(fn))]
} }
func (af assetFile) Data() ([]byte, error) { func (af assetFile) Data() ([]byte, error) {
return rt.Asset(string(af)) return rt.Asset(string(af))
} }
func (nf namedFile) Name() string {
return nf.name
}
// AddRuntimeFile registers a file for the given filetype // AddRuntimeFile registers a file for the given filetype
func AddRuntimeFile(fileType RTFiletype, file RuntimeFile) { func AddRuntimeFile(fileType RTFiletype, file RuntimeFile) {
allFiles[fileType] = append(allFiles[fileType], file) allFiles[fileType] = append(allFiles[fileType], file)
@ -117,7 +105,7 @@ func AddRealRuntimeFile(fileType RTFiletype, file RuntimeFile) {
// AddRuntimeFilesFromDirectory registers each file from the given directory for // AddRuntimeFilesFromDirectory registers each file from the given directory for
// the filetype which matches the file-pattern // the filetype which matches the file-pattern
func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) { func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) {
files, _ := ioutil.ReadDir(directory) files, _ := os.ReadDir(directory)
for _, f := range files { for _, f := range files {
if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok { if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok {
fullPath := filepath.Join(directory, f.Name()) fullPath := filepath.Join(directory, f.Name())
@ -136,8 +124,8 @@ func AddRuntimeFilesFromAssets(fileType RTFiletype, directory, pattern string) {
assetLoop: assetLoop:
for _, f := range files { for _, f := range files {
if ok, _ := path.Match(pattern, f); ok { if ok, _ := filepath.Match(pattern, f); ok {
af := assetFile(path.Join(directory, f)) af := assetFile(filepath.Join(directory, f))
for _, rf := range realFiles[fileType] { for _, rf := range realFiles[fileType] {
if af.Name() == rf.Name() { if af.Name() == rf.Name() {
continue assetLoop continue assetLoop
@ -178,7 +166,7 @@ func InitRuntimeFiles(user bool) {
if user { if user {
AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern) AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern)
} }
AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern) AddRuntimeFilesFromAssets(fileType, filepath.Join("runtime", dir), pattern)
} }
initRuntimeVars() initRuntimeVars()
@ -204,14 +192,14 @@ func InitPlugins() {
// Search ConfigDir for plugin-scripts // Search ConfigDir for plugin-scripts
plugdir := filepath.Join(ConfigDir, "plug") plugdir := filepath.Join(ConfigDir, "plug")
files, _ := ioutil.ReadDir(plugdir) files, _ := os.ReadDir(plugdir)
isID := regexp.MustCompile(`^[_A-Za-z0-9]+$`).MatchString isID := regexp.MustCompile(`^[_A-Za-z0-9]+$`).MatchString
for _, d := range files { for _, d := range files {
plugpath := filepath.Join(plugdir, d.Name()) plugpath := filepath.Join(plugdir, d.Name())
if stat, err := os.Stat(plugpath); err == nil && stat.IsDir() { if stat, err := os.Stat(plugpath); err == nil && stat.IsDir() {
srcs, _ := ioutil.ReadDir(plugpath) srcs, _ := os.ReadDir(plugpath)
p := new(Plugin) p := new(Plugin)
p.Name = d.Name() p.Name = d.Name()
p.DirName = d.Name() p.DirName = d.Name()
@ -219,7 +207,7 @@ func InitPlugins() {
if strings.HasSuffix(f.Name(), ".lua") { if strings.HasSuffix(f.Name(), ".lua") {
p.Srcs = append(p.Srcs, realFile(filepath.Join(plugdir, d.Name(), f.Name()))) p.Srcs = append(p.Srcs, realFile(filepath.Join(plugdir, d.Name(), f.Name())))
} else if strings.HasSuffix(f.Name(), ".json") { } else if strings.HasSuffix(f.Name(), ".json") {
data, err := ioutil.ReadFile(filepath.Join(plugdir, d.Name(), f.Name())) data, err := os.ReadFile(filepath.Join(plugdir, d.Name(), f.Name()))
if err != nil { if err != nil {
continue continue
} }
@ -311,7 +299,7 @@ func PluginAddRuntimeFile(plugin string, filetype RTFiletype, filePath string) e
if _, err := os.Stat(fullpath); err == nil { if _, err := os.Stat(fullpath); err == nil {
AddRealRuntimeFile(filetype, realFile(fullpath)) AddRealRuntimeFile(filetype, realFile(fullpath))
} else { } else {
fullpath = path.Join("runtime", "plugins", pldir, filePath) fullpath = filepath.Join("runtime", "plugins", pldir, filePath)
AddRuntimeFile(filetype, assetFile(fullpath)) AddRuntimeFile(filetype, assetFile(fullpath))
} }
return nil return nil
@ -328,7 +316,7 @@ func PluginAddRuntimeFilesFromDirectory(plugin string, filetype RTFiletype, dire
if _, err := os.Stat(fullpath); err == nil { if _, err := os.Stat(fullpath); err == nil {
AddRuntimeFilesFromDirectory(filetype, fullpath, pattern) AddRuntimeFilesFromDirectory(filetype, fullpath, pattern)
} else { } else {
fullpath = path.Join("runtime", "plugins", pldir, directory) fullpath = filepath.Join("runtime", "plugins", pldir, directory)
AddRuntimeFilesFromAssets(filetype, fullpath, pattern) AddRuntimeFilesFromAssets(filetype, fullpath, pattern)
} }
return nil return nil

View File

@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -12,8 +11,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/micro-editor/json5"
"github.com/zyedidia/glob" "github.com/zyedidia/glob"
"github.com/zyedidia/json5"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"golang.org/x/text/encoding/htmlindex" "golang.org/x/text/encoding/htmlindex"
) )
@ -29,21 +28,26 @@ var optionValidators = map[string]optionValidator{
"detectlimit": validateNonNegativeValue, "detectlimit": validateNonNegativeValue,
"encoding": validateEncoding, "encoding": validateEncoding,
"fileformat": validateChoice, "fileformat": validateChoice,
"helpsplit": validateChoice,
"matchbracestyle": validateChoice, "matchbracestyle": validateChoice,
"multiopen": validateChoice, "multiopen": validateChoice,
"pageoverlap": validateNonNegativeValue,
"reload": validateChoice, "reload": validateChoice,
"scrollmargin": validateNonNegativeValue, "scrollmargin": validateNonNegativeValue,
"scrollspeed": validateNonNegativeValue, "scrollspeed": validateNonNegativeValue,
"tabsize": validatePositiveValue, "tabsize": validatePositiveValue,
"truecolor": validateChoice,
} }
// a list of settings with pre-defined choices // a list of settings with pre-defined choices
var OptionChoices = map[string][]string{ var OptionChoices = map[string][]string{
"clipboard": {"internal", "external", "terminal"}, "clipboard": {"internal", "external", "terminal"},
"fileformat": {"unix", "dos"}, "fileformat": {"unix", "dos"},
"helpsplit": {"hsplit", "vsplit"},
"matchbracestyle": {"underline", "highlight"}, "matchbracestyle": {"underline", "highlight"},
"multiopen": {"tab", "hsplit", "vsplit"}, "multiopen": {"tab", "hsplit", "vsplit"},
"reload": {"prompt", "auto", "disabled"}, "reload": {"prompt", "auto", "disabled"},
"truecolor": {"auto", "on", "off"},
} }
// a list of settings that can be globally and locally modified and their // a list of settings that can be globally and locally modified and their
@ -66,20 +70,21 @@ var defaultCommonSettings = map[string]interface{}{
"hlsearch": false, "hlsearch": false,
"hltaberrors": false, "hltaberrors": false,
"hltrailingws": false, "hltrailingws": false,
"incsearch": true,
"ignorecase": true, "ignorecase": true,
"incsearch": true,
"indentchar": " ", "indentchar": " ",
"keepautoindent": false, "keepautoindent": false,
"matchbrace": true, "matchbrace": true,
"matchbraceleft": true, "matchbraceleft": true,
"matchbracestyle": "underline", "matchbracestyle": "underline",
"mkparents": false, "mkparents": false,
"pageoverlap": float64(2),
"permbackup": false, "permbackup": false,
"readonly": false, "readonly": false,
"relativeruler": false,
"reload": "prompt", "reload": "prompt",
"rmtrailingws": false, "rmtrailingws": false,
"ruler": true, "ruler": true,
"relativeruler": false,
"savecursor": false, "savecursor": false,
"saveundo": false, "saveundo": false,
"scrollbar": false, "scrollbar": false,
@ -89,13 +94,14 @@ var defaultCommonSettings = map[string]interface{}{
"softwrap": false, "softwrap": false,
"splitbottom": true, "splitbottom": true,
"splitright": true, "splitright": true,
"statusformatl": "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)", "statusformatl": "$(filename) $(modified)$(overwrite)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
"statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help", "statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
"statusline": true, "statusline": true,
"syntax": true, "syntax": true,
"tabmovement": false, "tabmovement": false,
"tabsize": float64(4), "tabsize": float64(4),
"tabstospaces": false, "tabstospaces": false,
"truecolor": "auto",
"useprimary": true, "useprimary": true,
"wordwrap": false, "wordwrap": false,
} }
@ -109,6 +115,7 @@ var DefaultGlobalOnlySettings = map[string]interface{}{
"divchars": "|-", "divchars": "|-",
"divreverse": true, "divreverse": true,
"fakecursor": false, "fakecursor": false,
"helpsplit": "hsplit",
"infobar": true, "infobar": true,
"keymenu": false, "keymenu": false,
"mouse": true, "mouse": true,
@ -151,6 +158,10 @@ var (
VolatileSettings map[string]bool VolatileSettings map[string]bool
) )
func writeFile(name string, txt []byte) error {
return util.SafeWrite(name, txt, false)
}
func init() { func init() {
ModifiedSettings = make(map[string]bool) ModifiedSettings = make(map[string]bool)
VolatileSettings = make(map[string]bool) VolatileSettings = make(map[string]bool)
@ -217,7 +228,7 @@ func ReadSettings() error {
parsedSettings = make(map[string]interface{}) parsedSettings = make(map[string]interface{})
filename := filepath.Join(ConfigDir, "settings.json") filename := filepath.Join(ConfigDir, "settings.json")
if _, e := os.Stat(filename); e == nil { if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename) input, err := os.ReadFile(filename)
if err != nil { if err != nil {
settingsParseError = true settingsParseError = true
return errors.New("Error reading settings.json file: " + err.Error()) return errors.New("Error reading settings.json file: " + err.Error())
@ -283,22 +294,31 @@ func InitGlobalSettings() error {
return err return err
} }
// InitLocalSettings scans the json in settings.json and sets the options locally based // UpdatePathGlobLocals scans the already parsed settings and sets the options locally
// on whether the filetype or path matches ft or glob local settings // based on whether the path matches a glob
// Must be called after ReadSettings // Must be called after ReadSettings
func InitLocalSettings(settings map[string]interface{}, path string) { func UpdatePathGlobLocals(settings map[string]interface{}, path string) {
for k, v := range parsedSettings { for k, v := range parsedSettings {
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") { if strings.HasPrefix(reflect.TypeOf(v).String(), "map") && !strings.HasPrefix(k, "ft:") {
if strings.HasPrefix(k, "ft:") { g, _ := glob.Compile(k)
if settings["filetype"].(string) == k[3:] { if g.MatchString(path) {
for k1, v1 := range v.(map[string]interface{}) { for k1, v1 := range v.(map[string]interface{}) {
settings[k1] = v1 settings[k1] = v1
}
} }
} else { }
g, _ := glob.Compile(k) }
if g.MatchString(path) { }
for k1, v1 := range v.(map[string]interface{}) { }
// UpdateFileTypeLocals scans the already parsed settings and sets the options locally
// based on whether the filetype matches to "ft:"
// Must be called after ReadSettings
func UpdateFileTypeLocals(settings map[string]interface{}, filetype string) {
for k, v := range parsedSettings {
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") && strings.HasPrefix(k, "ft:") {
if filetype == k[3:] {
for k1, v1 := range v.(map[string]interface{}) {
if k1 != "filetype" {
settings[k1] = v1 settings[k1] = v1
} }
} }
@ -342,7 +362,8 @@ func WriteSettings(filename string) error {
} }
txt, _ := json.MarshalIndent(parsedSettings, "", " ") txt, _ := json.MarshalIndent(parsedSettings, "", " ")
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644) txt = append(txt, '\n')
err = writeFile(filename, txt)
} }
return err return err
} }
@ -363,8 +384,9 @@ func OverwriteSettings(filename string) error {
} }
} }
txt, _ := json.MarshalIndent(settings, "", " ") txt, _ := json.MarshalIndent(parsedSettings, "", " ")
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644) txt = append(txt, '\n')
err = writeFile(filename, txt)
} }
return err return err
} }

View File

@ -4,11 +4,11 @@ import (
"strconv" "strconv"
runewidth "github.com/mattn/go-runewidth" runewidth "github.com/mattn/go-runewidth"
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2"
) )
// The BufWindow provides a way of displaying a certain section of a buffer. // The BufWindow provides a way of displaying a certain section of a buffer.
@ -58,9 +58,15 @@ func (w *BufWindow) SetBuffer(b *buffer.Buffer) {
if option == "softwrap" || option == "wordwrap" { if option == "softwrap" || option == "wordwrap" {
w.Relocate() w.Relocate()
for _, c := range w.Buf.GetCursors() { for _, c := range w.Buf.GetCursors() {
c.LastVisualX = c.GetVisualX() c.LastWrappedVisualX = c.GetVisualX(true)
} }
} }
if option == "diffgutter" || option == "ruler" || option == "scrollbar" ||
option == "statusline" {
w.updateDisplayInfo()
w.Relocate()
}
} }
b.GetVisualX = func(loc buffer.Loc) int { b.GetVisualX = func(loc buffer.Loc) int {
return w.VLocFromLoc(loc).VisualX return w.VLocFromLoc(loc).VisualX
@ -160,7 +166,7 @@ func (w *BufWindow) updateDisplayInfo() {
if w.bufWidth != prevBufWidth && w.Buf.Settings["softwrap"].(bool) { if w.bufWidth != prevBufWidth && w.Buf.Settings["softwrap"].(bool) {
for _, c := range w.Buf.GetCursors() { for _, c := range w.Buf.GetCursors() {
c.LastVisualX = c.GetVisualX() c.LastWrappedVisualX = c.GetVisualX(true)
} }
} }
} }
@ -238,7 +244,7 @@ func (w *BufWindow) Relocate() bool {
// horizontal relocation (scrolling) // horizontal relocation (scrolling)
if !b.Settings["softwrap"].(bool) { if !b.Settings["softwrap"].(bool) {
cx := activeC.GetVisualX() cx := activeC.GetVisualX(false)
rw := runewidth.RuneWidth(activeC.RuneUnder(activeC.X)) rw := runewidth.RuneWidth(activeC.RuneUnder(activeC.X))
if rw == 0 { if rw == 0 {
rw = 1 // tab or newline rw = 1 // tab or newline
@ -248,8 +254,8 @@ func (w *BufWindow) Relocate() bool {
w.StartCol = cx w.StartCol = cx
ret = true ret = true
} }
if cx+w.gutterOffset+rw > w.StartCol+w.Width { if cx+rw > w.StartCol+w.bufWidth {
w.StartCol = cx - w.Width + w.gutterOffset + rw w.StartCol = cx - w.bufWidth + rw
ret = true ret = true
} }
} }
@ -449,7 +455,7 @@ func (w *BufWindow) displayBuffer() {
currentLine := false currentLine := false
for _, c := range cursors { for _, c := range cursors {
if bloc.Y == c.Y && w.active { if !c.HasSelection() && bloc.Y == c.Y && w.active {
currentLine = true currentLine = true
break break
} }
@ -619,16 +625,21 @@ func (w *BufWindow) displayBuffer() {
wrap := func() { wrap := func() {
vloc.X = 0 vloc.X = 0
if w.hasMessage {
w.drawGutter(&vloc, &bloc)
}
if b.Settings["diffgutter"].(bool) {
w.drawDiffGutter(lineNumStyle, true, &vloc, &bloc)
}
// This will draw an empty line number because the current line is wrapped if vloc.Y >= 0 {
if b.Settings["ruler"].(bool) { if w.hasMessage {
w.drawLineNum(lineNumStyle, true, &vloc, &bloc) w.drawGutter(&vloc, &bloc)
}
if b.Settings["diffgutter"].(bool) {
w.drawDiffGutter(lineNumStyle, true, &vloc, &bloc)
}
// This will draw an empty line number because the current line is wrapped
if b.Settings["ruler"].(bool) {
w.drawLineNum(lineNumStyle, true, &vloc, &bloc)
}
} else {
vloc.X = w.gutterOffset
} }
} }

View File

@ -2,12 +2,12 @@ package display
import ( import (
runewidth "github.com/mattn/go-runewidth" runewidth "github.com/mattn/go-runewidth"
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/info" "github.com/zyedidia/micro/v2/internal/info"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2"
) )
type InfoWindow struct { type InfoWindow struct {

View File

@ -291,13 +291,7 @@ func (w *BufWindow) diff(s1, s2 SLoc) int {
// within the buffer boundaries. // within the buffer boundaries.
func (w *BufWindow) Scroll(s SLoc, n int) SLoc { func (w *BufWindow) Scroll(s SLoc, n int) SLoc {
if !w.Buf.Settings["softwrap"].(bool) { if !w.Buf.Settings["softwrap"].(bool) {
s.Line += n s.Line = util.Clamp(s.Line+n, 0, w.Buf.LinesNum()-1)
if s.Line < 0 {
s.Line = 0
}
if s.Line > w.Buf.LinesNum()-1 {
s.Line = w.Buf.LinesNum() - 1
}
return s return s
} }
return w.scroll(s, n) return w.scroll(s, n)

View File

@ -47,6 +47,12 @@ var statusInfo = map[string]func(*buffer.Buffer) string{
} }
return "" return ""
}, },
"overwrite": func(b *buffer.Buffer) string {
if b.OverwriteMode && !b.Type.Readonly {
return "[ovwr] "
}
return ""
},
"lines": func(b *buffer.Buffer) string { "lines": func(b *buffer.Buffer) string {
return strconv.Itoa(b.LinesNum()) return strconv.Itoa(b.LinesNum())
}, },

View File

@ -2,7 +2,7 @@ package display
import ( import (
runewidth "github.com/mattn/go-runewidth" runewidth "github.com/mattn/go-runewidth"
"github.com/zyedidia/tcell/v2" "github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"

View File

@ -1,13 +1,13 @@
package display package display
import ( import (
"github.com/micro-editor/tcell/v2"
"github.com/micro-editor/terminal"
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell" "github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2"
"github.com/zyedidia/terminal"
) )
type TermWindow struct { type TermWindow struct {

View File

@ -1,12 +1,16 @@
package info package info
import ( import (
"bytes"
"encoding/gob" "encoding/gob"
"errors"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
) )
@ -17,24 +21,23 @@ func (i *InfoBuf) LoadHistory() {
if config.GetGlobalOption("savehistory").(bool) { if config.GetGlobalOption("savehistory").(bool) {
file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", "history")) file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", "history"))
var decodedMap map[string][]string var decodedMap map[string][]string
if err == nil { if err != nil {
defer file.Close() if !errors.Is(err, fs.ErrNotExist) {
decoder := gob.NewDecoder(file) i.Error("Error loading history: ", err)
err = decoder.Decode(&decodedMap)
if err != nil {
i.Error("Error loading history:", err)
return
} }
return
}
defer file.Close()
err = gob.NewDecoder(file).Decode(&decodedMap)
if err != nil {
i.Error("Error decoding history: ", err)
return
} }
if decodedMap != nil { if decodedMap != nil {
i.History = decodedMap i.History = decodedMap
} else {
i.History = make(map[string][]string)
} }
} else {
i.History = make(map[string][]string)
} }
} }
@ -49,16 +52,18 @@ func (i *InfoBuf) SaveHistory() {
} }
} }
file, err := os.Create(filepath.Join(config.ConfigDir, "buffers", "history")) var buf bytes.Buffer
if err == nil { err := gob.NewEncoder(&buf).Encode(i.History)
defer file.Close() if err != nil {
encoder := gob.NewEncoder(file) screen.TermMessage("Error encoding history: ", err)
return
}
err = encoder.Encode(i.History) filename := filepath.Join(config.ConfigDir, "buffers", "history")
if err != nil { err = util.SafeWrite(filename, buf.Bytes(), true)
i.Error("Error saving history:", err) if err != nil {
return screen.TermMessage("Error saving history: ", err)
} return
} }
} }
} }

View File

@ -125,6 +125,7 @@ func importIo() *lua.LTable {
L.SetField(pkg, "MultiWriter", luar.New(L, io.MultiWriter)) L.SetField(pkg, "MultiWriter", luar.New(L, io.MultiWriter))
L.SetField(pkg, "NewSectionReader", luar.New(L, io.NewSectionReader)) L.SetField(pkg, "NewSectionReader", luar.New(L, io.NewSectionReader))
L.SetField(pkg, "Pipe", luar.New(L, io.Pipe)) L.SetField(pkg, "Pipe", luar.New(L, io.Pipe))
L.SetField(pkg, "ReadAll", luar.New(L, io.ReadAll))
L.SetField(pkg, "ReadAtLeast", luar.New(L, io.ReadAtLeast)) L.SetField(pkg, "ReadAtLeast", luar.New(L, io.ReadAtLeast))
L.SetField(pkg, "ReadFull", luar.New(L, io.ReadFull)) L.SetField(pkg, "ReadFull", luar.New(L, io.ReadFull))
L.SetField(pkg, "TeeReader", luar.New(L, io.TeeReader)) L.SetField(pkg, "TeeReader", luar.New(L, io.TeeReader))
@ -370,6 +371,8 @@ func importOs() *lua.LTable {
L.SetField(pkg, "PathListSeparator", luar.New(L, os.PathListSeparator)) L.SetField(pkg, "PathListSeparator", luar.New(L, os.PathListSeparator))
L.SetField(pkg, "PathSeparator", luar.New(L, os.PathSeparator)) L.SetField(pkg, "PathSeparator", luar.New(L, os.PathSeparator))
L.SetField(pkg, "Pipe", luar.New(L, os.Pipe)) L.SetField(pkg, "Pipe", luar.New(L, os.Pipe))
L.SetField(pkg, "ReadDir", luar.New(L, os.ReadDir))
L.SetField(pkg, "ReadFile", luar.New(L, os.ReadFile))
L.SetField(pkg, "Readlink", luar.New(L, os.Readlink)) L.SetField(pkg, "Readlink", luar.New(L, os.Readlink))
L.SetField(pkg, "Remove", luar.New(L, os.Remove)) L.SetField(pkg, "Remove", luar.New(L, os.Remove))
L.SetField(pkg, "RemoveAll", luar.New(L, os.RemoveAll)) L.SetField(pkg, "RemoveAll", luar.New(L, os.RemoveAll))
@ -388,6 +391,7 @@ func importOs() *lua.LTable {
L.SetField(pkg, "TempDir", luar.New(L, os.TempDir)) L.SetField(pkg, "TempDir", luar.New(L, os.TempDir))
L.SetField(pkg, "Truncate", luar.New(L, os.Truncate)) L.SetField(pkg, "Truncate", luar.New(L, os.Truncate))
L.SetField(pkg, "UserHomeDir", luar.New(L, os.UserHomeDir)) L.SetField(pkg, "UserHomeDir", luar.New(L, os.UserHomeDir))
L.SetField(pkg, "WriteFile", luar.New(L, os.WriteFile))
return pkg return pkg
} }
@ -423,7 +427,6 @@ func importPath() *lua.LTable {
func importFilePath() *lua.LTable { func importFilePath() *lua.LTable {
pkg := L.NewTable() pkg := L.NewTable()
L.SetField(pkg, "Join", luar.New(L, filepath.Join))
L.SetField(pkg, "Abs", luar.New(L, filepath.Abs)) L.SetField(pkg, "Abs", luar.New(L, filepath.Abs))
L.SetField(pkg, "Base", luar.New(L, filepath.Base)) L.SetField(pkg, "Base", luar.New(L, filepath.Base))
L.SetField(pkg, "Clean", luar.New(L, filepath.Clean)) L.SetField(pkg, "Clean", luar.New(L, filepath.Clean))

View File

@ -6,9 +6,9 @@ import (
"os" "os"
"sync" "sync"
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2"
) )
// Screen is the tcell screen we use to draw to the terminal // Screen is the tcell screen we use to draw to the terminal
@ -33,6 +33,12 @@ var lock sync.Mutex
// written to even if no event user event has occurred // written to even if no event user event has occurred
var drawChan chan bool var drawChan chan bool
// rawSeq is the list of raw escape sequences that are bound to some actions
// via keybindings and thus should be parsed by tcell. We need to register
// them in tcell every time we reinitialize the screen, so we need to remember
// them in a list
var rawSeq = make([]string, 0)
// Lock locks the screen lock // Lock locks the screen lock
func Lock() { func Lock() {
lock.Lock() lock.Lock()
@ -121,6 +127,34 @@ func SetContent(x, y int, mainc rune, combc []rune, style tcell.Style) {
} }
} }
// RegisterRawSeq registers a raw escape sequence that should be parsed by tcell
func RegisterRawSeq(r string) {
for _, seq := range rawSeq {
if seq == r {
return
}
}
rawSeq = append(rawSeq, r)
if Screen != nil {
Screen.RegisterRawSeq(r)
}
}
// UnregisterRawSeq unregisters a raw escape sequence that should be parsed by tcell
func UnregisterRawSeq(r string) {
for i, seq := range rawSeq {
if seq == r {
rawSeq[i] = rawSeq[len(rawSeq)-1]
rawSeq = rawSeq[:len(rawSeq)-1]
}
}
if Screen != nil {
Screen.UnregisterRawSeq(r)
}
}
// TempFini shuts the screen down temporarily // TempFini shuts the screen down temporarily
func TempFini() bool { func TempFini() bool {
screenWasNil := Screen == nil screenWasNil := Screen == nil
@ -150,10 +184,13 @@ func Init() error {
drawChan = make(chan bool, 8) drawChan = make(chan bool, 8)
// Should we enable true color? // Should we enable true color?
truecolor := os.Getenv("MICRO_TRUECOLOR") == "1" truecolor := config.GetGlobalOption("truecolor").(string)
if truecolor == "on" || (truecolor == "auto" && os.Getenv("MICRO_TRUECOLOR") == "1") {
if !truecolor { os.Setenv("TCELL_TRUECOLOR", "enable")
} else if truecolor == "off" {
os.Setenv("TCELL_TRUECOLOR", "disable") os.Setenv("TCELL_TRUECOLOR", "disable")
} else {
// For "auto", tcell already autodetects truecolor by default
} }
var oldTerm string var oldTerm string
@ -195,6 +232,10 @@ func Init() error {
Screen.EnableMouse() Screen.EnableMouse()
} }
for _, r := range rawSeq {
Screen.RegisterRawSeq(r)
}
return nil return nil
} }

View File

@ -78,8 +78,10 @@ func JobSpawn(cmdName string, cmdArgs []string, onStdout, onStderr, onExit func(
go func() { go func() {
// Run the process in the background and create the onExit callback // Run the process in the background and create the onExit callback
proc.Run() proc.Run()
jobFunc := JobFunction{onExit, outbuf.String(), userargs} if onExit != nil {
Jobs <- jobFunc jobFunc := JobFunction{onExit, outbuf.String(), userargs}
Jobs <- jobFunc
}
}() }()
return &Job{proc, stdin} return &Job{proc, stdin}

View File

@ -5,9 +5,9 @@ import (
"os/exec" "os/exec"
"strconv" "strconv"
"github.com/micro-editor/terminal"
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/terminal"
) )
type TermType int type TermType int

View File

@ -6,7 +6,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"net/http" "net/http"
"net/url"
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
@ -43,8 +45,44 @@ var (
Stdout *bytes.Buffer Stdout *bytes.Buffer
// Sigterm is a channel where micro exits when written // Sigterm is a channel where micro exits when written
Sigterm chan os.Signal Sigterm chan os.Signal
// To be used for fails on (over-)write with safe writes
ErrOverwrite = OverwriteError{}
) )
// To be used for file writes before umask is applied
const FileMode os.FileMode = 0666
const OverwriteFailMsg = `An error occurred while writing to the file:
%s
The file may be corrupted now. The good news is that it has been
successfully backed up. Next time you open this file with Micro,
Micro will ask if you want to recover it from the backup.
The backup path is:
%s`
// OverwriteError is a custom error to add additional information
type OverwriteError struct {
What error
BackupName string
}
func (e OverwriteError) Error() string {
return fmt.Sprintf(OverwriteFailMsg, e.What, e.BackupName)
}
func (e OverwriteError) Is(target error) bool {
return target == ErrOverwrite
}
func (e OverwriteError) Unwrap() error {
return e.What
}
func init() { func init() {
var err error var err error
SemVersion, err = semver.Make(Version) SemVersion, err = semver.Make(Version)
@ -233,18 +271,6 @@ func IsNonWordChar(r rune) bool {
return !IsWordChar(r) return !IsWordChar(r)
} }
// IsUpperWordChar returns whether or not a rune is an 'upper word character'
// Upper word characters are defined as numbers, upper-case letters or sub-word delimiters
func IsUpperWordChar(r rune) bool {
return IsUpperAlphanumeric(r) || IsSubwordDelimiter(r)
}
// IsLowerWordChar returns whether or not a rune is a 'lower word character'
// Lower word characters are defined as numbers, lower-case letters or sub-word delimiters
func IsLowerWordChar(r rune) bool {
return IsLowerAlphanumeric(r) || IsSubwordDelimiter(r)
}
// IsSubwordDelimiter returns whether or not a rune is a 'sub-word delimiter character' // IsSubwordDelimiter returns whether or not a rune is a 'sub-word delimiter character'
// i.e. is considered a part of the word and is used as a delimiter between sub-words of the word. // i.e. is considered a part of the word and is used as a delimiter between sub-words of the word.
// For now the only sub-word delimiter character is '_'. // For now the only sub-word delimiter character is '_'.
@ -332,6 +358,28 @@ func RunePos(b []byte, i int) int {
return CharacterCount(b[:i]) return CharacterCount(b[:i])
} }
// IndexAnyUnquoted returns the first position in s of a character from chars.
// Escaped (with backslash) and quoted (with single or double quotes) characters
// are ignored. Returns -1 if not successful
func IndexAnyUnquoted(s, chars string) int {
var e bool
var q rune
for i, r := range s {
if e {
e = false
} else if (q == 0 || q == '"') && r == '\\' {
e = true
} else if r == q {
q = 0
} else if q == 0 && (r == '\'' || r == '"') {
q = r
} else if q == 0 && strings.IndexRune(chars, r) >= 0 {
return i
}
}
return -1
}
// MakeRelative will attempt to make a relative path between path and base // MakeRelative will attempt to make a relative path between path and base
func MakeRelative(path, base string) (string, error) { func MakeRelative(path, base string) (string, error) {
if len(path) > 0 { if len(path) > 0 {
@ -398,8 +446,17 @@ func GetModTime(path string) (time.Time, error) {
return info.ModTime(), nil return info.ModTime(), nil
} }
// EscapePath replaces every path separator in a given path with a % func AppendBackupSuffix(path string) string {
func EscapePath(path string) string { return path + ".micro-backup"
}
// EscapePathUrl encodes the path in URL query form
func EscapePathUrl(path string) string {
return url.QueryEscape(filepath.ToSlash(path))
}
// EscapePathLegacy replaces every path separator in a given path with a %
func EscapePathLegacy(path string) string {
path = filepath.ToSlash(path) path = filepath.ToSlash(path)
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
// ':' is not valid in a path name on Windows but is ok on Unix // ':' is not valid in a path name on Windows but is ok on Unix
@ -408,6 +465,24 @@ func EscapePath(path string) string {
return strings.ReplaceAll(path, "/", "%") return strings.ReplaceAll(path, "/", "%")
} }
// DetermineEscapePath escapes a path, determining whether it should be escaped
// using URL encoding (preferred, since it encodes unambiguously) or
// legacy encoding with '%' (for backward compatibility, if the legacy-escaped
// path exists in the given directory).
func DetermineEscapePath(dir string, path string) string {
url := filepath.Join(dir, EscapePathUrl(path))
if _, err := os.Stat(url); err == nil {
return url
}
legacy := filepath.Join(dir, EscapePathLegacy(path))
if _, err := os.Stat(legacy); err == nil {
return legacy
}
return url
}
// GetLeadingWhitespace returns the leading whitespace of the given byte array // GetLeadingWhitespace returns the leading whitespace of the given byte array
func GetLeadingWhitespace(b []byte) []byte { func GetLeadingWhitespace(b []byte) []byte {
ws := []byte{} ws := []byte{}
@ -510,11 +585,6 @@ func IsAutocomplete(c rune) bool {
return c == '.' || IsWordChar(c) return c == '.' || IsWordChar(c)
} }
// ParseSpecial replaces escaped ts with '\t'.
func ParseSpecial(s string) string {
return strings.ReplaceAll(s, "\\t", "\t")
}
// String converts a byte array to a string (for lua plugins) // String converts a byte array to a string (for lua plugins)
func String(s []byte) string { func String(s []byte) string {
return string(s) return string(s)
@ -585,3 +655,77 @@ func HttpRequest(method string, url string, headers []string) (resp *http.Respon
} }
return client.Do(req) return client.Do(req)
} }
// SafeWrite writes bytes to a file in a "safe" way, preventing loss of the
// contents of the file if it fails to write the new contents.
// This means that the file is not overwritten directly but by writing to a
// temporary file first.
//
// If rename is true, write is performed atomically, by renaming the temporary
// file to the target file after the data is successfully written to the
// temporary file. This guarantees that the file will not remain in a corrupted
// state, but it also has limitations, e.g. the file should not be a symlink
// (otherwise SafeWrite silently replaces this symlink with a regular file),
// the file creation date in Linux is not preserved (since the file inode
// changes) etc. Use SafeWrite with rename=true for files that are only created
// and used by micro for its own needs and are not supposed to be used directly
// by the user.
//
// If rename is false, write is performed by overwriting the target file after
// the data is successfully written to the temporary file.
// This means that the target file may remain corrupted if overwrite fails,
// but in such case the temporary file is preserved as a backup so the file
// can be recovered later. So it is less convenient than atomic write but more
// universal. Use SafeWrite with rename=false for files that may be managed
// directly by the user, like settings.json and bindings.json.
func SafeWrite(path string, bytes []byte, rename bool) error {
var err error
if _, err = os.Stat(path); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return err
}
// Force rename for new files!
rename = true
}
var file *os.File
if !rename {
file, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, FileMode)
if err != nil {
return err
}
defer file.Close()
}
tmp := AppendBackupSuffix(path)
err = os.WriteFile(tmp, bytes, FileMode)
if err != nil {
os.Remove(tmp)
return err
}
if rename {
err = os.Rename(tmp, path)
} else {
err = file.Truncate(0)
if err == nil {
_, err = file.Write(bytes)
}
if err == nil {
err = file.Sync()
}
}
if err != nil {
if rename {
os.Remove(tmp)
} else {
err = OverwriteError{err, tmp}
}
return err
}
if !rename {
os.Remove(tmp)
}
return nil
}

View File

@ -439,11 +439,12 @@ func (n *Node) VSplit(right bool) uint64 {
} }
// unsplits the child of a split // unsplits the child of a split
func (n *Node) unsplit(i int, h bool) { func (n *Node) unsplit(i int) {
copy(n.children[i:], n.children[i+1:]) copy(n.children[i:], n.children[i+1:])
n.children[len(n.children)-1] = nil n.children[len(n.children)-1] = nil
n.children = n.children[:len(n.children)-1] n.children = n.children[:len(n.children)-1]
h := n.Kind == STVert
nonrs, numr := n.getResizeInfo(h) nonrs, numr := n.getResizeInfo(h)
if numr == 0 { if numr == 0 {
// This means that this was the last child // This means that this was the last child
@ -470,18 +471,62 @@ func (n *Node) Unsplit() bool {
ind = i ind = i
} }
} }
if n.parent.Kind == STVert { n.parent.unsplit(ind)
n.parent.unsplit(ind, true)
} else {
n.parent.unsplit(ind, false)
}
if n.parent.IsLeaf() { if n.parent.IsLeaf() {
return n.parent.Unsplit() return n.parent.Unsplit()
} }
n.parent.flatten()
return true return true
} }
// flattens the tree by removing unnecessary intermediate parents that have only one child
// and handles the side effect of it
func (n *Node) flatten() {
if n.parent == nil || len(n.children) != 1 {
return
}
ind := 0
for i, c := range n.parent.children {
if c.id == n.id {
ind = i
}
}
// Replace current node with its child node to remove chained parent
successor := n.children[0]
n.parent.children[ind] = successor
successor.parent = n.parent
// Maintain the tree in a consistent state: any child node's kind (horiz vs vert)
// should be the opposite of its parent's kind.
if successor.IsLeaf() {
successor.Kind = n.Kind
} else {
// If the successor node has children, that means it is a chained parent as well.
// Therefore it can be replaced by its own children.
origsize := len(n.parent.children)
// Let's say we have 5 children and want to replace [2] with its children [a] [b] [c]
// [0] [1] [2] [3] [4] --> [0] [1] [a] [b] [c] [3] [4]
// insertcount will be `3 - 1 = 2` in this case
insertcount := len(successor.children) - 1
n.parent.children = append(n.parent.children, make([]*Node, insertcount)...)
copy(n.parent.children[ind+insertcount+1:], n.parent.children[ind+1:origsize])
copy(n.parent.children[ind:], successor.children)
for i := 0; i < len(successor.children); i++ {
n.parent.children[ind+i].parent = n.parent
}
}
// Update propW and propH since the parent of the children has been updated,
// so the children have new siblings
n.parent.markSizes()
}
// String returns the string form of the node and all children (used for debugging) // String returns the string form of the node and all children (used for debugging)
func (n *Node) String() string { func (n *Node) String() string {
var strf func(n *Node, ident int) string var strf func(n *Node, ident int) string

View File

@ -51,19 +51,6 @@ func runePos(p int, str []byte) int {
return CharacterCount(str[:p]) return CharacterCount(str[:p])
} }
func combineLineMatch(src, dst LineMatch) LineMatch {
for k, v := range src {
if g, ok := dst[k]; ok {
if g == 0 {
dst[k] = v
}
} else {
dst[k] = v
}
}
return dst
}
// A State represents the region at the end of a line // A State represents the region at the end of a line
type State *region type State *region
@ -175,7 +162,7 @@ func (h *Highlighter) highlightRegion(highlights LineMatch, start int, canMatchE
if curRegion.group == curRegion.limitGroup || p.group == curRegion.limitGroup { if curRegion.group == curRegion.limitGroup || p.group == curRegion.limitGroup {
matches := findAllIndex(p.regex, line) matches := findAllIndex(p.regex, line)
for _, m := range matches { for _, m := range matches {
if ((endLoc == nil) || (m[0] < endLoc[0])) { if (endLoc == nil) || (m[0] < endLoc[0]) {
for i := m[0]; i < m[1]; i++ { for i := m[0]; i < m[1]; i++ {
fullHighlights[i] = p.group fullHighlights[i] = p.group
} }

View File

@ -258,7 +258,7 @@ func ParseDef(f *File, header *Header) (s *Def, err error) {
if s.rules == nil { if s.rules == nil {
// allow empty rules // allow empty rules
s.rules = new(rules) s.rules = &rules{}
} }
return s, err return s, err
@ -476,10 +476,17 @@ func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegio
r.limitGroup = r.group r.limitGroup = r.group
} }
r.rules, err = parseRules(regionInfo["rules"].([]interface{}), r) // rules are optional
if rules, ok := regionInfo["rules"]; ok {
r.rules, err = parseRules(rules.([]interface{}), r)
if err != nil {
return nil, err
}
}
if err != nil { if r.rules == nil {
return nil, err // allow empty rules
r.rules = &rules{}
} }
return r, nil return r, nil

View File

@ -24,6 +24,6 @@ color-link gutter-warning "88"
color-link cursor-line "229" color-link cursor-line "229"
#color-link color-column "196" #color-link color-column "196"
color-link current-line-number "246" color-link current-line-number "246"
color-line match-brace "230,22" color-link match-brace "230,22"
color-link tab-error "210" color-link tab-error "210"
color-link trailingws "210" color-link trailingws "210"

View File

@ -47,10 +47,7 @@ color support comes in three flavors.
displaying any colorscheme, but it should be noted that the user-configured displaying any colorscheme, but it should be noted that the user-configured
16-color palette is ignored when using true-color mode (this means the 16-color palette is ignored when using true-color mode (this means the
colors while using the terminal emulator will be slightly off). Not all colors while using the terminal emulator will be slightly off). Not all
terminals support true color but at this point most do. True color terminals support true color but at this point most do (see below).
support in micro is off by default but can be enabled by setting the
environment variable `MICRO_TRUECOLOR` to 1. In addition your terminal
must support it (usually indicated by setting `$COLORTERM` to `truecolor`).
True-color colorschemes in micro typically end with `-tc`, such as True-color colorschemes in micro typically end with `-tc`, such as
`solarized-tc`, `atom-dark`, `material-tc`, etc... If true color is not `solarized-tc`, `atom-dark`, `material-tc`, etc... If true color is not
enabled but a true color colorscheme is used, micro will do its best to enabled but a true color colorscheme is used, micro will do its best to
@ -84,11 +81,12 @@ These may vary widely based on the 16 colors selected for your terminal.
### True color ### True color
True color requires your terminal to support it. This means that the Micro enables true color support by default as long as it detects that the
environment variable `COLORTERM` should have the value `truecolor`, `24bit`, terminal supports it (which is usually indicated by the environment variable
or `24-bit`. In addition, to enable true color in micro, the environment `COLORTERM` being set to `truecolor`, `24bit` or `24-bit`). You can also force
variable `MICRO_TRUECOLOR` must be set to 1. Note that you have to create enabling it unconditionally by setting the option `truecolor` to `on` (or
and set this variable yourself. alternatively by setting the environment variable `MICRO_TRUECOLOR` to 1, which
is supported for backward compatibility).
* `solarized-tc`: this is the solarized colorscheme for true color. * `solarized-tc`: this is the solarized colorscheme for true color.
* `atom-dark`: this colorscheme is based off of Atom's "dark" colorscheme. * `atom-dark`: this colorscheme is based off of Atom's "dark" colorscheme.
@ -177,10 +175,14 @@ Here is a list of the colorscheme groups that you can use:
* todo * todo
* selection (Color of the text selection) * selection (Color of the text selection)
* statusline (Color of the statusline) * statusline (Color of the statusline)
* statusline.inactive (Color of the statusline of inactive split panes)
* statusline.suggestions (Color of the autocomplete suggestions menu)
* tabbar (Color of the tabbar that lists open files) * tabbar (Color of the tabbar that lists open files)
* tabbar.active (Color of the active tab in the tabbar)
* indent-char (Color of the character which indicates tabs if the option is * indent-char (Color of the character which indicates tabs if the option is
enabled) enabled)
* line-number * line-number
* gutter-info
* gutter-error * gutter-error
* gutter-warning * gutter-warning
* diff-added * diff-added
@ -371,7 +373,6 @@ highlighted. For example:
start: "\"" start: "\""
end: "\"" end: "\""
skip: "\\." skip: "\\."
rules: []
``` ```
#### Includes #### Includes

View File

@ -21,10 +21,16 @@ quotes here but these are not necessary when entering the command in micro.
This command will modify `bindings.json` and overwrite any bindings to This command will modify `bindings.json` and overwrite any bindings to
`key` that already exist. `key` that already exist.
* `help ['topic']`: opens the corresponding help topic. If no topic is provided * `help ['topic'] ['flags']`: opens the corresponding help topics.
opens the default help screen. Help topics are stored as `.md` files in the If no topic is provided opens the default help screen. If multiple topics are
`runtime/help` directory of the source tree, which is embedded in the final provided (separated via ` `) they are opened all as splits.
binary. Help topics are stored as `.md` files in the `runtime/help` directory of
the source tree, which is embedded in the final binary.
The `flags` are optional.
* `-hsplit`: Opens the help topic in a horizontal split
* `-vsplit`: Opens the help topic in a vertical split
The default split type is defined by the global `helpsplit` option.
* `save ['filename']`: saves the current buffer. If the file is provided it * `save ['filename']`: saves the current buffer. If the file is provided it
will 'save as' the filename. will 'save as' the filename.
@ -72,12 +78,15 @@ quotes here but these are not necessary when entering the command in micro.
command's output will be displayed in one line when it finishes running. command's output will be displayed in one line when it finishes running.
* `vsplit ['filename']`: opens a vertical split with `filename`. If no filename * `vsplit ['filename']`: opens a vertical split with `filename`. If no filename
is provided, a vertical split is opened with an empty buffer. is provided, a vertical split is opened with an empty buffer. If multiple
files are provided (separated via ` `) they are opened all as splits.
* `hsplit ['filename']`: same as `vsplit` but opens a horizontal split instead * `hsplit ['filename']`: same as `vsplit` but opens a horizontal split instead
of a vertical split. of a vertical split.
* `tab ['filename']`: opens the given file in a new tab. * `tab ['filename']`: opens the given file in a new tab. If no filename
is provided, a tab is opened with an empty buffer. If multiple files are
provided (separated via ` `) they are opened all as tabs.
* `tabmove '[-+]n'`: Moves the active tab to another slot. `n` is an integer. * `tabmove '[-+]n'`: Moves the active tab to another slot. `n` is an integer.
If `n` is prefixed with `-` or `+`, then it represents a relative position If `n` is prefixed with `-` or `+`, then it represents a relative position

View File

@ -66,7 +66,9 @@ bindings, tab is bound as
This means that if the `Autocomplete` action is successful, the chain will This means that if the `Autocomplete` action is successful, the chain will
abort. Otherwise, it will try `IndentSelection`, and if that fails too, it abort. Otherwise, it will try `IndentSelection`, and if that fails too, it
will execute `InsertTab`. will execute `InsertTab`. To use `,`, `|` or `&` in an action (as an argument
to a command, for example), escape it with `\` or wrap it in single or double
quotes.
## Binding commands ## Binding commands
@ -168,14 +170,15 @@ CursorLeft
CursorRight CursorRight
CursorStart CursorStart
CursorEnd CursorEnd
CursorToViewTop
CursorToViewCenter
CursorToViewBottom
SelectToStart SelectToStart
SelectToEnd SelectToEnd
SelectUp SelectUp
SelectDown SelectDown
SelectLeft SelectLeft
SelectRight SelectRight
SelectToStartOfText
SelectToStartOfTextToggle
WordRight WordRight
WordLeft WordLeft
SubWordRight SubWordRight
@ -184,20 +187,22 @@ SelectWordRight
SelectWordLeft SelectWordLeft
SelectSubWordRight SelectSubWordRight
SelectSubWordLeft SelectSubWordLeft
MoveLinesUp
MoveLinesDown
DeleteWordRight DeleteWordRight
DeleteWordLeft DeleteWordLeft
DeleteSubWordRight DeleteSubWordRight
DeleteSubWordLeft DeleteSubWordLeft
SelectLine SelectLine
SelectToStartOfLine SelectToStartOfLine
SelectToStartOfText
SelectToStartOfTextToggle
SelectToEndOfLine SelectToEndOfLine
ParagraphPrevious
ParagraphNext
SelectToParagraphPrevious
SelectToParagraphNext
InsertNewline InsertNewline
InsertSpace
Backspace Backspace
Delete Delete
Center
InsertTab InsertTab
Save Save
SaveAll SaveAll
@ -206,21 +211,28 @@ Find
FindLiteral FindLiteral
FindNext FindNext
FindPrevious FindPrevious
DiffPrevious
DiffNext DiffNext
DiffPrevious
Center
Undo Undo
Redo Redo
Copy Copy
CopyLine CopyLine
Cut Cut
CutLine CutLine
Duplicate
DuplicateLine DuplicateLine
DeleteLine DeleteLine
MoveLinesUp
MoveLinesDown
IndentSelection IndentSelection
OutdentSelection OutdentSelection
Autocomplete
CycleAutocompleteBack
OutdentLine OutdentLine
IndentLine IndentLine
Paste Paste
PastePrimary
SelectAll SelectAll
OpenFile OpenFile
Start Start
@ -231,33 +243,37 @@ SelectPageUp
SelectPageDown SelectPageDown
HalfPageUp HalfPageUp
HalfPageDown HalfPageDown
StartOfLine
EndOfLine
StartOfText StartOfText
StartOfTextToggle StartOfTextToggle
ParagraphPrevious StartOfLine
ParagraphNext EndOfLine
SelectToParagraphPrevious
SelectToParagraphNext
ToggleHelp ToggleHelp
ToggleKeyMenu
ToggleDiffGutter ToggleDiffGutter
ToggleRuler ToggleRuler
JumpLine ToggleHighlightSearch
UnhighlightSearch
ResetSearch ResetSearch
ClearInfo
ClearStatus ClearStatus
ShellMode ShellMode
CommandMode CommandMode
ToggleOverwriteMode
Escape
Quit Quit
QuitAll QuitAll
ForceQuit
AddTab AddTab
PreviousTab PreviousTab
NextTab NextTab
FirstTab
LastTab
NextSplit NextSplit
PreviousSplit
FirstSplit
LastSplit
Unsplit Unsplit
VSplit VSplit
HSplit HSplit
PreviousSplit
ToggleMacro ToggleMacro
PlayMacro PlayMacro
Suspend (Unix only) Suspend (Unix only)
@ -270,14 +286,25 @@ SpawnMultiCursorSelect
RemoveMultiCursor RemoveMultiCursor
RemoveAllMultiCursors RemoveAllMultiCursors
SkipMultiCursor SkipMultiCursor
None SkipMultiCursorBack
JumpToMatchingBrace JumpToMatchingBrace
Autocomplete JumpLine
Deselect
ClearInfo
None
``` ```
The `StartOfTextToggle` and `SelectToStartOfTextToggle` actions toggle between The `StartOfTextToggle` and `SelectToStartOfTextToggle` actions toggle between
jumping to the start of the text (first) and start of the line. jumping to the start of the text (first) and start of the line.
The `CutLine` action cuts the current line and adds it to the previously cut
lines in the clipboard since the last paste (rather than just replaces the
clipboard contents with this line). So you can cut multiple, not necessarily
consecutive lines to the clipboard just by pressing `Ctrl-k` multiple times,
without selecting them. If you want the more traditional behavior i.e. just
rewrite the clipboard every time, you can use `CopyLine,DeleteLine` action
instead of `CutLine`.
You can also bind some mouse actions (these must be bound to mouse buttons) You can also bind some mouse actions (these must be bound to mouse buttons)
``` ```
@ -495,23 +522,23 @@ conventions for text editing defaults.
"Alt-]": "DiffNext|CursorEnd", "Alt-]": "DiffNext|CursorEnd",
"Ctrl-z": "Undo", "Ctrl-z": "Undo",
"Ctrl-y": "Redo", "Ctrl-y": "Redo",
"Ctrl-c": "CopyLine|Copy", "Ctrl-c": "Copy|CopyLine",
"Ctrl-x": "Cut", "Ctrl-x": "Cut|CutLine",
"Ctrl-k": "CutLine", "Ctrl-k": "CutLine",
"Ctrl-d": "DuplicateLine", "Ctrl-d": "Duplicate|DuplicateLine",
"Ctrl-v": "Paste", "Ctrl-v": "Paste",
"Ctrl-a": "SelectAll", "Ctrl-a": "SelectAll",
"Ctrl-t": "AddTab", "Ctrl-t": "AddTab",
"Alt-,": "PreviousTab", "Alt-,": "PreviousTab|LastTab",
"Alt-.": "NextTab", "Alt-.": "NextTab|FirstTab",
"Home": "StartOfText", "Home": "StartOfText",
"End": "EndOfLine", "End": "EndOfLine",
"CtrlHome": "CursorStart", "CtrlHome": "CursorStart",
"CtrlEnd": "CursorEnd", "CtrlEnd": "CursorEnd",
"PageUp": "CursorPageUp", "PageUp": "CursorPageUp",
"PageDown": "CursorPageDown", "PageDown": "CursorPageDown",
"CtrlPageUp": "PreviousTab", "CtrlPageUp": "PreviousTab|LastTab",
"CtrlPageDown": "NextTab", "CtrlPageDown": "NextTab|FirstTab",
"ShiftPageUp": "SelectPageUp", "ShiftPageUp": "SelectPageUp",
"ShiftPageDown": "SelectPageDown", "ShiftPageDown": "SelectPageDown",
"Ctrl-g": "ToggleHelp", "Ctrl-g": "ToggleHelp",
@ -522,7 +549,7 @@ conventions for text editing defaults.
"Ctrl-b": "ShellMode", "Ctrl-b": "ShellMode",
"Ctrl-q": "Quit", "Ctrl-q": "Quit",
"Ctrl-e": "CommandMode", "Ctrl-e": "CommandMode",
"Ctrl-w": "NextSplit", "Ctrl-w": "NextSplit|FirstSplit",
"Ctrl-u": "ToggleMacro", "Ctrl-u": "ToggleMacro",
"Ctrl-j": "PlayMacro", "Ctrl-j": "PlayMacro",
"Insert": "ToggleOverwriteMode", "Insert": "ToggleOverwriteMode",
@ -621,8 +648,8 @@ are given below:
"Backtab": "CycleAutocompleteBack", "Backtab": "CycleAutocompleteBack",
"Ctrl-z": "Undo", "Ctrl-z": "Undo",
"Ctrl-y": "Redo", "Ctrl-y": "Redo",
"Ctrl-c": "CopyLine|Copy", "Ctrl-c": "Copy|CopyLine",
"Ctrl-x": "Cut", "Ctrl-x": "Cut|CutLine",
"Ctrl-k": "CutLine", "Ctrl-k": "CutLine",
"Ctrl-v": "Paste", "Ctrl-v": "Paste",
"Home": "StartOfTextToggle", "Home": "StartOfTextToggle",

View File

@ -79,22 +79,16 @@ Here are the available options:
default value: `0` default value: `0`
* `colorscheme`: loads the colorscheme stored in * `colorscheme`: use the given colorscheme. This setting is `global only`.
$(configDir)/colorschemes/`option`.micro, This setting is `global only`. The colorscheme can be either one of the colorschemes that micro comes with
by default (such as `default`, `solarized` or `solarized-tc`) which are
embedded in the micro binary, or a custom colorscheme stored in
`~/.config/micro/colorschemes/$(option).micro` where `$(option)` is the
option value. You can read more about micro's colorschemes and see the list
of default colorschemes in `> help colors`.
default value: `default` 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.
The colorscheme can be selected from all the files in the
~/.config/micro/colorschemes/ directory. Micro comes by default with
three colorschemes:
You can read more about micro's colorschemes in the `colors` help topic
(`help colors`).
* `cursorline`: highlight the line that the cursor is on in a different color * `cursorline`: highlight the line that the cursor is on in a different color
(the color is defined by the colorscheme you are using). (the color is defined by the colorscheme you are using).
@ -172,6 +166,13 @@ Here are the available options:
default value: `unknown`. This will be automatically overridden depending default value: `unknown`. This will be automatically overridden depending
on the file you open. on the file you open.
* `helpsplit`: sets the split type to be used by the `help` command.
Possible values:
* `vsplit`: open help in a vertical split pane
* `hsplit`: open help in a horizontal split pane
default value: `hsplit`
* `hlsearch`: highlight all instances of the searched text after a successful * `hlsearch`: highlight all instances of the searched text after a successful
search. This highlighting can be temporarily turned off via the search. This highlighting can be temporarily turned off via the
`UnhighlightSearch` action (triggered by the Esc key by default) or toggled `UnhighlightSearch` action (triggered by the Esc key by default) or toggled
@ -194,11 +195,11 @@ Here are the available options:
default value: `false` default value: `false`
* `incsearch`: enable incremental search in "Find" prompt (matching as you type). * `ignorecase`: perform case-insensitive searches.
default value: `true` default value: `true`
* `ignorecase`: perform case-insensitive searches. * `incsearch`: enable incremental search in "Find" prompt (matching as you type).
default value: `true` default value: `true`
@ -278,6 +279,23 @@ Here are the available options:
default value: `tab` default value: `tab`
* `pageoverlap`: the number of lines from the current view to keep in view
when paging up or down. If this is set to 2, for instance, and you page
down, the last two lines of the previous page will be the first two lines
of the next page.
default value: `2`
* `parsecursor`: if enabled, this will cause micro to parse filenames such as
`file.txt:10:5` as requesting to open `file.txt` with the cursor at line 10
and column 5. The column number can also be dropped to open the file at a
given line and column 0. Note that with this option enabled it is not possible
to open a file such as `file.txt:10:5`, where `:10:5` is part of the filename.
It is also possible to open a file with a certain cursor location by using the
`+LINE:COL` flag syntax. See `micro -help` for the command line options.
default value: `false`
* `paste`: treat characters sent from the terminal in a single chunk as a paste * `paste`: treat characters sent from the terminal in a single chunk as a paste
event rather than a series of manual key presses. If you are pasting using event rather than a series of manual key presses. If you are pasting using
the terminal keybinding (not `Ctrl-v`, which is micro's default paste the terminal keybinding (not `Ctrl-v`, which is micro's default paste
@ -287,16 +305,6 @@ Here are the available options:
default value: `false` default value: `false`
* `parsecursor`: if enabled, this will cause micro to parse filenames such as
file.txt:10:5 as requesting to open `file.txt` with the cursor at line 10
and column 5. The column number can also be dropped to open the file at a
given line and column 0. Note that with this option enabled it is not possible
to open a file such as `file.txt:10:5`, where `:10:5` is part of the filename.
It is also possible to open a file with a certain cursor location by using the
`+LINE:COL` flag syntax. See `micro -help` for the command line options.
default value: `false`
* `permbackup`: this option causes backups (see `backup` option) to be * `permbackup`: this option causes backups (see `backup` option) to be
permanently saved. With permanent backups, micro will not remove backups when permanently saved. With permanent backups, micro will not remove backups when
files are closed and will never apply them to existing files. Use this option files are closed and will never apply them to existing files. Use this option
@ -321,6 +329,12 @@ Here are the available options:
default value: `false` default value: `false`
* `relativeruler`: make line numbers display relatively. If set to true, all
lines except for the line that the cursor is located will display the distance
from the cursor's line.
default value: `false`
* `reload`: controls the reload behavior of the current buffer in case the file * `reload`: controls the reload behavior of the current buffer in case the file
has changed. The available options are `prompt`, `auto` & `disabled`. has changed. The available options are `prompt`, `auto` & `disabled`.
@ -338,12 +352,6 @@ Here are the available options:
default value: `true` default value: `true`
* `relativeruler`: make line numbers display relatively. If set to true, all
lines except for the line that the cursor is located will display the distance
from the cursor's line.
default value: `false`
* `savecursor`: remember where the cursor was last time the file was opened and * `savecursor`: remember where the cursor was last time the file was opened and
put it there when you open the file again. Information is saved to put it there when you open the file again. Information is saved to
`~/.config/micro/buffers/` `~/.config/micro/buffers/`
@ -401,11 +409,11 @@ Here are the available options:
* `statusformatl`: format string definition for the left-justified part of the * `statusformatl`: format string definition for the left-justified part of the
statusline. Special directives should be placed inside `$()`. Special statusline. Special directives should be placed inside `$()`. Special
directives include: `filename`, `modified`, `line`, `col`, `lines`, directives include: `filename`, `modified`, `line`, `col`, `lines`,
`percentage`, `opt`, `bind`. `percentage`, `opt`, `overwrite`, `bind`.
The `opt` and `bind` directives take either an option or an action afterward The `opt` and `bind` directives take either an option or an action afterward
and fill in the value of the option or the key bound to the action. and fill in the value of the option or the key bound to the action.
default value: `$(filename) $(modified)($(line),$(col)) $(status.paste)| default value: `$(filename) $(modified)$(overwrite)($(line),$(col)) $(status.paste)|
ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)` ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)`
* `statusformatr`: format string definition for the right-justified part of the * `statusformatr`: format string definition for the right-justified part of the
@ -427,20 +435,20 @@ Here are the available options:
default value: `true` default value: `true`
* `tabhighlight`: inverts the tab characters' (filename, save indicator, etc)
colors with respect to the tab bar.
default value: `false`
* `tabmovement`: navigate spaces at the beginning of lines as if they are tabs * `tabmovement`: navigate spaces at the beginning of lines as if they are tabs
(e.g. move over 4 spaces at once). This option only does anything if (e.g. move over 4 spaces at once). This option only does anything if
`tabstospaces` is on. `tabstospaces` is on.
default value: `false` default value: `false`
* `tabhighlight`: inverts the tab characters' (filename, save indicator, etc)
colors with respect to the tab bar.
default value: false
* `tabreverse`: reverses the tab bar colors when active. * `tabreverse`: reverses the tab bar colors when active.
default value: true default value: `true`
* `tabsize`: the size in spaces that a tab character should be displayed with. * `tabsize`: the size in spaces that a tab character should be displayed with.
@ -454,6 +462,19 @@ Here are the available options:
default value: `false` default value: `false`
* `truecolor`: controls whether micro will use true colors (24-bit colors) when
using a colorscheme with true colors, such as `solarized-tc` or `atom-dark`.
* `auto`: enable usage of true color if micro detects that it is supported by
the terminal, otherwise disable it.
* `on`: force usage of true color even if micro does not detect its support
by the terminal (of course this is not guaranteed to work well unless the
terminal actually supports true color).
* `off`: disable true color usage.
Note: The change will take effect after the next start of `micro`.
default value: `auto`
* `useprimary` (only useful on unix): defines whether or not micro will use the * `useprimary` (only useful on unix): defines whether or not micro will use the
primary clipboard to copy selections in the background. This does not affect primary clipboard to copy selections in the background. This does not affect
the normal clipboard using `Ctrl-c` and `Ctrl-v`. the normal clipboard using `Ctrl-c` and `Ctrl-v`.
@ -493,13 +514,13 @@ or disable them:
recent Git commit rather than the diff since opening the file. recent Git commit rather than the diff since opening the file.
Any option you set in the editor will be saved to the file Any option you set in the editor will be saved to the file
~/.config/micro/settings.json so, in effect, your configuration file will be `~/.config/micro/settings.json` so, in effect, your configuration file will be
created for you. If you'd like to take your configuration with you to another created for you. If you'd like to take your configuration with you to another
machine, simply copy the settings.json to the other machine. machine, simply copy the `settings.json` to the other machine.
## Settings.json file ## Settings.json file
The settings.json file should go in your configuration directory (by default The `settings.json` file should go in your configuration directory (by default
at `~/.config/micro`), and should contain only options which have been modified at `~/.config/micro`), and should contain only options which have been modified
from their default setting. Here is the full list of options in json format, from their default setting. Here is the full list of options in json format,
so that you can see what the formatting should look like. so that you can see what the formatting should look like.
@ -518,18 +539,24 @@ so that you can see what the formatting should look like.
"colorscheme": "default", "colorscheme": "default",
"comment": true, "comment": true,
"cursorline": true, "cursorline": true,
"detectlimit": 100,
"diff": true, "diff": true,
"diffgutter": false, "diffgutter": false,
"divchars": "|-", "divchars": "|-",
"divreverse": true, "divreverse": true,
"encoding": "utf-8", "encoding": "utf-8",
"eofnewline": true, "eofnewline": true,
"fakecursor": false,
"fastdirty": false, "fastdirty": false,
"fileformat": "unix", "fileformat": "unix",
"filetype": "unknown", "filetype": "unknown",
"incsearch": true,
"ftoptions": true, "ftoptions": true,
"helpsplit": "hsplit",
"hlsearch": false,
"hltaberrors": false,
"hltrailingws": false,
"ignorecase": true, "ignorecase": true,
"incsearch": true,
"indentchar": " ", "indentchar": " ",
"infobar": true, "infobar": true,
"initlua": true, "initlua": true,
@ -542,6 +569,8 @@ so that you can see what the formatting should look like.
"matchbracestyle": "underline", "matchbracestyle": "underline",
"mkparents": false, "mkparents": false,
"mouse": true, "mouse": true,
"multiopen": "tab",
"pageoverlap": 2,
"parsecursor": false, "parsecursor": false,
"paste": false, "paste": false,
"permbackup": false, "permbackup": false,
@ -551,12 +580,14 @@ so that you can see what the formatting should look like.
"pluginrepos": [], "pluginrepos": [],
"readonly": false, "readonly": false,
"relativeruler": false, "relativeruler": false,
"reload": "prompt",
"rmtrailingws": false, "rmtrailingws": false,
"ruler": true, "ruler": true,
"savecursor": false, "savecursor": false,
"savehistory": true, "savehistory": true,
"saveundo": false, "saveundo": false,
"scrollbar": false, "scrollbar": false,
"scrollbarchar": "|",
"scrollmargin": 3, "scrollmargin": 3,
"scrollspeed": 2, "scrollspeed": 2,
"smartpaste": true, "smartpaste": true,
@ -564,17 +595,18 @@ so that you can see what the formatting should look like.
"splitbottom": true, "splitbottom": true,
"splitright": true, "splitright": true,
"status": true, "status": true,
"statusformatl": "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)", "statusformatl": "$(filename) $(modified)$(overwrite)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
"statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help", "statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
"statusline": true, "statusline": true,
"sucmd": "sudo", "sucmd": "sudo",
"syntax": true, "syntax": true,
"tabmovement": false,
"tabhighlight": true, "tabhighlight": true,
"tabmovement": false,
"tabreverse": false, "tabreverse": false,
"tabsize": 4, "tabsize": 4,
"tabstospaces": false, "tabstospaces": false,
"useprimary": true, "useprimary": true,
"wordwrap": false,
"xterm": false "xterm": false
} }
``` ```

View File

@ -57,11 +57,13 @@ that micro defines:
* `deinit()`: cleanup function called when your plugin is unloaded or reloaded. * `deinit()`: cleanup function called when your plugin is unloaded or reloaded.
* `onSetActive(bufpane)`: runs when changing the currently active panel.
* `onBufferOpen(buf)`: runs when a buffer is opened. The input contains * `onBufferOpen(buf)`: runs when a buffer is opened. The input contains
the buffer object. the buffer object.
* `onBufferOptionChanged(buf, option, old, new)`: runs when an option of the
buffer has changed. The input contains the buffer object, the option name,
the old and the new value.
* `onBufPaneOpen(bufpane)`: runs when a bufpane is opened. The input * `onBufPaneOpen(bufpane)`: runs when a bufpane is opened. The input
contains the bufpane object. contains the bufpane object.
@ -350,7 +352,6 @@ The packages and their contents are listed below (in Go type signatures):
- `IsWordChar(s string) bool`: returns true if the first rune in a - `IsWordChar(s string) bool`: returns true if the first rune in a
string is a word character. string is a word character.
- `String(b []byte) string`: converts a byte array to a string. - `String(b []byte) string`: converts a byte array to a string.
- `RuneStr(r rune) string`: converts a rune to a string.
- `Unzip(src, dest string) error`: unzips a file to given folder. - `Unzip(src, dest string) error`: unzips a file to given folder.
- `Version`: micro's version number or commit hash - `Version`: micro's version number or commit hash
- `SemVersion`: micro's semantic version - `SemVersion`: micro's semantic version

View File

@ -107,7 +107,7 @@ function commentLine(bp, lineN, indentLen)
bp.Cursor.Y = curpos.Y bp.Cursor.Y = curpos.Y
end end
bp.Cursor:Relocate() bp.Cursor:Relocate()
bp.Cursor.LastVisualX = bp.Cursor:GetVisualX() bp.Cursor:StoreVisualX()
end end
function uncommentLine(bp, lineN, commentRegex) function uncommentLine(bp, lineN, commentRegex)
@ -135,7 +135,7 @@ function uncommentLine(bp, lineN, commentRegex)
end end
end end
bp.Cursor:Relocate() bp.Cursor:Relocate()
bp.Cursor.LastVisualX = bp.Cursor:GetVisualX() bp.Cursor:StoreVisualX()
end end
function toggleCommentLine(bp, lineN, commentRegex) function toggleCommentLine(bp, lineN, commentRegex)

View File

@ -9,6 +9,7 @@ following filetypes and linters:
* **c++**: g++ * **c++**: g++
* **d**: dmd * **d**: dmd
* **go**: go build * **go**: go build
* **go**: go vet
* **haskell**: hlint * **haskell**: hlint
* **java**: javac * **java**: javac
* **javascript**: jshint * **javascript**: jshint
@ -16,11 +17,16 @@ following filetypes and linters:
* **literate**: lit * **literate**: lit
* **lua**: luacheck * **lua**: luacheck
* **nim**: nim * **nim**: nim
* **nix**: nix-linter
* **objective-c**: clang * **objective-c**: clang
* **python**: pyflakes * **python**: flake8
* **python**: mypy * **python**: mypy
* **python**: pyflakes
* **python**: pylint * **python**: pylint
* **python**: ruff
* **rust**: cargo clippy
* **shell**: shfmt * **shell**: shfmt
* **shell**: shellcheck
* **swift**: swiftc (MacOS and Linux only) * **swift**: swiftc (MacOS and Linux only)
* **yaml**: yamllint * **yaml**: yamllint

View File

@ -66,7 +66,7 @@ function preinit()
end end
makeLinter("gcc", "c", "gcc", {"-fsyntax-only", "-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m") makeLinter("gcc", "c", "gcc", {"-fsyntax-only", "-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m")
makeLinter("g++", "c++", "gcc", {"-fsyntax-only","-std=c++14", "-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m") makeLinter("g++", "c++", "g++", {"-fsyntax-only","-Wall", "-Wextra", "%f"}, "%f:%l:%c:.+: %m")
makeLinter("dmd", "d", "dmd", {"-color=off", "-o-", "-w", "-wi", "-c", "%f"}, "%f%(%l%):.+: %m") makeLinter("dmd", "d", "dmd", {"-color=off", "-o-", "-w", "-wi", "-c", "%f"}, "%f%(%l%):.+: %m")
makeLinter("eslint", "javascript", "eslint", {"-f","compact","%f"}, "%f: line %l, col %c, %m") makeLinter("eslint", "javascript", "eslint", {"-f","compact","%f"}, "%f: line %l, col %c, %m")
makeLinter("gobuild", "go", "go", {"build", "-o", devnull, "%d"}, "%f:%l:%c:? %m") makeLinter("gobuild", "go", "go", {"build", "-o", devnull, "%d"}, "%f:%l:%c:? %m")
@ -82,6 +82,7 @@ function preinit()
makeLinter("pyflakes", "python", "pyflakes", {"%f"}, "%f:%l:.-:? %m") makeLinter("pyflakes", "python", "pyflakes", {"%f"}, "%f:%l:.-:? %m")
makeLinter("mypy", "python", "mypy", {"%f"}, "%f:%l: %m") makeLinter("mypy", "python", "mypy", {"%f"}, "%f:%l: %m")
makeLinter("pylint", "python", "pylint", {"--output-format=parseable", "--reports=no", "%f"}, "%f:%l: %m") makeLinter("pylint", "python", "pylint", {"--output-format=parseable", "--reports=no", "%f"}, "%f:%l: %m")
makeLinter("ruff", "python", "ruff", {"check", "--output-format=concise", "%f"}, "%f:%l:%c: %m")
makeLinter("flake8", "python", "flake8", {"%f"}, "%f:%l:%c: %m") makeLinter("flake8", "python", "flake8", {"%f"}, "%f:%l:%c: %m")
makeLinter("shfmt", "shell", "shfmt", {"%f"}, "%f:%l:%c: %m") makeLinter("shfmt", "shell", "shfmt", {"%f"}, "%f:%l:%c: %m")
makeLinter("shellcheck", "shell", "shellcheck", {"-f", "gcc", "%f"}, "%f:%l:%c:.+: %m") makeLinter("shellcheck", "shell", "shellcheck", {"-f", "gcc", "%f"}, "%f:%l:%c:.+: %m")
@ -107,26 +108,29 @@ function contains(list, element)
return false return false
end end
function checkFtMatch(ft, v)
local ftmatch = ft == v.filetype
if v.domatch then
ftmatch = string.match(ft, v.filetype)
end
local hasOS = contains(v.os, runtime.GOOS)
if not hasOS and v.whitelist then
ftmatch = false
end
if hasOS and not v.whitelist then
ftmatch = false
end
return ftmatch
end
function runLinter(buf) function runLinter(buf)
local ft = buf:FileType() local ft = buf:FileType()
local file = buf.Path local file = buf.Path
local dir = "." .. util.RuneStr(os.PathSeparator) .. filepath.Dir(file) local dir = "." .. util.RuneStr(os.PathSeparator) .. filepath.Dir(file)
for k, v in pairs(linters) do for k, v in pairs(linters) do
local ftmatch = ft == v.filetype if checkFtMatch(ft, v) then
if v.domatch then
ftmatch = string.match(ft, v.filetype)
end
local hasOS = contains(v.os, runtime.GOOS)
if not hasOS and v.whitelist then
ftmatch = false
end
if hasOS and not v.whitelist then
ftmatch = false
end
if ftmatch then
local args = {} local args = {}
for k, arg in pairs(v.args) do for k, arg in pairs(v.args) do
args[k] = arg:gsub("%%f", file):gsub("%%d", dir) args[k] = arg:gsub("%%f", file):gsub("%%d", dir)
@ -141,6 +145,19 @@ function onSave(bp)
return true return true
end end
function onBufferOptionChanged(buf, option, old, new)
if option == "filetype" then
if old ~= new then
for k, v in pairs(linters) do
if checkFtMatch(old, v) then
buf:ClearMessages(k)
end
end
end
end
return true
end
function lint(buf, linter, cmd, args, errorformat, loff, coff, callback) function lint(buf, linter, cmd, args, errorformat, loff, coff, callback)
buf:ClearMessages(linter) buf:ClearMessages(linter)

View File

@ -47,7 +47,6 @@ function onBufferOpen(buf)
syntaxFile = syntaxFile .. " - special:\n" syntaxFile = syntaxFile .. " - special:\n"
syntaxFile = syntaxFile .. " start: \"@\\\\{\"\n" syntaxFile = syntaxFile .. " start: \"@\\\\{\"\n"
syntaxFile = syntaxFile .. " end: \"\\\\}\"\n" syntaxFile = syntaxFile .. " end: \"\\\\}\"\n"
syntaxFile = syntaxFile .. " rules: []\n"
syntaxFile = syntaxFile .. " - include: " .. codetype .. "\n" syntaxFile = syntaxFile .. " - include: " .. codetype .. "\n"
config.AddRuntimeFileFromMemory(config.RTSyntax, "literate.yaml", syntaxFile) config.AddRuntimeFileFromMemory(config.RTSyntax, "literate.yaml", syntaxFile)

View File

@ -8,8 +8,10 @@ those options (`> help options`) for more information.
This plugin provides functions that can be used in the status line format: This plugin provides functions that can be used in the status line format:
* `status.branch`: returns the name of the current git branch. * `status.branch`: returns the name of the current git branch in the repository
* `status.hash`: returns the hash of the current git commit. where the file is located.
* `status.hash`: returns the hash of the current git commit in the repository
where the file is located.
* `status.paste`: returns "" if the paste option is disabled and "PASTE" * `status.paste`: returns "" if the paste option is disabled and "PASTE"
if it is enabled. if it is enabled.
* `status.lines`: returns the number of lines in the buffer. * `status.lines`: returns the number of lines in the buffer.

View File

@ -3,7 +3,10 @@ VERSION = "1.0.0"
local micro = import("micro") local micro = import("micro")
local buffer = import("micro/buffer") local buffer = import("micro/buffer")
local config = import("micro/config") local config = import("micro/config")
local shell = import("micro/shell")
local filepath = import("filepath")
local humanize = import("humanize") local humanize = import("humanize")
local strings = import("strings")
function init() function init()
micro.SetStatusInfoFn("status.branch") micro.SetStatusInfoFn("status.branch")
@ -21,7 +24,7 @@ function lines(b)
end end
function vcol(b) function vcol(b)
return tostring(b:GetActiveCursor():GetVisualX()) return tostring(b:GetActiveCursor():GetVisualX(false))
end end
function bytes(b) function bytes(b)
@ -32,30 +35,23 @@ function size(b)
return humanize.Bytes(b:Size()) return humanize.Bytes(b:Size())
end end
function branch(b) local function parseRevision(b, opt)
if b.Type.Kind ~= buffer.BTInfo then if b.Type.Kind ~= buffer.BTInfo then
local shell = import("micro/shell") local dir = filepath.Dir(b.Path)
local strings = import("strings") local str, err = shell.ExecCommand("git", "-C", dir, "rev-parse", opt, "HEAD")
local branch, err = shell.ExecCommand("git", "rev-parse", "--abbrev-ref", "HEAD")
if err == nil then if err == nil then
return strings.TrimSpace(branch) return strings.TrimSpace(str)
end end
return ""
end end
return ""
end
function branch(b)
return parseRevision(b, "--abbrev-ref")
end end
function hash(b) function hash(b)
if b.Type.Kind ~= 5 then return parseRevision(b, "--short")
local shell = import("micro/shell")
local strings = import("strings")
local hash, err = shell.ExecCommand("git", "rev-parse", "--short", "HEAD")
if err == nil then
return strings.TrimSpace(hash)
end
return ""
end
end end
function paste(b) function paste(b)

View File

@ -6,11 +6,6 @@ Each yaml file specifies how to detect the filetype based on file extension or h
In addition, a signature can be provided to help resolving ambiguities when multiple matching filetypes are detected. In addition, a signature can be provided to help resolving ambiguities when multiple matching filetypes are detected.
Then there are patterns and regions linked to highlight groups which tell micro how to highlight that filetype. Then there are patterns and regions linked to highlight groups which tell micro how to highlight that filetype.
Making your own syntax files is very simple. I recommend you check the file after you are finished with the
[`syntax_checker.go`](./syntax_checker.go) program (located in this directory). Just place your yaml syntax
file in the current directory and run `go run syntax_checker.go` and it will check every file. If there are no
errors it will print `No issues!`.
You can read more about how to write syntax files (and colorschemes) in the [colors](../help/colors.md) documentation. You can read more about how to write syntax files (and colorschemes) in the [colors](../help/colors.md) documentation.
# Legacy '.micro' filetype # Legacy '.micro' filetype
@ -38,6 +33,19 @@ Micro syntax files are almost identical to Nano's, except for some key differenc
* Micro does not use `icolor`. Instead, for a case insensitive match, use the case insensitive flag (`i`) in the regular expression * Micro does not use `icolor`. Instead, for a case insensitive match, use the case insensitive flag (`i`) in the regular expression
* For example, `icolor green ".*"` would become `color green "(?i).*"` * For example, `icolor green ".*"` would become `color green "(?i).*"`
# Incompatibilities with older versions of micro
With PR [#3458](https://github.com/zyedidia/micro/pull/3458) resp. commit
[a9b513a](https://github.com/zyedidia/micro/commit/a9b513a28adaaa7782505dc1e284e1a0132cb66f)
empty `rules: []` definitions are removed from all syntax files, since
`rules` are no longer mandatory.
Unfortunately they are mandatory for `micro` versions up to and including `v2.0.14`.
To use newer syntax definitions from this repository with older `micro` versions
you have to add these `rules: []` to all regions not including `rules` already.
Otherwise you need to use syntax definitions before the above mentioned PR
for example from version [v2.0.14](https://github.com/zyedidia/micro/tree/v2.0.14).
# Using with colorschemes # Using with colorschemes
Not all of these files have been converted to use micro's colorscheme feature. Most of them just hardcode the colors, which can be problematic depending on the colorscheme you use. Not all of these files have been converted to use micro's colorscheme feature. Most of them just hardcode the colors, which can be problematic depending on the colorscheme you use.

View File

@ -4,7 +4,7 @@ detect:
filename: "\\.(S|s|asm)$" filename: "\\.(S|s|asm)$"
rules: rules:
# This file is made for NASM assembly # This file is made mainly for NASM assembly
## Instructions ## Instructions
# x86 # x86
@ -108,3 +108,16 @@ rules:
rules: rules:
- todo: "(TODO|XXX|FIXME):?" - todo: "(TODO|XXX|FIXME):?"
## C-like comments (supported by some assemblers)
- comment:
start: "//"
end: "$"
rules:
- todo: "(TODO|XXX|FIXME):?"
- comment:
start: "/\\*"
end: "\\*/"
rules:
- todo: "(TODO|XXX|FIXME):?"

View File

@ -19,5 +19,4 @@ rules:
- comment: - comment:
start: "#" start: "#"
end: "$" end: "$"
rules: []

View File

@ -30,7 +30,6 @@ rules:
- preproc: - preproc:
start: "\\$(\\{|ENV\\{)" start: "\\$(\\{|ENV\\{)"
end: "\\}" end: "\\}"
rules: []
- identifier.macro: "\\b(APPLE|UNIX|WIN32|CYGWIN|BORLAND|MINGW|MSVC(_IDE|60|71|80|90)?)\\b" - identifier.macro: "\\b(APPLE|UNIX|WIN32|CYGWIN|BORLAND|MINGW|MSVC(_IDE|60|71|80|90)?)\\b"

View File

@ -27,12 +27,10 @@ rules:
- constant.string: - constant.string:
start: "`" start: "`"
end: "`" end: "`"
rules: []
- constant.string: - constant.string:
start: "%x\\{" start: "%x\\{"
end: "\\}" end: "\\}"
rules: []
- constant.string: - constant.string:
start: "\"" start: "\""
@ -68,5 +66,4 @@ rules:
- constant: - constant:
start: "<<-?'?EOT'?" start: "<<-?'?EOT'?"
end: "^EOT" end: "^EOT"
rules: []

File diff suppressed because one or more lines are too long

View File

@ -109,13 +109,10 @@ rules:
- comment: - comment:
start: "//" start: "//"
end: "$" end: "$"
rules: []
- comment: - comment:
start: "/\\*" start: "/\\*"
end: "\\*/" end: "\\*/"
rules: []
- comment: - comment:
start: "/\\+" start: "/\\+"
end: "\\+/" end: "\\+/"
rules: []

View File

@ -19,7 +19,6 @@ rules:
- default: - default:
start: "<%" start: "<%"
end: "%>" end: "%>"
rules: []
- preproc: "<%|%>" - preproc: "<%|%>"
- red: "&[^;[[:space:]]]*;" - red: "&[^;[[:space:]]]*;"
@ -37,6 +36,5 @@ rules:
- identifier.macro: - identifier.macro:
start: "<<-?'?EOT'?" start: "<<-?'?EOT'?"
end: "^EOT" end: "^EOT"
rules: []
- todo: "(XXX|TODO|FIXME|\\?\\?\\?)" - todo: "(XXX|TODO|FIXME|\\?\\?\\?)"

View File

@ -22,7 +22,6 @@ rules:
start: "'" start: "'"
end: "'" end: "'"
skip: "\\\\." skip: "\\\\."
rules: []
# - constant.specialChar: "%." # - constant.specialChar: "%."
# - constant.specialChar: "\\\\[abfnrtv'\\\"\\\\]" # - constant.specialChar: "\\\\[abfnrtv'\\\"\\\\]"
# - constant.specialChar: "\\\\([0-7]{3}|x[A-Fa-f0-9]{2}|u[A-Fa-f0-9]{4}|U[A-Fa-f0-9]{8})" # - constant.specialChar: "\\\\([0-7]{3}|x[A-Fa-f0-9]{2}|u[A-Fa-f0-9]{4}|U[A-Fa-f0-9]{8})"
@ -42,4 +41,3 @@ rules:
- comment: - comment:
start: "%" start: "%"
end: "$" end: "$"
rules: []

View File

@ -38,7 +38,6 @@ rules:
start: "'" start: "'"
end: "'" end: "'"
skip: "\\\\." skip: "\\\\."
rules: []
- comment: - comment:
start: "#" start: "#"

View File

@ -19,7 +19,6 @@ rules:
- constant.string: - constant.string:
start: "\\b([Ss.]\" )" start: "\\b([Ss.]\" )"
end: "\"" end: "\""
rules: []
- comment: - comment:
start: "\\(" start: "\\("

View File

@ -45,4 +45,3 @@ rules:
- comment: - comment:
start: "\\(\\*" start: "\\(\\*"
end: "\\*\\)" end: "\\*\\)"
rules: []

View File

@ -10,7 +10,6 @@ rules:
- special: - special:
start: "^```" start: "^```"
end: "^```" end: "^```"
rules: []
# heading lines # heading lines
- special: "^#{1,3}.*" - special: "^#{1,3}.*"
# unordered list items # unordered list items

View File

@ -20,4 +20,3 @@ rules:
- comment: - comment:
start: "#" start: "#"
end: "$" end: "$"
rules: []

View File

@ -22,7 +22,6 @@ rules:
- comment.line: - comment.line:
start: "^#" start: "^#"
end: "$" end: "$"
rules: []
# Diffs (i.e. git commit --verbose) # Diffs (i.e. git commit --verbose)
- default: - default:

View File

@ -11,4 +11,3 @@ rules:
- comment: - comment:
start: "#" start: "#"
end: "$" end: "$"
rules: []

View File

@ -16,4 +16,3 @@ rules:
- comment.line: - comment.line:
start: "^#" start: "^#"
end: "$" end: "$"
rules: []

View File

@ -47,7 +47,6 @@ rules:
- constant.string: - constant.string:
start: "`" start: "`"
end: "`" end: "`"
rules: []
- comment: - comment:
start: "//" start: "//"

View File

@ -44,4 +44,3 @@ rules:
- comment: - comment:
start: "#" start: "#"
end: "$" end: "$"
rules: []

View File

@ -11,7 +11,6 @@ rules:
- constant: - constant:
start: "(\\\\|\\\\\\\\)n\\[" start: "(\\\\|\\\\\\\\)n\\["
end: "]" end: "]"
rules: []
- type: "^\\.[[:space:]]*[^[[:space:]]]*" - type: "^\\.[[:space:]]*[^[[:space:]]]*"
- comment: "^\\.\\\\\".*$" - comment: "^\\.\\\\\".*$"
@ -19,12 +18,10 @@ rules:
- constant.string: - constant.string:
start: "(\\\\|\\\\\\\\)\\*\\[" start: "(\\\\|\\\\\\\\)\\*\\["
end: "]" end: "]"
rules: []
- constant.specialChar: "\\\\\\(.." - constant.specialChar: "\\\\\\(.."
- constant.specialChar: - constant.specialChar:
start: "\\\\\\[" start: "\\\\\\["
end: "]" end: "]"
rules: []
- identifier.macro: "\\\\\\\\\\$[1-9]" - identifier.macro: "\\\\\\\\\\$[1-9]"

View File

@ -75,7 +75,6 @@ rules:
- identifier: - identifier:
start: "[$][{]" start: "[$][{]"
end: "[}]" end: "[}]"
rules: []
# Triple-single-quoted strings # Triple-single-quoted strings
- constant.string: - constant.string:
@ -90,7 +89,6 @@ rules:
- constant.string: - constant.string:
start: "[$]/" start: "[$]/"
end: "/[$]" end: "/[$]"
rules: []
# Single-line comments # Single-line comments
- comment: - comment:
@ -110,4 +108,3 @@ rules:
- comment: - comment:
start: "/[*][*]@?" start: "/[*][*]@?"
end: "[*]/" end: "[*]/"
rules: []

View File

@ -4,35 +4,39 @@ detect:
filename: "\\.hs$" filename: "\\.hs$"
rules: rules:
- symbol.operator: "[!#$%&:*+/<=>?@.\\\\^\\|~\\p{Sm}\\-]+"
# Identifiers (with or without a module name)
- type: "\\b([A-Z][A-Za-z0-9_]*\\.)*[A-Z]+[A-Za-z0-9_']*\\b"
- default: "\\b([A-Z][A-Za-z0-9_]*\\.)*[a-z][A-Za-z0-9_']*\\b"
- statement: ";"
- symbol.bracket: "[\\(\\)\\[\\]\\{\\}]"
- special: "`[A-Za-z0-9']+`"
# Keywords # Keywords
- statement: "\\b(as|case|of|class|data|default|deriving|do|forall|foreign|hiding|if|then|else|import|infix|infixl|infixr|instance|let|in|mdo|module|newtype|qualified|type|where)\\b" - statement: "\\b(case|of|class|data|default|deriving|do|forall|foreign|hiding|if|then|else|import|infix|infixl|infixr|instance|let|in|mdo|module|newtype|qualified|type|where)\\b"
# Various symbols # Data constructors
- symbol: "(\\||@|!|:|_|~|=|\\\\|;|\\(\\)|,|\\[|\\]|\\{|\\})"
# Operators
- symbol.operator: "(==|/=|&&|\\|\\||<|>|<=|>=)"
# Various symbols
- special: "(->|<-)"
- symbol: "\\.|\\$"
# Data constructors
- constant.bool: "\\b(True|False)\\b" - constant.bool: "\\b(True|False)\\b"
- constant: "\\b(Nothing|Just|Left|Right|LT|EQ|GT)\\b" - constant: "\\b(Nothing|Just|Left|Right|LT|EQ|GT)\\b"
# Data classes - constant: "\\(\\)" # Unit
- identifier.class: "\\b(Read|Show|Enum|Eq|Ord|Data|Bounded|Typeable|Num|Real|Fractional|Integral|RealFrac|Floating|RealFloat|Monad|MonadPlus|Functor|Foldable|Additive|Zip)[ ]" - constant.number: "\\b(0[xX][0-9A-Fa-f]+|0[oO][0-7]+|0[bB][01]+|[-]?[0-9]+([.][0-9]+)?([eE][+-]?[0-9]+)?)\\b"
# Strings # Data classes
- identifier.class: "\\b(Additive|Applicative|Bounded|Data|Enum|Eq|Floating|Foldable|Fractional|Functor|Integral|Monad|MonadPlus|Monoid|Num|Ord|Read|Real|RealFloat|RealFrac|Semigroup|Show|Traversable|Typeable|Zip)[ ]"
# Strings
- constant.string: - constant.string:
start: "\"" start: "\""
end: "\"" end: "\""
skip: "\\\\." skip: "\\\\."
rules: rules:
- constant.specialChar: "\\\\." - special: "\\\\&"
- constant.specialChar: "\\\\([abfnrtv\"'\\\\]|[0-9]+|x[0-9a-fA-F]+|o[0-7]+|NUL|SOH|STX|ETX|EOT|ENQ|ACK|BEL|BS|HT|LF|VT|FF|CR|SO|SI|DLE|DC[1-4]|NAK|SYN|ETB|CAN|EM|SUB|ESC|FS|GS|RS|US|SP|DEL)"
# Comments # Comments
- comment: - comment:
start: "--" start: "--"
end: "$" end: "$"
@ -45,4 +49,4 @@ rules:
rules: rules:
- todo: "(TODO|XXX|FIXME):?" - todo: "(TODO|XXX|FIXME):?"
- identifier.micro: "undefined" - identifier.macro: "undefined"

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