Compare commits

...

510 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
Jöran Karl
cc195b6a96
syntax/json: Add comment support to the syntax highlighting (#3434)
* allow comments in json

* syntax/json: Add colon to `TODO` comments

---------

Co-authored-by: Sertonix <83883937+Sertonix@users.noreply.github.com>
2024-08-19 22:16:42 +02:00
stone-w4tch3r
f74eb23827
winget as windows installation in README.md (#3248)
* micro as windows installation in README.md

* suggested change

* Change urls

Co-authored-by: にてん <127052329+niten94@users.noreply.github.com>

---------

Co-authored-by: にてん <127052329+niten94@users.noreply.github.com>
2024-08-19 21:22:45 +02:00
m-kru
fa9bc6b98c
Small fix for VHDL syntax (#3375) 2024-08-19 21:11:24 +02:00
rei
63ffc40a6b
Fix Ocaml syntax highlighting (#3427)
* Fix (kinda) Ocaml syntax highlighting

* Remove functions rule

* Highlight character with escape sequence

* Add suggested changes
2024-08-19 21:03:41 +02:00
Gin
a09c98a6dc
Update micro.json (#3433)
Based on the description, the type here should be boolean rather than a string.
2024-08-19 20:52:04 +02:00
Jöran Karl
7dc78b780a
Merge pull request #3343 from JoeKar/fix/volatile-after-reinit
Rework `filetype` change, `reload` command and `autosave`
2024-08-19 19:59:08 +02:00
Jöran Karl
b80ea93486 config: Correct typo in validatePositiveValue() 2024-08-18 22:28:51 +02:00
Jöran Karl
33b7c9db7c config: Don't truncate float64 to int 2024-08-18 22:28:51 +02:00
Jöran Karl
8b31dc79bf config: Rework autosave to be rearmed upon change 2024-08-18 22:28:49 +02:00
Jöran Karl
4170df89eb config: Use float64 within the autosave processing 2024-08-18 21:10:37 +02:00
Jöran Karl
832c7deaf8 config: Remove unused GetAutoTime() 2024-08-18 21:10:37 +02:00
Jöran Karl
a678d42861 plugins/comment: Don't write to b.Settings directly
...and use `SetOptionNative()` instead.
2024-08-18 21:10:37 +02:00
Jöran Karl
0542765d95 action/command: Simplify ResetCmd
It doesn't need to loop over the DefaultCommonSettings() again,
since they're already included in the default settings and set via
SetGlobalOptionNative().
2024-08-18 21:10:37 +02:00
Jöran Karl
10511c9baf buffer/buffer: Store fileformat in LocalSettings on auto detection 2024-08-18 21:10:37 +02:00
Jöran Karl
a3071b173b action/command: Refactor reload & the filetype change 2024-08-18 21:10:37 +02:00
Jöran Karl
724e29446f action/command: On reload prevent overwriting local settings 2024-08-18 21:10:37 +02:00
Jöran Karl
05529596cf action/command: Prevent overwriting settings set locally or by plugin
- on `reload`
- on `filetype` change
2024-08-18 21:10:37 +02:00
Jöran Karl
f661b64e0a config/settings: Remove duplicating DefaultGlobalSettings()
Additionally pricise the documentation of the remaining
DefaultCommonSettings() and DefaultAllSettings() functions.
2024-08-18 21:10:37 +02:00
Jöran Karl
4663927098 config: Refactor parsed option handling
Validate the parsed options directly after read and inform about errors.
2024-08-18 21:10:37 +02:00
Jöran Karl
395d848980 buffer/settings: Prevent setting of unchanged option 2024-08-18 21:10:37 +02:00
Jöran Karl
c741e36d2d action/command: Prevent setting of unchanged option 2024-08-18 21:10:37 +02:00
Jöran Karl
521b63a28c buffer/settings: On filetype change reload parsed local settings only
Additionally keep volatile settings as they have been set by the user.
2024-08-18 21:10:37 +02:00
Jöran Karl
62c1c667b8 buffer/settings: On filetype change remove the following steps
- reload runtime files
- reset globals
- parse the settings again

since this isn't the task of a filetype change.
2024-08-18 21:10:37 +02:00
Jöran Karl
c67a30e611 action/command: On reload prevent overwriting volatile settings 2024-08-18 21:10:37 +02:00
Jöran Karl
c51f848df2 action/command: On reload prevent overwriting settings.json 2024-08-18 21:10:37 +02:00
Jöran Karl
7f6e5bc860 action/command: On reload check and inform about errors 2024-08-18 21:10:37 +02:00
Jöran Karl
9afcb80c95 action/command: Let reload really reload the settings.json 2024-08-18 21:10:37 +02:00
Dmytro Maluka
fd3a00226c
Add matchbraceleft option (#3432)
Add `matchbraceleft` option to allow disabling the default behavior
matching not just the brace under cursor but also the brace to the left
of it (which is arguably convenient, but also ambiguous and
non-intuitive). With `matchbraceleft` disabled, micro will only match
the brace character that is precisely under the cursor, and also when
jumping to the matching brace, will always move cursor precisely to the
matching brace character, not to the character next to it.

Nota bene: historical journey:

- There was already a `matchbraceleft` option introduced in commit
  ea6a87d41a, when this feature (matching brace to the left) was
  introduced first time. That time it was matching _only_ the brace
  to the left, _instead_ of the brace under the cursor, and was
  disabled by default.

- Later this feature was removed during the big refactoring of micro.

- Then this feature was reintroduced again in commit d1e713ce08, in
  its present form (i.e. combined brace matching both under the cursor
  and to the left, simulating I-beam cursor behavior), and it was
  introduced unconditionally, without an option to disable it.

- Since then, multiple users complained about this feature and asked
  for an option to disable it, so now we are reintroducing it as an
  option again (this time enabled by default though).
2024-08-18 21:08:05 +02:00
Jöran Karl
f88ac6d4fe
Merge pull request #3430 from dmaluka/calchash-fixes
Various bugfixes and improvements around buffer md5 hash calculation and `fastdirty`
2024-08-18 19:56:06 +02:00
Dmytro Maluka
0b15b57e63 buffer: Set fastdirty=true for large file when reopening
Similarly to how we force `fastdirty` to true when opening a large file
(when creating the buffer), force it also when reopening a file, in case
the file on disk became large since we opened it.
2024-08-18 15:51:47 +02:00
Dmytro Maluka
d31095fe8f buffer/settings: Add comment why do we need to zero origHash 2024-08-18 15:51:47 +02:00
Dmytro Maluka
7fe98ccfee calcHash: Remove checking file size
Let calcHash() unconditionally hash whatever buffer it is asked to hash,
and let its callers explicitly check if the buffer is too large before
calling calcHash(). This makes things simpler and less error-prone
(no extra source of truth about whether the file is too large, we don't
need to remember to check if calcHash() fails, we can be sure calcHash()
will actually update the provided hash), and actually faster (since just
calculating the buffer size, i.e. adding line lengths, is faster than
md5 calculation).

In particular, this fixes the following bugs:

1. Since ReOpen() doesn't check calcHash() return value, if the reloaded
   file is too large while the old version of the file is not,
   calcHash() returns ErrFileTooLarge and doesn't update origHash, so
   so Modified() returns true since the reloaded file's md5 sum doesn't
   match the old origHash, so micro wrongly reports the newly reloaded
   file as modified.

2. Since Modified() doesn't check calcHash() return value, Modified()
   may return false positives or false negatives if the buffer has
   *just* become too large so calcHash() returns ErrFileTooLarge and
   doesn't update `buff`.
2024-08-18 15:42:18 +02:00
Dmytro Maluka
93efc9eabe buffer/settings: Don't use Modified() before we updated origHash
When we have already enabled `fastdirty` but have not updated origHash
yet, we shouldn't use Modified() since it depends on origHash which is
still outdated, and thus returns wrong values.

This fixes the following issue: enable `fastdirty`, modify the buffer,
save the buffer and disable `fastdirty` -> micro wrongly reports the
buffer as modified (whereas it has just been saved).

Note that this fix, though, also causes a regression: e.g. if we run
`set fastdirty false` while fastdirty is already disabled, micro may
unexpectedly report a non-modified buffer as modified (in the case if
isModified is true but the buffer it actually not modified, since its
md5 sum matches and fastdirty is disabled), since this fix assumes that
since we are disabling fastdirty, it has been enabled. This shall be
fixed by PR #3343 which makes `set` do nothing if the option value
doesn't change.
2024-08-18 15:39:47 +02:00
Jöran Karl
352fd2be22 buffer/settings: On fastdirty off set to on in case of "large file"
This behavior is then aligned to the actual documentation of `fastdirty`.
Additionally set the origHash to zero in case the buffer was already modified.
2024-08-18 14:01:35 +02:00
Dmytro Maluka
c0f6b65ed6 calcHash: use correct line endings
Make calcHash() respect the buffer's file endings (unix vs dos), to make
its calculation of the file size consistent with how we calculate it in
other cases (i.e. when opening or saving the file) and with the
`fastdirty` option documentation, i.e. make calcHash() return
ErrFileTooLarge if and only if the exact file size exceeds 50KB.
2024-08-18 13:35:37 +02:00
Dmytro Maluka
e0f5361d97 calcHash: remove unneeded h.Write() error checks
According to the Go hash package documentation [1]:

type Hash interface {
	// Write (via the embedded io.Writer interface) adds more data to the running hash.
	// It never returns an error.
	io.Writer

[1] https://pkg.go.dev/hash#Hash
2024-08-18 13:35:27 +02:00
Dmytro Maluka
3737979139
Merge pull request #3416 from dmaluka/undo-cursor-fix
Fix cursor moving to an unexpected location after a redo
2024-08-15 16:07:51 +02:00
Neko Box Coder
be69b2580b
Adding missing deselect when calling RemoveAllMultiCursors (#3428)
This adds missing deselect calls that were present previously for
RemoveAllMultiCursors before PR #3352
2024-08-15 16:06:32 +02:00
Jöran Karl
21bb61c5ff
Merge pull request #3418 from JoeKar/fix/nightly-assets
nightly: Fix asset names with `nightly` as version string
2024-08-08 18:37:29 +02:00
Jöran Karl
d744872f4c nightly: Call tools/cross-compile.sh with nightly as argument 2024-08-08 06:20:01 +02:00
Jöran Karl
a9c359a719 tools/cross-compile.sh: Make VERSION configurable 2024-08-08 06:19:59 +02:00
Jöran Karl
1fae584eea tools/cross-compile.sh: Remove unused variables 2024-08-05 23:16:53 +02:00
Dmytro Maluka
0d51035acd undo/redo: Remove no longer needed teCursor temp var 2024-08-04 13:56:03 +02:00
Dmytro Maluka
658c20ff2a undo/redo: Don't change remembered cursor location
Remember the cursor location in TextEvent just once - when the original
text event happens, so that when we redo after an undo, the cursor is
placed at the location where the actual redone modification happens (as
the user would expect), not at the location where the cursor was before
the undo (which may be a completely unrelated location and may be far
away).

Fixes #3411
2024-08-04 13:43:16 +02:00
Justin Su
2259fd10af
Add Shift-Page Up/Down keybindings (#3407)
* Add Shift-Page Up/Down keybindings in defaults_other.go

* Add Shift-Page Up/Down keybindings in defaults_darwin.go

* Document Shift-Page Up/Down keybindings in keybindings.md

* Fix indentation
2024-07-31 20:35:09 +02:00
Justin Su
b8772b69c2
Use // comments for .json (#3388)
Add comments to `.json` files using `//` instead of the default `#`.

Even though JSON does not support comments, JSON5 and JSONC exist, so `//` is a much more sane default. It also improves the experience of editing micro's own config files.
2024-07-30 22:58:07 +02:00
Jöran Karl
e042bb3514
release: Use GitHub Actions for manually triggered builds and artifact uploads (#3397) 2024-07-21 13:10:26 +02:00
Dmytro Maluka
d173e527ac
Merge pull request #3352 from dmaluka/action-return-values
Improve return values of some actions + some improvements
2024-07-20 15:52:18 +02:00
hchac
f8e532b0d7
Adding selection for ParagraphPrevious and ParagraphNext. (#3353)
Also tweaked the behavior of Paragraph/{Previous/Next} so that it skips
all empty lines immediately next to cursor position, until it finds the
start/end of the paragraph closest to it. Once it finds the paragraph
closest to it, the same behavior as before applies. With the previous
behavior if the cursor was surrounded by empty lines, then
Paragraph/{Previous/Next} would only jump to the next empty line,
instead of jumping to the start/end of a paragraph.
2024-07-20 12:21:57 +02:00
Dmytro Maluka
0373a63b31 Refactor action execution code
Instead of calling execAction() and then letting it check whether it
should actually execute this action, do this check before calling
execAction(), to make the code clear and straightforward.

Precisely: for multicursor actions, call execAction() in a loop for
every cursor, but for non-multicursor actions, call execAction() just
once, without a loop. This, in particular, allows to get rid of the
hacky "c == nil" check, since we no longer iterate a slice that may
change in the meantime (since SpawnMultiCursor and RemoveMultiCursor
are non-multicursor actions and thus are no longer executed while
iterating the slice).
2024-07-18 23:54:57 +02:00
Dmytro Maluka
765889f610 Fix execution of {Spawn,Remove}MultiCursor in chained actions
- SpawnMultiCursor and RemoveMultiCursor actions change the set of
  cursors, so we cannot assume that it stays the same. So refresh the
  `cursors` list after executing every action in the chain.

- If execAction() did not execute an action since it is not a
  multicursor, it should return true, not false, to not prevent
  executing next actions in the chain.
2024-07-18 23:54:57 +02:00
Dmytro Maluka
5c8bf6b3a6 Improve RemoveAllMultiCursors behavior
Use Deselect() in order to place the cursor at the beginning of the
selection, not at the end of it, and to refresh its LastVisualX.
2024-07-18 23:54:57 +02:00
Dmytro Maluka
aa9c476b1e Improve RemoveMultiCursor behavior
If the original selection was not done by the user manually but as a
result of the initial SpawnMultiCursor, deselect this original selection
if we execute RemoveMultiCursor and there is no multicursor to remove
(i.e. the original spawned cursor is the only one). This improves user
experience by making RemoveMultiCursor behavior nicely symmetrical to
SpawnMultiCursor.
2024-07-18 23:54:57 +02:00
Dmytro Maluka
2793c37a94 Fix SkipMultiCursor behavior when there is no selection
When there is no selection (i.e. selection is empty), SkipMultiCursor
searches for the empty text, "finds" it as the beginning of the buffer,
and as a result, jumps to the beginning of the buffer, which confuses
the user. Fix it.
2024-07-18 23:54:57 +02:00
Dmytro Maluka
7e09a921e4 Make it clear that ClearStatus is the same as ClearInfo
ClearInfo and ClearStatus actions do exactly the same thing. Let's keep
them both, for compatibility reasons (who knows how many users are using
either of the two), but at least document that there is no difference
between the two.
2024-07-18 23:54:56 +02:00
Dmytro Maluka
e2e8baf4f3 Improve misc actions return values 2024-07-18 23:54:14 +02:00
Dmytro Maluka
fc5d83f6c6 Improve RemoveMultiCursor & RemoveAllMultiCursors actions return values 2024-07-18 23:54:14 +02:00
Dmytro Maluka
781f057e6f Improve Undo & Redo actions return values
Return false if there is nothing to undo/redo.

This also fixes false "Undid action" and "Redid actions" infobar
messages in the case when no action was actually undone or redone.
2024-07-18 23:54:14 +02:00
Jöran Karl
762e31f2fd
Merge pull request #3333 from masmu/feature/reset-search
Implemented `ResetSearch` and allow action chaining of `FindNext` and `FindPrevious`
2024-07-16 06:21:47 +02:00
Dmytro Maluka
1f71667616
Fix termpane not closing automatically after terminal job finished (#3386)
Fix regression caused by the fix 0de16334d3 ("micro: Don't forward
nil events into the sub event handler"): even if the terminal was
started with `wait` set to false, it is not closed immediately after
it finished its job, instead it shows "Press enter to close".

The reason is that since the commit b68461cf72 ("Terminal plugin
callback support") the termpane code has been (slightly hackily) relying
on nil events as notifications to close the terminal after it finished
its job. So fix this by introducing a separate CloseTerms() function
for notifying termpanes about that, decoupled from HandleEvent() which
is for tcell events only.
2024-07-15 09:35:50 +02:00
Neko Box Coder
a10624cc33
Fixing tabmove not working properly when there's a split in pane (#3371) 2024-07-07 10:20:22 +02:00
Massimo Mund
bbf6ec292e Implemented 'ResetSearch' to allow a search to be resetted
Added ResetSearch to the list of bindable actions in keybindings.md
2024-07-04 15:44:36 +02:00
Dmytro Maluka
dc7759204b
Merge pull request #3357 from niten94/shell-sigint-recv
Receive SIGINT only in RunInteractiveShell
2024-06-28 10:37:41 +02:00
niten94
a84aa225ab Return error with start in RunInteractiveShell
Print and return error with process start in RunInteractiveShell if
process was not able to be started. Wait until enter is pressed even if
`wait` is false.

Co-authored-by: Dmitry Maluka <dmitrymaluka@gmail.com>
2024-06-22 21:21:13 +08:00
mdom
882b98f3f1
Fix typo in README.md (#3361) 2024-06-21 16:53:25 +02:00
Jöran Karl
0fa4a3a8db
Merge pull request #3354 from JoeKar/feature/action-nightly
workflows: General improvements
2024-06-20 23:24:51 +02:00
Jöran Karl
531c7d88e2 workflow: Fetch with a fetch-depth of 0 and fetch-tags true
This will allow us to read the whole history especially all the tags.
2024-06-20 22:59:05 +02:00
Jöran Karl
c58ed0e51a workflows: Perform the setup uncached 2024-06-20 22:59:05 +02:00
Jöran Karl
f475220e67 workflows/test: Bump version of setup & checkout actions
This will correct the following warning:
"Node.js 16 actions are deprecated."
2024-06-20 22:59:05 +02:00
Jöran Karl
57375e0732 workflows/nightly: Allow manual trigger for better testability 2024-06-20 22:59:05 +02:00
Jöran Karl
d98fafd2f9 tools/build-version.go: Remove the git fetch step
build-version.go shall only provide information and not modify the repository
in which it runs.
2024-06-20 22:59:03 +02:00
Jöran Karl
f5a9744bde tools/build-version.go: Improve error verbosity 2024-06-20 22:30:00 +02:00
Borna Lang
a3e25e3701
docs: Improve plugin documentation (#3240)
* docs: Improve documentation

`commands.md`
  - documented the `reopen` command
  - improved the documentation of the `reload` command

`plugins.md`
  - added direct links to relevant external documentation pages
  - rewrote some sections
  - documented missing functions/callbacks
  - added a note about installing/managing plugins

* Omit number of default plugins

Co-authored-by: Jöran Karl <3951388+JoeKar@users.noreply.github.com>

---------

Co-authored-by: Jöran Karl <3951388+JoeKar@users.noreply.github.com>
2024-06-19 21:35:19 +02:00
Maxim Ostapenko
f0f4afa272
Added a way to install the program in ALT Linux (#3348) 2024-06-19 18:56:05 +02:00
Dmytro Maluka
3fb34cf4f2
Merge pull request #3355 from dmaluka/tab-mouse-events-fix
Fix non-working mouse events at the top line of the screen
2024-06-18 18:10:30 +02:00
niten94
f05d3582b3 Receive SIGINT only in RunInteractiveShell
Temporarily reset SIGINT signal handlers and receive SIGINT in
RunInteractiveShell. Do not try to kill the process in micro when signal
is received.
2024-06-17 19:07:10 +08:00
Dmytro Maluka
dc62dd9d82
autosave: don't save unmodified buffer (#3356)
Saving a buffer every time without even checking if it was modified
(i.e. even when the user is not editing the buffer) is wasteful,
especially if the autosave period is set to a short value.
2024-06-17 12:59:32 +02:00
niten94
26ae1b95cc Move sigterm channel to internal/util 2024-06-17 18:08:18 +08:00
Dmytro Maluka
0a6b32d775 TabList: HandleEvent: small refactoring 2024-06-16 14:02:36 +02:00
Dmytro Maluka
badaba66f3 Fix non-working mouse wheel scrolling on the top line of the screen
Scroll the tab bar only if there actually is the tab bar, otherwise
propagate the mouse wheel event to the bufpane.
2024-06-16 13:57:37 +02:00
Dmytro Maluka
602acff42f Fix non-working mouse click on top-left and top-right cells
Scroll the tab bar only if there actually is the tab bar, otherwise
propagate the mouse click event to the bufpane.
2024-06-16 13:57:21 +02:00
Jöran Karl
ced6d9487a
Merge pull request #3334 from JoeKar/feature/action-nightly
nightly: Use GitHub Actions for scheduled builds
2024-06-15 14:35:29 +02:00
Jöran Karl
c701ba66af nightly: Use GitHub Actions for scheduled builds and artifact uploads 2024-06-15 13:24:36 +02:00
Jöran Karl
fc3a5cd038 tools: Improve cross-compile.sh
1. doesn't need a given parameter to set the VERSION,
since it is determined itself
2. moves the *.deb only in case package-deb.sh succeeded
3. rename *.tar.gz to *.tgz shorten the extension to...
4. add SHA256 sums per artifact
2024-06-15 13:24:30 +02:00
Jöran Karl
c965447416 tools: Improve package-deb.sh to check for fpm first and return the result 2024-06-15 13:24:26 +02:00
Mikko
352580a50a
detect .pyw files (#3346) 2024-06-14 19:52:02 +02:00
Dmytro Maluka
bf6584739f help/keybindings: document CutLine behavior 2024-06-14 00:49:51 +02:00
Alex Rønne Petersen
55f45ce8ff
Fix highlighting of load and reference directives in Cake syntax. (#3341) 2024-06-13 19:57:39 +02:00
Bryan Honof
650c0a8db0
Update README.md to include nix install and flox install (#3340)
* Update README.md to include nix install and flox install

* Update README to indent Linux install section
2024-06-12 19:23:49 +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
Commander Nout
bad1a4b8ca
Update README.md to add brew install on MacOS (#3338)
Add installation option for Homebrew on MacOS. Homebrew is a popular package manager on MacOS - likely more popular than MacPorts.
2024-06-11 22:00:58 +02:00
Jöran Karl
5540cae610
Merge pull request #3337 from dmaluka/keyrune-usage-fix
Fix usage of tcell's `Rune()`
2024-06-10 21:01:27 +02:00
Dmytro Maluka
25c7fa55b1 De-duplicate code for KeyEvent creation 2024-06-10 02:54:37 +02:00
Dmytro Maluka
a85696d5e0 Don't use tcell's Rune() for non-KeyRune events
According to tcell documentation, Rune() should only be used for KeyRune
events. Otherwise its return value is not guaranteed and should not be
relied upon.

This fixes issue #2947: Esc key not working on Windows, since tcell
sends lone Esc key event with rune == 0 on Unix but with rune == 27
(the keycode) on Windows.
2024-06-10 02:30:55 +02:00
Alex Rønne Petersen
9face7484e
Update Zig syntax to handle ZON (Zig Object Notation) files. (#3329) 2024-06-09 17:57:22 +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
Massimo Mund
f4d576b6e0 Allow action chaining of 'FindNext' and 'FindPrevious' 2024-06-08 11:06:54 +02:00
Dmytro Maluka
9eb8782ff2
Rework FindMatchingBrace() interface and implementation (#3319)
Instead of passing a single brace pair to FindMatchingBrace(), make it
traverse all brace pairs in buffer.BracePairs on its own.

This has the following advantages:

1. Makes FindMatchingBrace() easier to use, in particular much easier
   to use from Lua.

2. Lets FindMatchingBrace() ensure that we use just one matching brace -
   the higher-priority one. This fixes the following issues:

    ([foo]bar)
     ^

when the cursor is on `[`:

- Both `[]` and `()` pairs are highlighted, whereas the expected
  behavior is that only one pair is highlighted - the one that the
  JumpToMatchingBrace action would jump to.

- JumpToMatchingBrace action incorrectly jumps to `)` instead of
  `]` (which should take higher priority in this case).

In contrast, with `((foo)bar)` it works correctly.
2024-06-05 00:56:19 +02:00
Massimo Mund
46e55c8e91
Fixed trailing line spaces being ignored by word- or subword-jumps (#3321) 2024-06-04 21:10:09 +02:00
Neko Box Coder
dd913df9e9
Reordered prompt done callback to avoid accessing out of bound history (#3318)
* Reordered prompt done callback to avoid accessing out of bound history

* Formatting
2024-06-02 20:00:13 +02:00
Jöran Karl
e9bd1b35f4
Merge pull request #3270 from niten94/sh-break-continue
Add, move commands in shell syntax file
2024-05-22 22:21:06 +02:00
niten94
4911a56181 Add commands in shell syntax file
Add `break`, `command`, `continue`, `eval`, `exec`, `getopt`, `getopts`,
`trap` and `wait` command in shell syntax file.
2024-05-23 00:59:58 +08:00
niten94
343812bd2e Change color of commands in shell syntax file
Move `local`, `read`, `shift` and `time` to "Shell commands" in shell
syntax file.
2024-05-23 00:49:13 +08:00
Jöran Karl
35630aa736
Merge pull request #2665 from masmu/feature/sub-words
Implemented sub-word cursor movement
2024-05-22 06:24:19 +02:00
Massimo Mund
78fcf2fc31 Updated WordLeft() and WordRight() behavior to be in line with SubWordLeft() and SubWordRight() 2024-05-20 23:23:33 +02:00
Massimo Mund
5dbdf8c0e8 Implemented SubWordRight, SubWordLeft, SelectSubWordRight, SelectSubWordLeft and DeleteSubWordRight, DeleteSubWordLeft 2024-05-20 23:23:33 +02:00
Massimo Mund
889a841575 Replaced IsNonAlphaNumeric() with IsNonWordChar() 2024-05-20 23:23:33 +02:00
Dmytro Maluka
917650826a
Merge pull request #3291 from dmaluka/diffgutter-cleanup
Diffgutter: simplify + fix race
2024-05-14 18:03:07 +02:00
Dmytro Maluka
b70f0eb113
Add onAnyEvent callback (#3244)
Implement a radical approach to improving abilities of plugins to detect
and handle various changes of micro's state: add onAnyEvent callback
which is called, literally, after any event. A plugin can use this
callback to compare a state after the previous event and after the
current event, and thus is able to catch various events that cannot be
detected using other callbacks.

Some examples of such events:

- change of current working directory
- switching cursor focus between a bufpane and the command bar
- change of message text in the status bar
2024-05-14 18:01:15 +02:00
Dmytro Maluka
5a159ce444 updateDiffSync(): fix potential race
When updateDiffSync() is called asynchronously, it should lock the
line array when calling Bytes(), to prevent race if the line array is
being modified by the main goroutine in the meantime.
2024-05-12 21:07:12 +02:00
Dmytro Maluka
bca35a5939 Simplify UpdateDiff() interface
The callback passed to UpdateDiff() is superfluous: in the synchronous
case screen.Redraw() is not needed anyway (since the screen is redrawn
at every iteration of the main loop), and in the asynchronous case
UpdateDiff() can just call screen.Redraw() directly.
2024-05-12 20:05:14 +02:00
Jöran Karl
1f51d0b9e2
Merge pull request #3271 from JoeKar/fix/inactive-mouse-release
Fix lost mouse release events in case the pane becomes inactive
2024-04-27 23:22:57 +02:00
Jöran Karl
0a1447b688 action: tab: Stop resize in case of mouse release while not pressed 2024-04-27 21:38:02 +02:00
Jöran Karl
2ecdac8405 action: tab: Release mouse press in case of mouse release while not pressed 2024-04-27 21:37:59 +02:00
Jöran Karl
385437d400
Merge pull request #3266 from JoeKar/fix/keysequence-comparison
bindings: Correct `KeySequenceEvent` comparison (fix crash)
2024-04-26 17:37:19 +02:00
Jöran Karl
1c35f3dc39
Merge pull request #3261 from JoeKar/fix/command-term
action: Stop processing chained actions/commands in the moment the current `Pane` is not a `BufPane` (fix crash)
2024-04-26 17:36:12 +02:00
Jöran Karl
07cda68795 initlua: Correct return type of CurPane() to be of type *BufPane 2024-04-25 23:34:39 +02:00
Jöran Karl
3919cf399f action: Provide Name() to treat TermPane as Pane
This will add the capability to address the `TermPane` within the tabs, since
the tab list only stores panes.
2024-04-25 23:34:39 +02:00
Jöran Karl
b05df07df2 bindings: Small refactoring of TryBindKey() for better readability 2024-04-25 23:21:52 +02:00
Jöran Karl
8af890a0a3 bindings: Correct KeySequenceEvent comparison
We've to iterate over the included elements,
since slices can't be simply compared with the comparison operators.
2024-04-25 23:20:30 +02:00
Dmytro Maluka
ff5b147639
Merge pull request #3267 from dmaluka/dokeyevent-improvements
Small fixes and improvements for InfoPane's key event handling
2024-04-25 21:59:44 +02:00
Dmytro Maluka
3f810c24d2
Fix Deselect() after mouse selection (#3268)
Ensure that the selection start is always before the selection end,
regardless of the direction of a mouse selection, to make
h.Cursor.Deselect() handle its `start` argument correctly.

This makes the cursor behavior after mouse selections consistent with
the cursor behavior after keyboard selections.

Fixes #3055
2024-04-25 21:58:40 +02:00
Jöran Karl
26fa15c147 action: Stop action iteration in the moment the current pane isn't a BufPane 2024-04-25 18:07:03 +02:00
Yevhen Babiichuk (DustDFG)
147943837d
Fix cursor moving down when selection exist. Solves (#3087) (#3091)
Previously `CursorDown` function called `Deselect` with a wrong
argument which lead to the situation when cursor was moved to the
start instead of the end of the selection

Signed-off-by: Yevhen Babiichuk (DustDFG) <dfgdust@gmail.com>
2024-04-25 02:27:41 +02:00
Dmytro Maluka
24406a5ae8 Comment plugin: doc: fix incorrect keybinding 2024-04-25 01:28:34 +02:00
Dmytro Maluka
8632b82cbe infopane: DoKeyEvent: it is buggy, let's add a TODO for now 2024-04-25 00:30:41 +02:00
Dmytro Maluka
fade304667 infopane: HandleEvent: refactor y/n prompt handling 2024-04-25 00:13:37 +02:00
Dmytro Maluka
5b3737fb2a infopane: HandleEvent: reset key sequence when handling y/n prompt
Fix the following buggy behavior:

1. bind "<n><a>" to the Paste action in the command bar
2. open a split pane, type some text and press Ctrl-q to close it
3. answer "n" to the "Save changes before closing?" prompt
4. press Ctrl-e to open the command prompt and press "a"

-> result: instead of inserting the "a" letter, clipboard is pasted.
2024-04-25 00:05:27 +02:00
Dmytro Maluka
36bf3f6619 DoKeyEvent: document return value
The return value of DoKeyEvent() has a dual meaning, which makes the
code not obvious and confusing. So at least document it.
2024-04-24 23:21:28 +02:00
Dmytro Maluka
8c7f63ac15 infopane: DoKeyEvent: ignore action return value
It is not really defined what is the meaning of this return value.
Currently this value is always true. And even if this value actually
meant something (for example, the result of the last executed action
in the chain), we should not use this value in HandleEvent(). The key
event handling logic should behave the same regardless of whether the
action triggered by this key succeeded or not.
2024-04-24 22:51:27 +02:00
Dmytro Maluka
18f3e1bf89
Merge pull request #3245 from dmaluka/onsetactive-fix
Fix issues with `onSetActive` callback
2024-04-23 21:28:03 +02:00
Dmytro Maluka
e48575f349
Add onBufPaneOpen error checking (#3246)
If onBufPaneOpen callback execution fails (e.g. due to a Lua runtime
error), report this error to the user, like we do for all other Lua
callbacks, rather than silently continue working as if nothing
happened.
2024-04-23 21:23:25 +02:00
Dmytro Maluka
eec068a4fc
help/colors: syntax: document default.yaml (#3262) 2024-04-23 21:21:51 +02:00
Dmytro Maluka
5510317942
Relocate buffer view when reloading file (#3250)
After reloading a file that has been externally modified, the buffer
view may become invalid: the displayed subset of lines of the file may
no longer exist, since the file may have been truncated. So relocate the
buffer view in this case.

In particular, this fixes crashes caused by out of bounds accesses to
the line array by displayBuffer() trying to display no longer existing
lines.
2024-04-21 22:49:01 +02:00
Dmytro Maluka
169a9a65fa
Merge pull request #3259 from dmaluka/default-syntax-followup
Follow-ups after adding `default.yaml` support
2024-04-21 22:48:33 +02:00
Jöran Karl
c3052b491f
parser: Check and prompt for empty patterns and region properties (fix crash) (#3256)
* parser: Precise error message for missing `start` & `end` in region

* parser: Check and prompt for empty patterns and region properties

* syntax: Remove empty identifier pattern from log definition
2024-04-21 20:13:28 +02:00
Dmytro Maluka
b929c61228 help/colors: syntax: document that nested includes are not supported 2024-04-21 15:41:49 +02:00
Dmytro Maluka
08c516c730 UpdateRules: optimize out HasIncludes() usage 2024-04-21 15:14:21 +02:00
Dmytro Maluka
1bddc8d03e UpdateRules: move include logic to a helper function 2024-04-21 15:13:03 +02:00
Jöran Karl
f9cad2e448
action: Fix the duplication of the unknown filetype (#3258) 2024-04-19 06:01:27 +02:00
Dmytro Maluka
3aed20fde9 UpdateRules: correct the comments
The "runtime" term is ambiguous: it refers to both built-in and user's
custom ("real runtime") files.
2024-04-19 00:10:58 +02:00
Dmytro Maluka
a436dae587 UpdateRules: allow includes in default.yaml 2024-04-18 23:29:33 +02:00
Dmytro Maluka
5610d01e08 UpdateRules: fix set filetype unknown
Fix `set filetype unknown` not working as expected in the following
scenario:

1. open foo.txt (no filetype detected) -> ft is `unknown`, highlighted
   with default.yaml, as expected

2. `set filetype go` -> ft is `go`, highlighted with go.yaml as expected

3. `set filetype unknown` -> ft is still `go`, still highlighted with
   go.yaml (whereas expected behavior is: ft is `unknown`, highlighted
   with default.yaml)

Fix that by always updating b.SyntaxDef value, not reusing the old one.

This also makes the code simpler and easier to understand.
2024-04-18 22:39:16 +02:00
Jöran Karl
0806addbd7
Merge pull request #2933 from JoeKar/feature/default-syntax
syntax: Provide default.yaml as fallback definition
2024-04-18 19:38:35 +02:00
Jöran Karl
6cd39efddc buffer: Refactor UpdateRules() by creating further helper functions
- `findRealRuntimeSyntaxDef()`
- `findRuntimeSyntaxDef()`

This will reduce the length of this function again and thus improves the
readability.
2024-04-18 18:33:00 +02:00
Jöran Karl
089160a7e4 buffer: Refactor UpdateRules() by creating parseDefFromFile()
This will reduce the length of this function and thus improves the
readability.
2024-04-18 18:29:52 +02:00
Jöran Karl
ed993a4021 buffer: Precise comment about searching in the internal runtime files 2024-04-18 18:20:11 +02:00
Jöran Karl
4cafa601b5 syntax: Optimize the patterns and remove the comment region 2024-04-18 18:20:11 +02:00
Jöran Karl
87ee41ab27 buffer: Don't process the default syntax in the user's custom file lookup
It needs to be processed earliest in the moment no match could be determined.
2024-04-18 18:20:08 +02:00
Kevin Klement
8d8bc58f91
Update html.yaml by adding support for dialog tags (#3255)
Another relatively new tag but commonly used
2024-04-18 14:36:19 +02:00
Jöran Karl
6ffabd626f buffer: Let the user override the default.yaml 2024-04-17 18:10:15 +02:00
Jöran Karl
2c53d1fcab test: Perform DoEvent() as long as normal or draw events are present
This is necessary since DoEvent() isn't called in a loop like in the main
application, but as one-shot only and a async draw event can lead to ignore
the explicit injected events.
Additional checks have been added to check the presence of the expected buffers.
2024-04-14 16:55:59 +02:00
Jöran Karl
f265179def buffer: Correct error message in case of failed read 2024-04-14 16:55:59 +02:00
Jöran Karl
390794213e syntax: Provide default.yaml as fallback definition 2024-04-14 16:55:59 +02:00
Jöran Karl
430da61314 highlighter: Remove EmptyDef since it's superseeded by a nil check of SyntaxDef 2024-04-14 16:55:59 +02:00
matthias314
f386b29e16
add public keyword to Julia syntax file (#3247) 2024-04-13 14:47:42 +02:00
Dmytro Maluka
4283881591 onSetActive doc: move it
Cosmetic change: move onSetActive description to keep it together with
other callbacks that are associated with bufpane, not with buffer.
2024-04-12 02:33:16 +02:00
Dmytro Maluka
186817d0c4 onSetPane doc: s/panel/bufpane/
It is pane, not panel. Also, let's call it bufpane here, like we do in
other callbacks' descriptions.
2024-04-12 02:30:56 +02:00
Dmytro Maluka
2a1790d15a Don't call onSetActive for an already active pane
Currently onSetActive is called when the user clicks with the mouse on
a pane even if this pane is already active. We should avoid calling it
in this case.

Implementation detail: like with tabs in the previous commit, we cannot
check if the pane is already active just by checking the index passed
to the Tab's SetActive() (since the index may not change while the pane
itself changes), we need to check state of the pane itself. So we move
the onSetActive invocation from the Tab's SetActive() to the BufPane's
SetActive().
2024-04-12 02:21:03 +02:00
Dmytro Maluka
c6dc5a4b1f Call onSetActive when switching to another tab
We should call the onSetActive callback not only when switching to
another bufpane within the same tab but also when switching to another
tab.

Note on implementation details:

- In SetActive() we need to check if the tab is not already active, to
  avoid calling onSetActive for an already active bufpane.

- We cannot check that just by checking if the tab index passed to
  SetActive() is different from the current active tab index, since this
  index may remain the same even if the tab itself is different (in the
  case of removing a tab from the tablist). So we need to check the tab
  itself, not just the tab index. So we introduce the isActive field,
  to track the tab's active state in the Tab structure itself.
2024-04-12 02:07:29 +02:00
Jöran Karl
426aa9bb8b
command: Prevent re-writing settings in case of local option (#3178)
* command: Prevent re-writing settings in case of local option

* command: Refactor SetGlobalOptionNative()

Co-authored-by: Dmitry Maluka <dmitrymaluka@gmail.com>

---------

Co-authored-by: Dmitry Maluka <dmitrymaluka@gmail.com>
2024-04-11 18:35:13 +02:00
Dmytro Maluka
acb0d763df
ReHighlightStates: sanity-check startline value (#3237)
Check if startline value is valid before passing it to input.State(),
to prevent a theoretically possible race when the number of lines
changes in the meantime, causing an out of bounds access.

Actually this race cannot happen: ReHighlightStates() is only called
from the main goroutine, and the line array is modified, again, only by
the main goroutine. So for now this change is rather cosmetic: it is
just to make the highligher API implementation self-sufficiently safe
without assumptions about which goroutines are using which API functions
and how.
2024-04-09 00:31:01 +02:00
lvyaoting
d1d38d1ed7
chore: fix some typos (#3239)
Signed-off-by: lvyaoting <lvyaoting@outlook.com>
2024-04-08 12:04:38 +02:00
Jöran Karl
467c71dbb8
Merge pull request #3224 from JoeKar/fix/line-synchronization
buffer: Add proper lock mechanism to lock the full `LineArray` instead of single lines
2024-04-06 00:09:17 +02:00
Jöran Karl
a3ca054371 buffer: Uncomment InitRuntimeFiles(false) in the buffer_test.go
...since we fixed the race between the syntax highlighting and the buffer
editing.
2024-04-05 14:24:59 +02:00
Jöran Karl
b6dcbfa846 highlighter: Fix race between the async highlighter and the main routine
This is achieved by the usage of the new `LineArray` locking machanism,
which prevents the interruption in the moment of modifications like insertion
or removal of lines.

Co-authored-by: Dmytro Maluka <dmitrymaluka@gmail.com>
2024-04-05 14:24:39 +02:00
Jöran Karl
6e71e37568 buffer: Rename LineBytes parameter to "lineN" to fit to the rest 2024-04-05 14:24:39 +02:00
Jöran Karl
dd7134a762 buffer: Remove superfluous rehighlight from LineArray
...which isn't used so far and probably handled better in a different way.
2024-04-05 14:24:39 +02:00
Jöran Karl
2830c4878e buffer: Lock the LineArray in case of modifications and export this lock 2024-04-05 14:24:06 +02:00
Jöran Karl
53d56d032c buffer: Remove unneeded recursion of insert()
This is necessary as a preparation to introduce a lock for the whole LineArray.
The modification can then be done without trying to lock the same lock twice.

Co-authored-by: Dmytro Maluka <dmitrymaluka@gmail.com>
2024-04-05 14:19:37 +02:00
Jöran Karl
c493e14eb4
Merge pull request #3220 from dmaluka/tests-rtfiles-fix
Don't initialize plugins and user settings in tests
2024-04-05 11:24:40 +02:00
Dmytro Maluka
69dc54b407 Temporarily don't initialize runtime files in buffer test
Adding InitRuntimeFiles() to buffer_test.go has changed the behavior
of this test: now it tests not just buffer editing per se, but also
how well buffer editing works together with syntax highlighting (since
InitRuntimeFiles() loads syntax files, and many of the test buffers
match the json header pattern in the json.yaml syntax file, so they are
"highlighted" as json). This revealed long existing races between
buffer editing and syntax highlighting.

Until we fix those races, temporarily disable InitRuntimeFiles() in this
test.
2024-04-03 04:37:44 +02:00
Dmytro Maluka
c5d32f625b Ignore user-defined runtime files in buffer test and rtfiles test
When initializing runtime files (syntax files etc) in tests, initialize
built-in runtime files only, to ensure that the tests are not affected
by whatever is in ~/.config/micro/ on the test machine.

micro_test.go already ensures that, by using its own temporary directory
as an (empty) config directory. So we only need to fix buffer_test.go
and rtfiles_test.go. In those tests, don't repeat the same dance with
a temporary directory, instead just ignore the config directory.
2024-04-03 03:44:15 +02:00
Dmytro Maluka
baca0e5cb2 Add param to InitRuntimeFiles() to init built-in files only 2024-04-03 03:41:06 +02:00
Dmytro Maluka
d67ce731ed Don't initialize plugins in buffer test and rtfiles test
Adding InitPlugins() to tests has caused noisy error logs when running
the buffer_test.go test (although the test result is still PASS):

2024/03/23 15:14:30 Plugin does not exist: autoclose at autoclose : &{autoclose autoclose <nil> [runtime/plugins/autoclose/autoclose.lua] false true}
2024/03/23 15:14:30 Plugin does not exist: comment at comment : &{comment comment <nil> [runtime/plugins/comment/comment.lua] false true}
2024/03/23 15:14:30 Plugin does not exist: diff at diff : &{diff diff <nil> [runtime/plugins/diff/diff.lua] false true}
2024/03/23 15:14:30 Plugin does not exist: ftoptions at ftoptions : &{ftoptions ftoptions <nil> [runtime/plugins/ftoptions/ftoptions.lua] false true}
...

These errors are caused simply by the fact that plugins are initialized
but not loaded. Adding config.LoadAllPlugins() to buffer_test.go "fixes"
this problem.

However, at the moment it doesn't seem a good idea to load plugins in
buffer_test.go, since buffer_test.go doesn't properly initialize Lua. It
only does ulua.L = lua.NewState() but doesn't do the other stuff that
init() in cmd/micro/initlua.go does. As a result, plugins will not be
able to do anything correctly.

So in order to initialize Lua correctly we need to be inside cmd/micro/,
so we cannot do it in buffer_test.go or any other tests except
micro_test.go.
2024-04-03 03:04:42 +02:00
Dmytro Maluka
828871acdf
Improve crontab filetype detection (#3222)
Support crontab filetype detection in the case crontab is opened via
sudoedit. Also apparently this fixes crontab filetype detection when
it is opened normally via `crontab -e` but in MacOS.

Fixes #3172
2024-04-01 19:50:42 +02:00
Dmytro Maluka
dc833d3552 Check for missing or empty filetype in syntax files
To avoid surprises like with jsonnet.
2024-03-28 01:22:25 +01:00
Dmytro Maluka
08028cf415 s/filename/filetype/ in jsonnet syntax file
This typo causes a funny bug: the autodetected filetype for *.jsonnet
files is an empty string instead of "jsonnet".

Even funnier, when autocompleting "set filetype " (with the fix from
PR #3218), the first suggested filetype is this empty string.
2024-03-28 01:22:25 +01:00
Dmytro Maluka
93dd8ca729 help/colors: s/line/file/ 2024-03-28 01:20:16 +01:00
Jöran Karl
3d7024e059
infocomplete: Complete filetypes (follow-up) (#3218)
* infocomplete: Complete filetypes (follow-up)

The first shot of the feature unfortunately completed the *.yaml file
names instead of the included filetypes. This will be corrected with
this follow up.

* infocomplete: Correct comment of filetypeComplete according to review hint

Co-authored-by: Dmytro Maluka <dmitrymaluka@gmail.com>

---------

Co-authored-by: Dmytro Maluka <dmitrymaluka@gmail.com>
2024-03-27 18:58:12 +01:00
blt-r
b291f27c3f
Add missing <release> entries in metainfo file (#3170)
* Add missing <release> entries in metainfo file

* Fix date

* Fix release date
2024-03-26 19:11:56 +01:00
Jöran Karl
3903859970
command: Add jump to perform a relative goto (#3210)
* help: Precise `goto` command documentation

* command: Add `jump` to perform a relative `goto`

* command: Refactor GotoCmd() and JumpCmd()
2024-03-25 21:16:23 +01:00
Dmytro Maluka
839e86849e
Update color groups documentation (#3203) 2024-03-25 21:06:06 +01:00
Dmytro Maluka
20bf7096b8
Make set filetype off work as expected (#3216)
Disable syntax highlighting after setting filetype to `off`.
2024-03-25 19:38:33 +01:00
Dmytro Maluka
d96f060b4c
Merge pull request #3214 from dmaluka/filetype-autocomplete-unknown
Autocomplete `unknown` value in `set filetype ...`
2024-03-25 19:38:10 +01:00
Dmytro Maluka
08892b125f
Fix crash when syntax file has no rules (#3213)
If a syntax file aaa.yaml contains no `rules` directive, then after
`set filetype aaa` micro crashes with d.rules nil pointer dereference
in HasIncludes():

Micro encountered an error: runtime.errorString runtime error: invalid memory address or nil pointer dereference
runtime/panic.go:221 (0x44c527)
runtime/panic.go:220 (0x44c4f7)
github.com/zyedidia/micro/v2/pkg/highlight/parser.go:239 (0x820919)
github.com/zyedidia/micro/v2/internal/buffer/buffer.go:830 (0x82b818)
github.com/zyedidia/micro/v2/internal/buffer/settings.go:33 (0x83b665)
github.com/zyedidia/micro/v2/internal/action/command.go:578 (0x87d75f)
github.com/zyedidia/micro/v2/internal/action/command.go:598 (0x87da79)
github.com/zyedidia/micro/v2/internal/action/command.go:634 (0x87de54)
github.com/zyedidia/micro/v2/internal/action/command.go:1030 (0x880f68)
github.com/zyedidia/micro/v2/internal/action/actions.go:1545 (0x870d72)
github.com/zyedidia/micro/v2/internal/info/infobuffer.go:152 (0x8421b4)
github.com/zyedidia/micro/v2/internal/action/infopane.go:208 (0x8854cc)
github.com/zyedidia/micro/v2/internal/action/infopane.go:54 (0x8844d6)
github.com/zyedidia/micro/v2/internal/action/infopane.go:131 (0x884d42)
github.com/zyedidia/micro/v2/internal/action/infopane.go:95 (0x8849ff)
github.com/zyedidia/micro/v2/cmd/micro/micro.go:481 (0x8bfb86)
github.com/zyedidia/micro/v2/cmd/micro/micro.go:397 (0x8bf63e)
runtime/proc.go:255 (0x438867)
runtime/asm_amd64.s:1581 (0x467a81)
2024-03-25 19:35:57 +01:00
Dmytro Maluka
838f371486
Revert "Don't expose Go timers directly to lua" (#3211)
* Revert "Don't expose Go timers directly to lua"

This reverts commit 4ffc2206ee.

Reason for revert: some plugins happen to use raw Go timers via
time.AfterFunc(), in an unsafe way (without synchronizing their
async code with micro). Let them keep doing that for now, in an
unsafe way but at least without immediate crashes.

Fixes #3209

* Add TODO about Go timers deprecation
2024-03-25 17:11:12 +01:00
Dmytro Maluka
fc7efbdbe9
Merge pull request #3212 from dmaluka/help-misc-improvements
Misc documentation improvements
2024-03-25 03:31:02 +01:00
Dmytro Maluka
2ab1b3132e
Merge pull request #3208 from dmaluka/restore-header-matches
Reintroduce `header` patterns for filetype detection
2024-03-25 03:30:32 +01:00
Dmytro Maluka
d64c9443f5 Autocomplete off value as well
It is also a documented special value of the `filetype` option.
2024-03-25 03:25:13 +01:00
Dmytro Maluka
ee6519f5cb Autocomplete unknown value in set filetype ...
`unknown` is a valid value for the `filetype` option (and executing
`set filetype unknown` does what is expected: it forces filetype
autodetection). So let's add `unknown` to the autocomplete suggestions
for `filetype`, along with actual filetypes.
2024-03-24 22:35:17 +01:00
Dmytro Maluka
984c32b513 help/colors: add break before paragraph about colorscheme includes
Make it a well-visible subsection of the "Creating a Colorscheme"
section.
2024-03-24 19:56:26 +01:00
Dmytro Maluka
1595c5ddda help/colors: remove "Syntax file headers" section
The section says that users may use their own .hdr files for their own
custom syntax files, which is simply not true.

As a matter of fact, .hdr files are an implementation detail that
doesn't need to be mentioned in the user documentation.
2024-03-24 19:50:30 +01:00
Dmytro Maluka
1021f61a81 syntax: remove some commented out garbage 2024-03-24 15:22:43 +01:00
Dmytro Maluka
053949eac6 UpdateRules: de-densify code arouns signatureMatch
Purely cosmetic change: make the code a bit more readable by reducing
its visual "density".
2024-03-24 04:47:04 +01:00
Dmytro Maluka
9ee82a6cb3 UpdateRules: rename syntaxFileBuffer to syntaxFileInfo
To make it more clear. Why Buffer?
2024-03-24 04:47:04 +01:00
Dmytro Maluka
66a3839589 Update and clarify documentation on filetype detection patterns 2024-03-24 04:47:04 +01:00
Dmytro Maluka
b2a428f1cd Restore header instead of signature in most syntax files
Turning `header` patterns into `signature` patterns in all syntax files
was a mistake. The two are different things. In almost all syntax files
those patterns are things like shebangs or <?xml ... ?> or
<!DOCTYPE html5> i.e. things that:

1. can be (and should be) used for detecting the filetype when there is
   no `filename` match (and that is actually the purpose of those
   patterns, so it's a regression that it doesn't work anymore).

2. should only occur in the first line of the file, not in the first
   100 lines or so.

In other words, the old `header` semantics was exactly what was needed
for those filetypes, while the new `signature` semantics makes little
sense for them.

So replace `signature` back with `header` in most syntax files. Keep
`signature` only in C++ and Objective-C syntax files, for which it was
actually introduced.
2024-03-24 04:47:04 +01:00
Dmytro Maluka
5492d30953 UpdateRules: add comment about the reason for signature match 2024-03-24 04:47:04 +01:00
Dmytro Maluka
6c3b5ad17c UpdateRules: refactor "header.FileType == ft" case 2024-03-24 04:47:04 +01:00
Dmytro Maluka
39e410aa46 UpdateRules: reintroduce using header regex for filetype detection
Replacing header patterns with signature patterns was a mistake, since
both are quite different from each other, and both have their uses. In
fact, this caused a serious regression: for such files as shell scripts
without *.sh extension but with #!/bin/sh inside, filetype detection
does not work at all anymore.

Since both header and signature patterns are useful, reintroduce support
for header patterns while keeping support for signature patterns as well
and make both work nicely together.

Also, unlike in the old implementation (before signatures were
introduced), ensure that filename matches take precedence over header
matches, i.e. if there is at least one filename match found, all header
matches are ignored. This makes the behavior more deterministic and
prevents previously observed issues like #2894 and #3054: wrongly
detected filetypes caused by some overly general header patterns.

Precisely, the new behavior is:

1. if there is at least one filename match, use filename matches only
2. if there are no filename matches, use header matches
3. in both cases, try to use signatures to find the best match among
multiple filename or header matches
2024-03-24 04:47:04 +01:00
Dmytro Maluka
3f4942cedb syntax parser: reintroduce header regex in .hdr files
Replacing header patterns with signature patterns was a mistake, since
both have their own uses. So restore support for header regex, while
keeping support for signature regex as well.
2024-03-24 04:47:04 +01:00
Dmytro Maluka
2b8d925925 UpdateRules: rename syntaxFiles to fnameMatches
As a preparation for reintroducing header matches.
2024-03-24 04:47:04 +01:00
Dmytro Maluka
0c923aa156 UpdateRules: don't call highlight.ParseFile() needlessly
No need to parse a syntax YAML file if we are not going to use it,
it's a waste of CPU cycles.
2024-03-24 04:47:04 +01:00
Dmytro Maluka
13483602d5 UpdateRules: fix foundDef logic
The original meaning of foundDef was: "we already found the final syntax
definition in a user's custom syntax file". After introducing signatures
its meaning became: "we found some potential syntax definition in a
user's custom syntax file, but we don't know yet if it's the final one".
This makes the code confusing and actually buggy.

At least one bug is that if we found some potential filename matches in
the user's custom syntax files, we don't search for more matches in the
built-in syntax files. Which is wrong: we should keep searching for as
many potential matches as possible, in both user's and built-in syntax
files, to select the best one among them.

Fix that by restoring the original meaning of foundDef and updating the
logic accordingly.
2024-03-24 04:47:04 +01:00
Jöran Karl
c2c2b2addf
chore: remove repetitive words (follow-up) (#3207) 2024-03-23 20:40:15 +01:00
occupyhabit
8b4e9d2c5e
chore: remove repetitive words (#3205)
Signed-off-by: occupyhabit <wangmengjiao@outlook.com>
2024-03-23 17:02:41 +01:00
Jöran Karl
a57d29ada9
command: Fix reload command to correctly initialize and reload all runtime files (#3062)
* rtfiles: Initialize all-/realFiles and Plugins in InitRuntimeFiles

* command: Reload plugins at ReloadCmd too

* command: Don't reload plugins in case of ReloadConfig()

* rtfiles: Split InitRuntimeFiles() into one func for assets and one for plugins

* rtfiles: Remove the unnecessary init function

With this modification the InitRuntimeFiles() and InitPlugins() (if needed)
must be called first, otherwise uninitialized runtime file variables are most
likely.
2024-03-22 20:47:30 +01:00
Jöran Karl
bb1f4dad77
help: Exchange all indentations to spaces, remove trailing ws and generalize indentations (#3193)
* help: Exchange all indentations to spaces and remove trailing ws

* Add some missing `` marks

Co-authored-by: Jöran Karl <3951388+JoeKar@users.noreply.github.com>

* help: Generalize indentation levels

* help: Some small visual changes

- removed some superfluous whitespaces
- add a line break in before an link
- corrected one typo

---------

Co-authored-by: Yevhen Babiichuk (DustDFG) <dfgdust@gmail.com>
2024-03-22 17:58:44 +01:00
Yevhen Babiichuk (DustDFG)
426e6c600f
Fix trailing spaces/tabs in yaml syntax files (#3200)
Signed-off-by: Yevhen Babiichuk (DustDFG) <dfgdust@gmail.com>
2024-03-22 17:56:09 +01:00
Dmytro Maluka
9ab9f8bc1c
Forward resize event to both TabList and InfoBar (#3179)
InfoBar should really receive the resize event, to know the window width
in order to do horizontal scrolling of the command line when it doesn't
fit in the screen. Although currently it doesn't scroll the command line
at all (see issue #2527) and just ignores the resize event, but we
should fix that anyway, so let's forward the resize event to it.
2024-03-21 21:40:22 +01:00
Mikko
f15db6aa30
improve Rust raw string literal highlighting (#3192) 2024-03-21 21:34:54 +01:00
Jöran Karl
4895a29be2
colorscheme: Add capability to include schemes (#2844) 2024-03-21 18:37:51 +01:00
Yevhen Babiichuk (DustDFG)
b518bda50c
Dont highlight tab/space errors in the BTHelp buffers (#3189)
Signed-off-by: Yevhen Babiichuk (DustDFG) <dfgdust@gmail.com>
2024-03-19 16:22:28 +01:00
Jöran Karl
c64add289b
command: Fix replace to be able to insert '$' (#2954)
* command: Fix replace to be able to insert '$'

* help: commands: Precise the documentation of `replace`

* help: commands: Further improvement suggested within the review

Co-authored-by: Beni Cherniavsky-Paskin <cben@redhat.com>

* Fix replace with '$' in a more kosher way

On top of JoeKar's fix.

---------

Co-authored-by: Beni Cherniavsky-Paskin <cben@redhat.com>
Co-authored-by: Dmytro Maluka <dmitrymaluka@gmail.com>
2024-03-17 21:37:16 +01:00
Paulo S. Costa
5ae2799b70
Color material-tc scrollbar (#1838)
* Color material-tc scrollbar

* Update scrollbar color to correct name
2024-03-17 17:57:21 +01:00
Suhaas Joshi
16e38b988c
cmd: Fix typo in the plugin line of "micro --help" (#2594)
fixes #2582
2024-03-17 16:48:41 +01:00
Lizzy Fleckenstein
8a3d83f7c7
Add support for rust async/await keywords (#2556) 2024-03-17 16:47:46 +01:00
Dmytro Maluka
55b251ffee Revert "command: Add capability to use relative numbers in goto (#2985)"
This reverts commit ca3a9d0794.
2024-03-17 16:39:47 +01:00
Jöran Karl
8724709cf9
Reduce the available string option validators and add autocompletion for them (#3021)
* settings: Move all options to the start of the file

This will help with the overview of all available options and their optional
validators.

* settings: Add generic string option validator

* settings: Autocomplete string options
2024-03-15 22:20:39 +01:00
Jöran Karl
4a53419c62
option: Don't apply rmtrailingws in case of timed autosave (#2850) 2024-03-15 18:46:51 +01:00
cyqsimon
8af304cc21
Update OSC52 info for Alacritty (#3174) 2024-03-15 18:43:34 +01:00
Dmytro Maluka
399134fe5b Escape regex in pre-filled search pattern in Find prompt
Fixes #3177
2024-03-15 12:25:39 +01:00
Dmytro Maluka
db26b5fee5 Add TODO about mysterious behavior of ResizeSplit() 2024-03-14 05:26:34 +01:00
Dmytro Maluka
cb903f414c
Remove unused autosave channel leftover (#3024)
config.Autosave channel is used instead.
2024-03-14 04:58:45 +01:00
Dmytro Maluka
0a69cc68dc
Merge pull request #3023 from dmaluka/timerchan
Rework lua timers and remove lua.Lock
2024-03-14 04:58:19 +01:00
Dmytro Maluka
1d1b363fa7 Remove lua.Lock
Exposing locking primitives to lua plugins is tricky and may lead to
deadlocks. Instead, if possible, it's better to ensure all the needed
synchonization in micro itself, without leaving this burden to lua code.

Since we've added micro.After() timer API and removed exposing Go timers
directly to lua, now we (probably?) have no cases of lua code possibly
running asynchronously without micro controlling when it is running. So
now we can remove lua.Lock.

This means breaking compatibility, but, until recently lua.Lock wasn't
workable at all (see #2945), which suggests that it has never been
really used by anyone. So it should be safe to remove it.
2024-03-14 04:53:56 +01:00
Dmytro Maluka
4ffc2206ee Don't expose Go timers directly to lua
Since we now expose our own micro.After() API which is more convenient
and safer to use than directly using Go timers, we can remove exposing
Go timers to lua directly.
2024-03-14 04:52:59 +01:00
Dmytro Maluka
9089e9ec83 Add micro's own lua timer function micro.After()
Directly using Go's time.AfterFunc() from lua is tricky. First, it
requires the lua timer callback to explicitly lock ulua.Lock to prevent
races. Second, it requires the lua timer callback to explicitly redraw
the screen if the callback changes the screen contents (see #2923).

So instead provide micro's own timer API which ensures both
synchronization and redrawing on its own, instead of leaving this burden
to lua code. In fact, its implementation runs the lua timer callback in
the main micro's goroutine (i.e. from micro's perspective it is
synchronous, not asynchronous), so both redrawing and synchronization
are ensured automatically.

Fixes #2923
2024-03-14 04:52:59 +01:00
Dmytro Maluka
c24604d1ab
Fix overwriting persistent non-default settings with temporary default settings (#3010)
Passing options via micro -option=value in the command line should only
temporarily override the option value for the current micro session,
not change it permanently in settings.json. But currently it wrongly
writes it to settings.json in the case when the value passed via command
line is the default value of this option, while the current permanent
setting in settings.json is a non-default value.

Fixes #3005
2024-03-14 04:43:40 +01:00
Dmytro Maluka
606bcecf03
Merge pull request #3009 from dmaluka/fix-unneeded-settings-write
Fix unneeded rewriting of settings.json
2024-03-14 04:40:34 +01:00
Alan
94af5f13bd
Update commands.md (#2966)
* Update commands.md

removed question marks since they're not meant to be typed and their meaning is equivocal

* Update commands.md

added brackets around optional arguments, and added 'key' placeholder indicating a required argument for 'showkey'

* Update commands.md

added single quotes inside of bracketed optional params (though I feel they should be reserved for shell escaping, and italics should be used for replacing text of arguments. I also added brackets (and quotes) around the `exec` parameter for `term`, although I'm not really sure if that's right because this command doesn't work on my system.
2024-03-14 04:39:17 +01:00
Dmytro Maluka
80db98dc81
Merge pull request #2959 from JoeKar/fix/raw-esc-sequence
bindings: Allow raw escape sequence to be bound with `bind`
2024-03-14 04:38:05 +01:00
Dmytro Maluka
e424537ff8
Merge pull request #2819 from JoeKar/fix/file-detection
Improve file detection with signature check capabilities
2024-03-14 04:37:10 +01:00
Dmytro Maluka
5bfda7b5f6
Merge branch 'master' into fix/file-detection 2024-03-14 04:32:09 +01:00
Dmytro Maluka
3dba23a348
Minor: fix weird error message text when unable to load help (#2618)
If we add something like this to init.lua:

   config.AddRuntimeFile("status", config.RTHelp, "help/foo.md")

then start micro and run "help foo", the resulting error message looks
weird:

   Unable to load help textfooopen plugins/status/help/foo.md: file does not exist

Change it to:

   Unable to load help text for foo: open plugins/status/help/foo.md: file does not exist
2024-03-14 03:59:36 +01:00
Dmytro Maluka
00174bb376
Merge pull request #2606 from dmaluka/mouse-release-and-drag-events
Introduce mouse release and mouse drag events
2024-03-14 03:54:04 +01:00
Dmytro Maluka
c4c5b184c2
Improve support for mouse events handling (#2605)
- If a mouse event is bound to a Lua function, pass *tcell.EventMouse to
  this Lua function, so that it can find out the position where a button
  was clicked etc, just like the built-in MousePress and MouseMultiCursor
  actions.

- Make mouse actions more a first-class citizen: allow chaining them and
  running onAction and preAction callbacks for them, just like key actions.
2024-03-14 03:52:52 +01:00
Dmytro Maluka
7b718cb87c
Merge pull request #1897 from dmaluka/wserrors
New feature: Highlighting whitespace errors
2024-03-14 03:26:09 +01:00
Dmitry Maluka
6dc3df646b readme: Mention hltrailingws/hltaberrors feature 2024-03-14 03:18:11 +01:00
Dmitry Maluka
13d1407f60 hltrailingws: simpler and better undo/redo handling 2024-03-14 03:18:11 +01:00
Dmitry Maluka
53efce72fa hltrailingws: improve behavior with selection
Improve user experience: if we are at a line with a new (i.e.
not highlighted yet) trailingws and we begin selecting text,
don't highlight the trailingws until we are done with selection,
even if we moved the cursor to another line while selecting.
2024-03-14 03:18:11 +01:00
Dmitry Maluka
f108c90643 hltrailingws: improve updateTrailingWs logic
Handle the case when the cursor itself hasn't really moved to
another line, but its line number has changed due to insert
or remove of some lines above.
In this case, if the cursor is still at its new trailingws,
we should not reset NewTrailingWsY to -1 but update it to the
new line number.

A scenario exemplifying this issue:
Bind some key, e.g. Alt-r, to such a lua function:

function insertNewlineAbove(bp)
    bp.Buf:Insert(buffer.Loc(0, bp.Cursor.Y), "\n")
end

Then in a file containing these lines:

aaa
bbb
ccc

insert a space at the end of bbb line, and then press Alt-r.
bbb and ccc are moved one line down, but also the trailing space
after bbb becomes highlighted, which isn't what we expect.
This commit fixes that.
2024-03-14 03:18:11 +01:00
Dmitry Maluka
b824e767d6 Add tab-error and trailingws colors to colorschemes 2024-03-14 03:18:10 +01:00
Dmitry Maluka
c52ccad14b hltrailingws: adjust autoclose plugin implementation
Fix unwanted highlighting of whitespace in the new line when inserting
a newline after a bracket (when hltrailingws is on). To fix it, change
the order of operations: insert the new empty line after all other
things, to avoid moving the cursor between lines after that.
2024-03-14 03:10:33 +01:00
Dmitry Maluka
104caf08dd Highlighting trailing whitespaces
Added option `hltrailingws` for highlighting trailing whitespaces
at the end of lines. Note that it behaves in a "smart" way.
It doesn't highlight newly added (transient) trailing whitespaces
that naturally occur while typing text. It would be annoying to
see transient highlighting every time we enter a space at the end
of a line while typing.
So a newly added trailing whitespace starts being highlighting
only after the cursor moves to another line. Thus the highlighting
serves its purpose: it draws our attention to annoying sloppy
forgotten trailing whitespaces.
2024-03-14 03:10:31 +01:00
Dmitry Maluka
64370b70d6 Highlighting tab errors
Added option `hltaberrors` which helps to spot sloppy whitespace errors
with tabs used instead of spaces or vice versa.

It uses the value of `tabstospaces` option as a criterion whether a
tab or space character is an error or not.
If `tabstospaces` is on, we probably expect that the file should contain
no tab characters, so any tab character is highlighted as an error.
If `tabstospaces` is off, we probably expect that the file uses
indentation with tabs, so space characters in the initial indent part
of lines are highlighted as errors.
2024-03-14 03:09:30 +01:00
Jöran Karl
8368af3cc8
command: Fix set local-only options for the current buffer only (#3042) 2024-03-13 21:34:52 +01:00
Jöran Karl
ca3a9d0794
command: Add capability to use relative numbers in goto (#2985)
* command: Handle relative line numbers for goto

* help: Adapt goto command documentation
2024-03-13 21:32:12 +01:00
Mikko
bd306d67b4
Smarter smartpaste (#3001) (#3002)
* smarterpaste(?)

* make it more readable

* fix edge cases

* fix paste starting with a single space

* fix single line paste
2024-03-13 21:16:10 +01:00
dimaguy
a01ae92541
Add main tag to html syntax highlighting (#2999)
* Add main tag to html syntax highlighting

* Reorder main
2024-03-13 21:12:38 +01:00
Dmytro Maluka
dcdd3e749a
Fix ruler overwriting neighboring split pane + fix crash #3052 (#3069)
* Fix gutter overwriting other split pane

When we resize a split pane to a very small width, so that the gutter
does not fit in the pane, it overwrites the sibling split pane.

To fix it, clean up the calculation of gutter width, buffer width and
scrollbar width, so that they add up exactly to the window width, and
ensure that we don't draw the gutter beyond this calculated gutter
width (gutterOffset).

As a bonus, this also fixes the crash #3052 (observed when resizing a
split pane to a very small width, if wordwrap is enabled), by ensuring
that bufWidth is never negative.

[*] By the gutter we mean of course gutter + diffgutter + ruler.

* Don't display line numbers if buffer width is 0 and softwrap is on

If softwrap is enabled, the line numbers displayed in the ruler depend
on the heights of the displayed softwrapped lines, which depend on the
width of the displayed buffer. If this width is 0 (e.g. after resizing
buffer pane to a very small width), there is no displayed text at all,
so line numbers don't make sense. So don't display line numbers in this
case.

* Fix buffer text overwriting scrollbar when window width is 1 char
2024-03-13 21:11:04 +01:00
Dmytro Maluka
628d9bb37b
Fix split pane divider hovering over neighboring split pane (#3070)
Fix the following funny issue: if we open 3 vertical split panes (i.e.
with 2 vertical dividers between them) and drag the rightmost divider
to the left (for resizing the middle and the rightmost split panes), it
does not stop at the leftmost divider but jumps over it and then hovers
over the leftmost split pane. And likewise with horizontal split panes.
2024-03-13 21:02:11 +01:00
Jöran Karl
bfc4b1d195
termwindow: Show cursor only when his X and Y axis is smaller than the window (#3036) 2024-03-13 20:58:44 +01:00
Yevhen Babiichuk (DustDFG)
d2ee6107a3
Highlight autcompleted command in statusline for simple theme (#3057)
Signed-off-by: Yevhen Babiichuk (DustDFG) <dfgdust@gmail.com>
Co-authored-by: Avi Halachmi (:avih) <avihpit@yahoo.com>
2024-03-13 20:44:41 +01:00
toiletbril
69eaa9191a
options: add matchbracestyle (#2876)
* Update docs to include `matchbracestyle`

* Add `matchbracestyle` to infocomplete.go

* Add validator and default settings for `matchbracestyle`

* Highlight or underline braces based on `matchbracestyle`

* Add `match-brace` to default colorschemes

* Correct `FindMatchingBrace()` counting

Make brace under the cursor have priority over brace to the left in
ambiguous cases when matching braces

Co-authored-by: Dmitry Maluka <dmitrymaluka@gmail.com>

* Fix conflicts

---------

Co-authored-by: Jöran Karl <3951388+JoeKar@users.noreply.github.com>
Co-authored-by: Dmitry Maluka <dmitrymaluka@gmail.com>
2024-03-13 20:21:27 +01:00
taconi
14dca7d349
syntax: log: add syntax highlight code for log files (#3105) 2024-03-13 19:01:16 +01:00
taconi
fad4e449fb
syntax: kvlang: add syntax highlight code for .kv files (#3106) 2024-03-13 19:00:29 +01:00
にてん
f0bc6281d4
Add onRune parameter, utf8 package in plugins.md (#3100)
Add bufpane in parameters of onRune, preRune, and unicode/utf8
Go package in plugins.md.
2024-03-12 21:40:13 +01:00
taconi
fe4ade78df
feat: adds GetArg and GetWord methods to Buffer (#3112) 2024-03-12 21:23:08 +01:00
Alex Rønne Petersen
88b4498ce0
Some syntax highlighting updates for C and C#. (#3125)
* Update C syntax with keywords up to C23.

* Update C syntax with some GCC extensions.

* Update C# syntax with new keywords up to C# 12.

* Update C# syntax with preprocessor directives.

* Add Cake build script (C#) syntax.

* Add MSBuild (XML) syntax.
2024-03-12 21:20:03 +01:00
Jöran Karl
3fce03dfd0
syntax: sh: Fix command parameter highlighting (#3128) 2024-03-12 21:11:20 +01:00
blt-r
15b36ce0d6
Remove the NoDisplay=true from desktop file (#3171)
TUI apps usually have their desktop files visible in system menus.
Also, flathub no longer allows apps with invisible desktop files.
2024-03-12 20:58:09 +01:00
Jöran Karl
321322af31
micro: DoEvent: Don't forward the resize event into the InfoBar (#3035) 2024-03-12 18:49:24 +01:00
Jöran Karl
0de16334d3
micro: Don't forward nil events into the sub event handler (#2992) 2024-03-12 18:35:33 +01:00
Jöran Karl
c15abea64c
rtfiles: Give user defined runtime files precedence over asset files (#3066) 2024-03-04 13:24:40 -08:00
Dmytro Maluka
9fdea82542
Fix various issues with SpawnMultiCursor{Up,Down} (#3145)
* SpawnMultiCursorUp/Down: change order of adding cursors

SpawnMultiCursor{Up,Down} currently works in a tricky way: instead of
creating a new cursor above or below, it moves the current "primary"
cursor above or below, and then creates a new cursor below or above the
new position of the current cursor (i.e. at its previous position),
creating an illusion for the user that the current (top-most or
bottom-most) cursor is a newly spawned cursor.

This trick causes at least the following issues:

- When the line above or below, where we spawn a new cursor, is shorter
  than the current cursor position in the current line, the new cursor
  is placed at the end of this short line (which is expected), but also
  the current cursor unexpectedly changes its x position and moves
  below/above the new cursor.

- When removing a cursor in RemoveMultiCursor (default Alt-p key), it
  non-intuitively removes the cursor which, from the user point of view,
  is not the last but the last-but-one cursor.

Fix these issues by replacing the trick with a straightforward logic:
just create the new cursor above or below the last one.

Note that this fix has a user-visible side effect: the last cursor is
no longer the "primary" one (since it is now the last in the list, not
the first), so e.g. when the user clears multicursors via Esc key, the
remaining cursor is the first one, not the last one. I assume it's ok.

* SpawnMultiCursorUp/Down: move common code to a helper fn

* SpawnMultiCursorUp/Down: honor visual width and LastVisualX

Make spawning multicursors up/down behave more similarly to cursor
movements up/down. This change fixes 2 issues at once:

- SpawnMultiCursorUp/Down doesn't take into account the visual width of
  the text before the cursor, which may be different from its character
  width (e.g. if it contains tabs). So e.g. if the number of tabs before
  the cursor in the current line is not the same as in the new line, the
  new cursor is placed at an unexpected location.

- SpawnMultiCursorUp/Down doesn't take into account the cursor's
  remembered x position (LastVisualX) when e.g. spawning a new cursor
  in the below line which is short than the current cursor position, and
  then spawning yet another cursor in the next below line which is
  longer than this short line.

* SpawnMultiCursorUp/Down: honor softwrap

When softwrap is enabled and the current line is wrapped, make
SpawnMultiCursor{Up,Down} spawn cursor in the next visual line within
this wrapped line, similarly to how we handle cursor movements up/down
within wrapped lines.

* SpawnMultiCursorUp/Down: deselect when spawning cursors

To avoid weird user experience (spawned cursors messing with selections
of existing cursors).
2024-03-04 13:23:50 -08:00
Jöran Karl
eedebd80d4
util: Fix opening filenames including colons with parsecursor (#3119)
The regex pattern shall search for the end of the filename first as it does
while opening with +LINE:COL.
2024-03-04 13:22:47 -08:00
Dmytro Maluka
e5026ef3fa
Make MouseMultiCursor toggle cursors (#3146)
It is useful to be able to use mouse not only for adding new cursors
but also for removing them. So let's modify MouseMultiCursor behavior:
if a cursor already exists at the mouse click location, remove it.
2024-03-04 13:21:50 -08:00
Dmytro Maluka
af2ec9d540
Make default fileformat value suited to the OS (#3141)
Set fileformat by default to `dos` on Windows.
2024-03-04 13:20:02 -08:00
Dmytro Maluka
59dda01cb7
Make plugins in ~/.config/micro/plug dir override built-in plugins (#3031)
If ~/.config/micro/plug directory contains a plugin with the same name
as a built-in plugin, the expected behavior is that the user-defined
plugin in ~/.config/micro/plug is loaded instead of the built-in one.

Whereas the existing behavior is that the built-in plugin is used
instead of the user-defined one. Even worse, it is buggy: in this case
the plugin is registered twice, so its callbacks are executed twice
(e.g. with the autoclose plugin, a bracket is autoclosed with two
closing brackets instead of one).

Fix this by ensuring that if a plugin with the same name exists in the
~/.config/micro/plug directory, the built-in one is ignored.

Fixes #3029
2024-01-17 00:09:33 -08:00
Yevhen Babiichuk (DustDFG)
fce8db80de
Add go.mod syntax support (#3061)
Signed-off-by: Yevhen Babiichuk (DustDFG) <dfgdust@gmail.com>
2024-01-17 00:07:51 -08:00
Jöran Karl
e5a9b906f3
infocomplete: Complete filetypes (#3090) 2024-01-17 00:06:45 -08:00
niten94
422305af99
Set bits in mode used when opening files (#3095)
Set write permission bits of group and other users in mode used when
opening files.
2024-01-17 00:06:14 -08:00
Yevhen Babiichuk (DustDFG)
4e383dd110
Do correct cursor right with storing visual X in CursorRight action (#3103)
Signed-off-by: Yevhen Babiichuk (DustDFG) <dfgdust@gmail.com>
2024-01-17 00:04:18 -08:00
Jöran Karl
2d82362a66
actions: saveas: Fix crash at access without permission (#3082) 2023-12-10 13:52:22 -08:00
Dmitry Maluka
359b58a89b Don't rewrite settings.json when registering options
It doesn't seem necessary to write settings to settings.json when
registering a new option. The option is set to its default value, which
means that it will not be written to settings.json (precisely because
it's the default value), so the contents of settings.json don't change
and thus don't need to be written again.

This unneeded writing, in particular, causes unexpected "The file on
disk has changed. Reload file? (y,n,esc)" each time when we open
settings.json via micro.

Fixes #2647
2023-11-03 01:11:05 +01:00
Dmitry Maluka
a373d22939 Refactor defaultvalue setting a bit 2023-11-03 01:07:29 +01:00
Dmitry Maluka
12398916c7 Fix code duplication in RegisterCommonOptionPlug
Avoid code duplication between RegisterCommonOption() and
RegisterCommonOptionPlug(), exactly the same way as it is done for
RegisterGlobalOption() and RegisterGlobalOptionPlug().
2023-11-03 00:58:30 +01:00
Dmitry Maluka
c791cef9c6 Fix default setting for global & common options
Apply the same fix as 4d13308 to all kinds of options, not just to
plugin options.
2023-11-03 00:51:30 +01:00
Jöran Karl
3c16df87ee options: Add capability to define the line count parsed for the signature check 2023-10-26 20:59:42 +02:00
Jöran Karl
2d0d0416e7 buffer: Prefer user defined over built-in file types 2023-10-26 20:59:42 +02:00
Jöran Karl
2aa386f455 syntax: Prepare a concrete signature example for C++ 2023-10-26 20:59:37 +02:00
Jöran Karl
93151f8109 syntax: Prepare a concrete signature example for objective C 2023-10-26 20:48:27 +02:00
Jöran Karl
433879046e Improve file detection with signature check capabilities
This allows more complex detection upon regex rules for a certain amount of
lines.
2023-10-26 20:20:02 +02:00
Zachary Yedidia
d8e9d61a95 Build arm32 binaries with GOARM=6
Ref #2986
2023-10-22 22:50:27 +02:00
Jöran Karl
6fa12743d6 bindings: Add capability to unregister user defined raw escape sequence 2023-10-22 13:59:59 +02:00
Jöran Karl
dcc7205699 bindings: Allow raw escape sequence to be bound with bind 2023-10-22 13:59:38 +02:00
Dmitry Maluka
2d95064ff6 Make a pane active whenever any mouse button is pressed on it
Since now bufpane handles mouse move and release events generically and
separately from mouse press events, that creates a mess when we dispatch
a mouse press event to an inactive pane without making it active.

For example:
1. Click the right button on an inactive pane. It remains inactive.
2. Then click the left button on it. It becomes active, and an
unexpected text selection appears.
The reason is that the release event for the first click was dispatched
to a wrong pane - the (then) active one, so the (then) inactive pane
didn't get the release event and treats the second click not as a mouse
press but as a mouse move.

The simplest way to fix it is to avoid this scenario entirely, i.e.
always activate the pane when clicking any mouse button on it, not just
the left button.

For mouse wheel motion events we keep the existing behavior: the pane
gets the event but doesn't become active. Mouse wheel motion events are
not affected by the described issue, as they have no paired "release"
events.
2023-01-29 18:27:22 +01:00
Dmitry Maluka
e5093892fd Reset mouse release state after restarting the screen
When we temporarily disable the screen (e.g. during TermMessage or
RunInteractiveShell), if the mouse is pressed when the screen is still
active and then released when the screen is already stopped, we aren't
able to catch this mouse release event, so we erroneously think that the
mouse is still pressed after the screen is restarted. This results in
wrong behavior due to a mouse press event treated as a mouse move event,
e.g. upon the left button click we see an unexpected text selection.

So need to reset the mouse release state to "released" after restarting
the screen, assuming it is always released when the screen is restarted.
2023-01-29 18:26:55 +01:00
Dmitry Maluka
124fa9e2e7 Fix up double release event after drag
If we press mouse, drag and then release, the release event is
generated twice, since both mouse press and mouse drag events have been
saved in mousePressed map. To fix that, ensure that we only store mouse
press events in it.
2023-01-29 18:22:45 +01:00
Dmitry Maluka
34ac83b594 Introduce mouse release and mouse drag events
Introduce separate mouse release and mouse drag (move while pressed)
events: MouseLeftRelease, MouseLeftDrag, MouseRightRelease etc,
to allow binding them to actions independently from mouse press events
(MouseLeft, MouseRight etc).

This change:

- Makes it possible to handle mouse release and drag for arbitrary mouse
  events and actions (including Lua actions), not just for MouseLeft as
  in the current code.

- Fixes issue #2599 with PastePrimary and MouseMultiCursor actions:
  selection is pasted not only when pressing MouseMiddle but also when
  moving mouse with MouseMiddle pressed; similarly, a new multicursor is
  added not only when pressing Ctrl-MouseLeft but also when moving mouse
  with Ctrl-MouseLeft pressed.

My initial approach was not to introduce new events for mouse release
and mouse drag but to pass "mouse released" info to action functions
in addition to *tcell.EventMouse to let the action functions do the
necessary checks (similarly to what MousePress is already doing). But
then I realized it was a bad idea, since we still want to be able also
to bind mouse events to regular key actions (such as PastePrimary)
which don't care about mouse event info.
2023-01-29 18:21:59 +01:00
252 changed files with 5997 additions and 3203 deletions

46
.github/workflows/nightly.yaml vendored Normal file
View File

@ -0,0 +1,46 @@
name: Nightly builds
on:
workflow_dispatch: # Allows manual trigger
schedule:
- cron: '0 0 * * *'
jobs:
nightly:
strategy:
matrix:
go-version: [1.23.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Setup
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: false
- name: Checkout
uses: actions/checkout@v4
with:
ref: master
fetch-depth: 0
fetch-tags: true
- name: Build
run: tools/cross-compile.sh nightly
- name: Tag
uses: rickstaa/action-create-tag@v1
with:
tag: nightly
force_push_tag: true
message: "Pre-release nightly"
- name: Publish
uses: softprops/action-gh-release@v2
with:
name: nightly
tag_name: nightly
files: binaries/*
prerelease: true
- name: Cleanup
run: rm -rf binaries

36
.github/workflows/release.yaml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Release builds
on:
workflow_dispatch: # Allows manual trigger
# push:
# tags:
# - 'v*.*.*' # automatically react on semantic versioned tags
jobs:
release:
strategy:
matrix:
go-version: [1.23.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Setup
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: false
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Build
run: tools/cross-compile.sh
- name: Publish
uses: softprops/action-gh-release@v2
with:
files: binaries/*
- name: Cleanup
run: rm -rf binaries

View File

@ -4,14 +4,19 @@ jobs:
test:
strategy:
matrix:
go-version: [1.19.x]
go-version: [1.19.x, 1.23.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-go@v3
- uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- uses: actions/checkout@v3
cache: false
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Build
run: |

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/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/zyedidia/json5/LICENSE (fork)
github.com/micro-editor/json5/LICENSE (fork)
================
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/zyedidia/terminal/LICENSE (fork)
github.com/micro-editor/terminal/LICENSE (fork)
================
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)
DATE = $(shell GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) \
go run tools/build-date.go)
ADDITIONAL_GO_LINKER_FLAGS = $(shell GOOS=$(shell go env GOHOSTOS) \
GOARCH=$(shell go env GOHOSTARCH) \
go run tools/info-plist.go "$(shell go env GOOS)" "$(VERSION)")
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)'
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/'
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-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:
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
go build -trimpath -ldflags "-s -w $(GOVARS) $(ADDITIONAL_GO_LINKER_FLAGS)" ./cmd/micro
build-tags: fetch-tags build
build-all: build
@ -32,7 +39,7 @@ install: generate
install-all: install
fetch-tags:
git fetch --tags
git fetch --tags --force
generate:
GOOS=$(shell go env GOHOSTOS) GOARCH=$(shell go env GOHOSTARCH) go generate ./runtime

View File

@ -18,25 +18,9 @@ Here is a picture of micro editing its source code.
![Screenshot](./assets/micro-solarized.png)
To see more screenshots of micro, showcasing some of the default color schemes, see [here](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
@ -63,11 +47,12 @@ You can also check out the website for Micro at https://micro-editor.github.io.
- Syntax highlighting for over [130 languages](runtime/syntax).
- Color scheme support.
- By default, micro comes with 16, 256, and true color themes.
- True color support (set the `MICRO_TRUECOLOR` environment variable to 1 to enable it).
- True color support.
- Copy and paste with the system clipboard.
- Small and simple.
- Easily configurable.
- Macros.
- Smart highlighting of trailing whitespace and tab vs space errors.
- Common editor features such as undo/redo, line numbers, Unicode support, soft wrapping, …
## Installation
@ -87,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`.
#### Quick-install script
#### Third-party quick-install script
```bash
curl https://getmic.ro | bash
@ -137,24 +122,33 @@ for other operating systems. These packages are not guaranteed to be up-to-date.
<!-- * `apt install micro` (Ubuntu 20.04 `focal`, and Debian `unstable | testing | buster-backports`). At the moment, this package (2.0.1-1) is outdated and has a known bug where debug mode is enabled. -->
* Linux: Available in distro-specific package managers.
* `dnf install micro` (Fedora).
* `apt install micro` (Ubuntu and Debian).
* `pacman -S micro` (Arch Linux).
* `emerge app-editors/micro` (Gentoo).
* `zypper install micro-editor` (SUSE)
* `eopkg install micro` (Solus).
* `pacstall -I micro` (Pacstall).
* See [wiki](https://github.com/zyedidia/micro/wiki/Installing-Micro) for details about CRUX, Termux.
* Windows: [Chocolatey](https://chocolatey.org) and [Scoop](https://github.com/lukesampson/scoop).
* Linux:
* distro-specific package managers:
* `dnf install micro` (Fedora).
* `apt install micro` (Ubuntu and Debian).
* `pacman -S micro` (Arch Linux).
* `emerge app-editors/micro` (Gentoo).
* `zypper install micro-editor` (SUSE)
* `eopkg install micro` (Solus).
* `pacstall -I micro` (Pacstall).
* `apt-get install micro` (ALT Linux)
* See [wiki](https://github.com/zyedidia/micro/wiki/Installing-Micro) for details about CRUX, Termux.
* distro-agnostic package managers:
* `nix profile install nixpkgs#micro` (with [Nix](https://nixos.org/) and flakes enabled)
* `flox install micro` (with [Flox](https://flox.dev))
* Windows: [Chocolatey](https://chocolatey.org), [Scoop](https://scoop.sh/) and [WinGet](https://learn.microsoft.com/en-us/windows/package-manager/winget/).
* `choco install micro`.
* `scoop install micro`.
* `winget install zyedidia.micro`
* OpenBSD: Available in the ports tree and also available as a binary package.
* `pkd_add -v micro`.
* `pkg_add -v micro`.
* NetBSD, macOS, Linux, Illumos, etc. with [pkgsrc](http://www.pkgsrc.org/)-current:
* `pkg_add micro`
* macOS with [MacPorts](https://www.macports.org):
* `sudo port install micro`
* macOS: Available in package managers.
* `sudo port install micro` (with [MacPorts](https://www.macports.org))
* `brew install micro` (with [Homebrew](https://brew.sh/))
* `nix profile install nixpkgs#micro` (with [Nix](https://nixos.org/) and flakes enabled)
* `flox install micro` (with [Flox](https://flox.dev))
**Note for Linux desktop environments:**
@ -168,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.
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
@ -186,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),
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
recommended for security and portability). However, there is a fully static prebuilt binary that
is provided for amd64 as `linux-static.tar.gz`, and to build a fully static binary from source, run
By default, the micro binary is linked statically to increase the portability of the prebuilt binaries.
This behavior can simply be overriden by providing `CGO_ENABLED=1` to the build target.
```
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
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

@ -12,5 +12,4 @@ Keywords=text;editor;syntax;terminal;
Exec=micro %F
StartupNotify=false
Terminal=true
NoDisplay=true
MimeType=text/plain;text/x-chdr;text/x-csrc;text/x-c++hdr;text/x-c++src;text/x-java;text/x-dsrc;text/x-pascal;text/x-perl;text/x-python;application/x-php;application/x-httpd-php3;application/x-httpd-php4;application/x-httpd-php5;application/xml;text/html;text/css;text/x-sql;text/x-diff;

View File

@ -3,8 +3,8 @@ package main
import (
"bufio"
"encoding/gob"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
@ -12,6 +12,7 @@ import (
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/util"
)
func shouldContinue() bool {
@ -39,7 +40,16 @@ func CleanConfig() {
}
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
var unusedOptions []string
@ -67,16 +77,20 @@ func CleanConfig() {
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() {
for _, s := range unusedOptions {
delete(config.GlobalSettings, s)
}
err := config.OverwriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
err := config.OverwriteSettings(settingsFile)
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")
@ -85,12 +99,13 @@ func CleanConfig() {
}
// 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 {
var badFiles []string
var buffer buffer.SerializedBuffer
for _, f := range files {
fname := filepath.Join(config.ConfigDir, "buffers", f.Name())
fname := filepath.Join(buffersPath, f.Name())
file, e := os.Open(fname)
if e == nil {
@ -105,9 +120,9 @@ func CleanConfig() {
}
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.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() {
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
func InitLog() {
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 {
log.Fatalf("error opening file: %v", err)
}

View File

@ -2,6 +2,7 @@ package main
import (
"log"
"time"
lua "github.com/yuin/gopher-lua"
luar "layeh.com/gopher-luar"
@ -47,14 +48,18 @@ func luaImportMicro() *lua.LTable {
ulua.L.SetField(pkg, "InfoBar", luar.New(ulua.L, action.GetInfoBar))
ulua.L.SetField(pkg, "Log", luar.New(ulua.L, log.Println))
ulua.L.SetField(pkg, "SetStatusInfoFn", luar.New(ulua.L, display.SetStatusInfoFnLua))
ulua.L.SetField(pkg, "CurPane", luar.New(ulua.L, func() action.Pane {
ulua.L.SetField(pkg, "CurPane", luar.New(ulua.L, func() *action.BufPane {
return action.MainTab().CurPane()
}))
ulua.L.SetField(pkg, "CurTab", luar.New(ulua.L, action.MainTab))
ulua.L.SetField(pkg, "Tabs", luar.New(ulua.L, func() *action.TabList {
return action.Tabs
}))
ulua.L.SetField(pkg, "Lock", luar.New(ulua.L, &ulua.Lock))
ulua.L.SetField(pkg, "After", luar.New(ulua.L, func(t time.Duration, f func()) {
time.AfterFunc(t, func() {
timerChan <- f
})
}))
return pkg
}

View File

@ -4,10 +4,10 @@ import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/signal"
"path/filepath"
"regexp"
"runtime"
"runtime/pprof"
@ -18,22 +18,18 @@ import (
"github.com/go-errors/errors"
isatty "github.com/mattn/go-isatty"
"github.com/micro-editor/tcell/v2"
lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/micro/v2/internal/action"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/clipboard"
"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/shell"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2"
)
var (
// Event channel
autosave chan bool
// Command line flags
flagVersion = flag.Bool("version", false, "Show the version number and information")
flagConfigDir = flag.String("config-dir", "", "Specify a custom location for the configuration directory")
@ -44,8 +40,9 @@ var (
flagClean = flag.Bool("clean", false, "Clean configuration directory")
optionFlags map[string]*string
sigterm chan os.Signal
sighup chan os.Signal
sighup chan os.Signal
timerChan chan func()
)
func InitFlags() {
@ -68,7 +65,7 @@ func InitFlags() {
fmt.Println("-version")
fmt.Println(" \tShow the version number and information")
fmt.Print("\nMicro's plugin's can be managed at the command line with the following commands.\n")
fmt.Print("\nMicro's plugins can be managed at the command line with the following commands.\n")
fmt.Println("-plugin install [PLUGIN]...")
fmt.Println(" \tInstall plugin(s)")
fmt.Println("-plugin remove [PLUGIN]...")
@ -102,7 +99,7 @@ func InitFlags() {
fmt.Println("Version:", util.Version)
fmt.Println("Commit hash:", util.CommitHash)
fmt.Println("Compiled on", util.CompileDate)
os.Exit(0)
exit(0)
}
if *flagOptions {
@ -118,7 +115,7 @@ func InitFlags() {
fmt.Printf("-%s value\n", k)
fmt.Printf(" \tDefault value: '%v'\n", v)
}
os.Exit(0)
exit(0)
}
if util.Debug == "OFF" && *flagDebug {
@ -139,7 +136,7 @@ func DoPluginFlags() {
CleanConfig()
}
os.Exit(0)
exit(0)
}
}
@ -212,7 +209,7 @@ func LoadInput(args []string) []*buffer.Buffer {
// Option 2
// The input is not a terminal, so something is being piped in
// and we should read from stdin
input, err = ioutil.ReadAll(os.Stdin)
input, err = io.ReadAll(os.Stdin)
if err != nil {
screen.TermMessage("Error reading from stdin: ", err)
input = []byte{}
@ -226,12 +223,55 @@ func LoadInput(args []string) []*buffer.Buffer {
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() {
defer func() {
if util.Stdout.Len() > 0 {
fmt.Fprint(os.Stdout, util.Stdout.String())
}
os.Exit(0)
exit(0)
}()
var err error
@ -256,7 +296,15 @@ func main() {
screen.TermMessage(err)
}
config.InitRuntimeFiles()
config.InitRuntimeFiles(true)
config.InitPlugins()
err = checkBackup("settings.json")
if err != nil {
screen.TermMessage(err)
exit(1)
}
err = config.ReadSettings()
if err != nil {
screen.TermMessage(err)
@ -274,7 +322,12 @@ func main() {
screen.TermMessage(err)
continue
}
if err = config.OptionIsValid(k, nativeValue); err != nil {
screen.TermMessage(err)
continue
}
config.GlobalSettings[k] = nativeValue
config.VolatileSettings[k] = true
}
}
@ -284,7 +337,7 @@ func main() {
if err != nil {
fmt.Println(err)
fmt.Println("Fatal: Micro could not initialize a Screen.")
os.Exit(1)
exit(1)
}
m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string))
clipErr := clipboard.Initialize(m)
@ -303,7 +356,7 @@ func main() {
for _, b := range buffer.OpenBuffers {
b.Backup()
}
os.Exit(1)
exit(1)
}
}()
@ -312,6 +365,12 @@ func main() {
screen.TermMessage(err)
}
err = checkBackup("bindings.json")
if err != nil {
screen.TermMessage(err)
exit(1)
}
action.InitBindings()
action.InitCommands()
@ -352,18 +411,20 @@ func main() {
log.Println(clipErr, " or change 'clipboard' option")
}
config.StartAutoSave()
if a := config.GetGlobalOption("autosave").(float64); a > 0 {
config.SetAutoTime(int(a))
config.StartAutoSave()
config.SetAutoTime(a)
}
screen.Events = make(chan tcell.Event)
sigterm = make(chan os.Signal, 1)
util.Sigterm = make(chan os.Signal, 1)
sighup = make(chan os.Signal, 1)
signal.Notify(sigterm, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGABRT)
signal.Notify(util.Sigterm, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGABRT)
signal.Notify(sighup, syscall.SIGHUP)
timerChan = make(chan func())
// Here is the event loop which runs in a separate thread
go func() {
for {
@ -414,39 +475,26 @@ func DoEvent() {
select {
case f := <-shell.Jobs:
// If a new job has finished while running in the background we should execute the callback
ulua.Lock.Lock()
f.Function(f.Output, f.Args)
ulua.Lock.Unlock()
case <-config.Autosave:
ulua.Lock.Lock()
for _, b := range buffer.OpenBuffers {
b.Save()
b.AutoSave()
}
ulua.Lock.Unlock()
case <-shell.CloseTerms:
action.Tabs.CloseTerms()
case event = <-screen.Events:
case <-screen.DrawChan():
for len(screen.DrawChan()) > 0 {
<-screen.DrawChan()
}
case f := <-timerChan:
f()
case b := <-buffer.BackupCompleteChan:
b.RequestedBackup = false
case <-sighup:
for _, b := range buffer.OpenBuffers {
if !b.Modified() {
b.Fini()
}
}
os.Exit(0)
case <-sigterm:
for _, b := range buffer.OpenBuffers {
if !b.Modified() {
b.Fini()
}
}
if screen.Screen != nil {
screen.Screen.Fini()
}
os.Exit(0)
exit(0)
case <-util.Sigterm:
exit(0)
}
if e, ok := event.(*tcell.EventError); ok {
@ -454,27 +502,25 @@ func DoEvent() {
if e.Err() == io.EOF {
// shutdown due to terminal closing/becoming inaccessible
for _, b := range buffer.OpenBuffers {
if !b.Modified() {
b.Fini()
}
}
if screen.Screen != nil {
screen.Screen.Fini()
}
os.Exit(0)
exit(0)
}
return
}
ulua.Lock.Lock()
// if event != nil {
if action.InfoBar.HasPrompt {
action.InfoBar.HandleEvent(event)
} else {
action.Tabs.HandleEvent(event)
if event != nil {
_, resize := event.(*tcell.EventResize)
if resize {
action.InfoBar.HandleEvent(event)
action.Tabs.HandleEvent(event)
} else if action.InfoBar.HasPrompt {
action.InfoBar.HandleEvent(event)
} else {
action.Tabs.HandleEvent(event)
}
}
err := config.RunPluginFn("onAnyEvent")
if err != nil {
screen.TermMessage(err)
}
// }
ulua.Lock.Unlock()
}

View File

@ -2,18 +2,17 @@ package main
import (
"fmt"
"io/ioutil"
"log"
"os"
"testing"
"github.com/go-errors/errors"
"github.com/micro-editor/tcell/v2"
"github.com/stretchr/testify/assert"
"github.com/zyedidia/micro/v2/internal/action"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/tcell/v2"
)
var tempDir string
@ -26,7 +25,7 @@ func init() {
func startup(args []string) (tcell.SimulationScreen, error) {
var err error
tempDir, err = ioutil.TempDir("", "micro_test")
tempDir, err = os.MkdirTemp("", "micro_test")
if err != nil {
return nil, err
}
@ -35,7 +34,9 @@ func startup(args []string) (tcell.SimulationScreen, error) {
return nil, err
}
config.InitRuntimeFiles()
config.InitRuntimeFiles(true)
config.InitPlugins()
err = config.ReadSettings()
if err != nil {
return nil, err
@ -107,7 +108,10 @@ func handleEvent() {
if e != nil {
screen.Events <- e
}
DoEvent()
for len(screen.DrawChan()) > 0 || len(screen.Events) > 0 {
DoEvent()
}
}
func injectKey(key tcell.Key, r rune, mod tcell.ModMask) {
@ -149,20 +153,32 @@ func openFile(file string) {
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
}
func createTestFile(name string, content string) (string, error) {
testf, err := ioutil.TempFile("", name)
func findBuffer(file string) *buffer.Buffer {
var buf *buffer.Buffer
for _, b := range buffer.OpenBuffers {
if b.Path == file {
buf = b
}
}
return buf
}
func createTestFile(t *testing.T, content string) string {
f, err := os.CreateTemp(t.TempDir(), "")
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 "", err
}
if err := testf.Close(); err != nil {
return "", err
}
return testf.Name(), nil
return f.Name()
}
func TestMain(m *testing.M) {
@ -179,25 +195,12 @@ func TestMain(m *testing.M) {
}
func TestSimpleEdit(t *testing.T) {
file, err := createTestFile("micro_simple_edit_test", "base content")
if err != nil {
t.Error(err)
return
}
defer os.Remove(file)
file := createTestFile(t, "base content")
openFile(file)
var buf *buffer.Buffer
for _, b := range buffer.OpenBuffers {
if b.Path == file {
buf = b
}
}
if buf == nil {
t.Errorf("Could not find buffer %s", file)
return
if findBuffer(file) == nil {
t.Fatalf("Could not find buffer %s", file)
}
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
@ -215,25 +218,23 @@ func TestSimpleEdit(t *testing.T) {
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
data, err := ioutil.ReadFile(file)
data, err := os.ReadFile(file)
if err != nil {
t.Error(err)
return
t.Fatal(err)
}
assert.Equal(t, "firstfoobar\nbase content\n", string(data))
}
func TestMouse(t *testing.T) {
file, err := createTestFile("micro_mouse_test", "base content")
if err != nil {
t.Error(err)
return
}
defer os.Remove(file)
file := createTestFile(t, "base content")
openFile(file)
if findBuffer(file) == nil {
t.Fatalf("Could not find buffer %s", file)
}
// buffer:
// base content
// the selections need to happen at different locations to avoid a double click
@ -262,10 +263,9 @@ func TestMouse(t *testing.T) {
// base content
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
data, err := ioutil.ReadFile(file)
data, err := os.ReadFile(file)
if err != nil {
t.Error(err)
return
t.Fatal(err)
}
assert.Equal(t, "firstline\nsecondline\nbase content\n", string(data))
@ -288,25 +288,23 @@ Ernleȝe test_string æðelen
`
func TestSearchAndReplace(t *testing.T) {
file, err := createTestFile("micro_search_replace_test", srTestStart)
if err != nil {
t.Error(err)
return
}
defer os.Remove(file)
file := createTestFile(t, srTestStart)
openFile(file)
if findBuffer(file) == nil {
t.Fatalf("Could not find buffer %s", file)
}
injectKey(tcell.KeyCtrlE, rune(tcell.KeyCtrlE), tcell.ModCtrl)
injectString(fmt.Sprintf("replaceall %s %s", "foo", "test_string"))
injectKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
data, err := ioutil.ReadFile(file)
data, err := os.ReadFile(file)
if err != nil {
t.Error(err)
return
t.Fatal(err)
}
assert.Equal(t, srTest2, string(data))
@ -319,10 +317,9 @@ func TestSearchAndReplace(t *testing.T) {
injectKey(tcell.KeyCtrlS, rune(tcell.KeyCtrlS), tcell.ModCtrl)
data, err = ioutil.ReadFile(file)
data, err = os.ReadFile(file)
if err != nil {
t.Error(err)
return
t.Fatal(err)
}
assert.Equal(t, srTest3, string(data))

View File

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

View File

@ -170,10 +170,19 @@
"default": false
},
"matchbrace": {
"description": "Whether to underline matching braces\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
"description": "Whether to show matching braces\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
"type": "boolean",
"default": true
},
"matchbracestyle": {
"description": "Whether to underline or highlight matching braces\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
"type": "string",
"enum": [
"underline",
"highlight"
],
"default": "underline"
},
"mkparents": {
"description": "Whether to create missing directories\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
"type": "boolean",
@ -296,8 +305,8 @@
},
"statusline": {
"description": "Whether to display a status line\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
"type": "string",
"default": "sudo"
"type": "boolean",
"default": true
},
"sucmd": {
"description": "A super user command\nhttps://github.com/zyedidia/micro/blob/master/runtime/help/options.md#options",
@ -355,4 +364,4 @@
}
},
"additionalProperties": false
}
}

33
go.mod
View File

@ -5,26 +5,35 @@ require (
github.com/dustin/go-humanize v1.0.0
github.com/go-errors/errors v1.0.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/go-isatty v0.0.11
github.com/mattn/go-runewidth v0.0.7
github.com/mattn/go-isatty v0.0.20
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/sergi/go-diff v1.1.0
github.com/stretchr/testify v1.4.0
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb
github.com/yuin/gopher-lua v1.1.1
github.com/zyedidia/clipper v0.1.1
github.com/zyedidia/glob v0.0.0-20170209203856-dd4023a66dc3
github.com/zyedidia/json5 v0.0.0-20200102012142-2da050b1a98d
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
golang.org/x/text v0.4.0
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/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
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/layeh/gopher-luar v1.0.7/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
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/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
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/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
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/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/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/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-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
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/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/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/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-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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
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=
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/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.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
// +build plan9 nacl windows
//go:build plan9 || nacl || windows
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

View File

@ -4,17 +4,18 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
"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/screen"
"github.com/zyedidia/tcell/v2"
"github.com/zyedidia/micro/v2/internal/util"
)
var Binder = map[string]func(e Event, action string){
@ -23,9 +24,13 @@ var Binder = map[string]func(e Event, action string){
"terminal": TermMapEvent,
}
func writeFile(name string, txt []byte) error {
return util.SafeWrite(name, txt, false)
}
func createBindingsIfNotExist(fname string) {
if _, e := os.Stat(fname); os.IsNotExist(e) {
ioutil.WriteFile(fname, []byte("{}"), 0644)
if _, e := os.Stat(fname); errors.Is(e, fs.ErrNotExist) {
writeFile(fname, []byte("{}"))
}
}
@ -37,7 +42,7 @@ func InitBindings() {
createBindingsIfNotExist(filename)
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
input, err := os.ReadFile(filename)
if err != nil {
screen.TermMessage("Error reading bindings.json file: " + err.Error())
return
@ -88,6 +93,10 @@ func BindKey(k, v string, bind func(e Event, a string)) {
return
}
if strings.HasPrefix(k, "\x1b") {
screen.RegisterRawSeq(k)
}
bind(event, v)
// switch e := event.(type) {
@ -153,7 +162,6 @@ modSearch:
k = k[5:]
modifiers |= tcell.ModShift
case strings.HasPrefix(k, "\x1b"):
screen.Screen.RegisterRawSeq(k)
return RawEvent{
esc: k,
}, true
@ -173,39 +181,35 @@ modSearch:
// see if the key is in bindingKeys with the Ctrl prefix.
k = string(unicode.ToUpper(rune(k[0]))) + k[1:]
if code, ok := keyEvents["Ctrl"+k]; ok {
var r tcell.Key
// Special case for escape, for some reason tcell doesn't send it with the esc character
if code < 256 && code != 27 {
r = code
}
// It is, we're done.
return KeyEvent{
code: code,
mod: modifiers,
r: rune(r),
}, true
}
}
// See if we can find the key in bindingKeys
if code, ok := keyEvents[k]; ok {
var r tcell.Key
// Special case for escape, for some reason tcell doesn't send it with the esc character
if code < 256 && code != 27 {
r = code
}
return KeyEvent{
code: code,
mod: modifiers,
r: rune(r),
}, true
}
var mstate MouseState = MousePress
if strings.HasSuffix(k, "Drag") {
k = k[:len(k)-4]
mstate = MouseDrag
} else if strings.HasSuffix(k, "Release") {
k = k[:len(k)-7]
mstate = MouseRelease
}
// See if we can find the key in bindingMouse
if code, ok := mouseEvents[k]; ok {
return MouseEvent{
btn: code,
mod: modifiers,
btn: code,
mod: modifiers,
state: mstate,
}, true
}
@ -239,6 +243,24 @@ func findEvent(k string) (Event, error) {
return event, nil
}
func eventsEqual(e1 Event, e2 Event) bool {
seq1, ok1 := e1.(KeySequenceEvent)
seq2, ok2 := e2.(KeySequenceEvent)
if ok1 && ok2 {
if len(seq1.keys) != len(seq2.keys) {
return false
}
for i := 0; i < len(seq1.keys); i++ {
if seq1.keys[i] != seq2.keys[i] {
return false
}
}
return true
}
return e1 == e2
}
// TryBindKey tries to bind a key by writing to config.ConfigDir/bindings.json
// Returns true if the keybinding already existed and a possible error
func TryBindKey(k, v string, overwrite bool) (bool, error) {
@ -248,7 +270,7 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) {
filename := filepath.Join(config.ConfigDir, "bindings.json")
createBindingsIfNotExist(filename)
if _, e = os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
input, err := os.ReadFile(filename)
if err != nil {
return false, errors.New("Error reading bindings.json file: " + err.Error())
}
@ -264,28 +286,31 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) {
}
found := false
for ev := range parsed {
var ev string
for ev = range parsed {
if e, err := findEvent(ev); err == nil {
if e == key {
if overwrite {
parsed[ev] = v
}
if eventsEqual(e, key) {
found = true
break
}
}
}
if found && !overwrite {
return true, nil
} else if !found {
if found {
if overwrite {
parsed[ev] = v
} else {
return true, nil
}
} else {
parsed[k] = v
}
BindKey(k, v, Binder["buffer"])
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
}
@ -298,7 +323,7 @@ func UnbindKey(k string) error {
filename := filepath.Join(config.ConfigDir, "bindings.json")
createBindingsIfNotExist(filename)
if _, e = os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
input, err := os.ReadFile(filename)
if err != nil {
return errors.New("Error reading bindings.json file: " + err.Error())
}
@ -315,13 +340,17 @@ func UnbindKey(k string) error {
for ev := range parsed {
if e, err := findEvent(ev); err == nil {
if e == key {
if eventsEqual(e, key) {
delete(parsed, ev)
break
}
}
}
if strings.HasPrefix(k, "\x1b") {
screen.UnregisterRawSeq(k)
}
defaults := DefaultBindings("buffer")
if a, ok := defaults[k]; ok {
BindKey(k, a, Binder["buffer"])
@ -331,7 +360,8 @@ func UnbindKey(k string) error {
}
txt, _ := json.MarshalIndent(parsed, "", " ")
return ioutil.WriteFile(filename, append(txt, '\n'), 0644)
txt = append(txt, '\n')
return writeFile(filename, txt)
}
return e
}

View File

@ -6,17 +6,18 @@ import (
luar "layeh.com/gopher-luar"
"github.com/micro-editor/tcell/v2"
lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/clipboard"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/display"
ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2"
)
type BufAction interface{}
// BufKeyAction represents an action bound to a key.
type BufKeyAction func(*BufPane) bool
@ -44,8 +45,9 @@ func init() {
BufBindings = NewKeyTree()
}
// LuaAction makes a BufKeyAction from a lua function.
func LuaAction(fn string) func(*BufPane) bool {
// LuaAction makes an action from a lua function. It returns either a BufKeyAction
// or a BufMouseAction depending on the event type.
func LuaAction(fn string, k Event) BufAction {
luaFn := strings.Split(fn, ".")
if len(luaFn) <= 1 {
return nil
@ -55,33 +57,42 @@ func LuaAction(fn string) func(*BufPane) bool {
if pl == nil {
return nil
}
return func(h *BufPane) bool {
val, err := pl.Call(plFn, luar.New(ulua.L, h))
if err != nil {
screen.TermMessage(err)
}
if v, ok := val.(lua.LBool); !ok {
return false
} else {
return bool(v)
}
var action BufAction
switch k.(type) {
case KeyEvent, KeySequenceEvent, RawEvent:
action = BufKeyAction(func(h *BufPane) bool {
val, err := pl.Call(plFn, luar.New(ulua.L, h))
if err != nil {
screen.TermMessage(err)
}
if v, ok := val.(lua.LBool); !ok {
return false
} else {
return bool(v)
}
})
case MouseEvent:
action = BufMouseAction(func(h *BufPane, te *tcell.EventMouse) bool {
val, err := pl.Call(plFn, luar.New(ulua.L, h), luar.New(ulua.L, te))
if err != nil {
screen.TermMessage(err)
}
if v, ok := val.(lua.LBool); !ok {
return false
} else {
return bool(v)
}
})
}
return action
}
// BufMapKey maps an event to an action
// BufMapEvent maps an event to an action
func BufMapEvent(k Event, action string) {
config.Bindings["buffer"][k.Name()] = action
switch e := k.(type) {
case KeyEvent, KeySequenceEvent, RawEvent:
bufMapKey(e, action)
case MouseEvent:
bufMapMouse(e, action)
}
}
func bufMapKey(k Event, action string) {
var actionfns []func(*BufPane) bool
var actionfns []BufAction
var names []string
var types []byte
for i := 0; ; i++ {
@ -89,9 +100,7 @@ func bufMapKey(k Event, action string) {
break
}
// TODO: fix problem when complex bindings have these
// characters (escape them?)
idx := strings.IndexAny(action, "&|,")
idx := util.IndexAnyUnquoted(action, "&|,")
a := action
if idx >= 0 {
a = action[:idx]
@ -102,7 +111,7 @@ func bufMapKey(k Event, action string) {
action = ""
}
var afn func(*BufPane) bool
var afn BufAction
if strings.HasPrefix(a, "command:") {
a = strings.SplitN(a, ":", 2)[1]
afn = CommandAction(a)
@ -113,7 +122,7 @@ func bufMapKey(k Event, action string) {
names = append(names, "")
} else if strings.HasPrefix(a, "lua:") {
a = strings.SplitN(a, ":", 2)[1]
afn = LuaAction(a)
afn = LuaAction(a, k)
if afn == nil {
screen.TermMessage("Lua Error:", a, "does not exist")
continue
@ -129,47 +138,52 @@ func bufMapKey(k Event, action string) {
} else if f, ok := BufKeyActions[a]; ok {
afn = f
names = append(names, a)
} else if f, ok := BufMouseActions[a]; ok {
afn = f
names = append(names, a)
} else {
screen.TermMessage("Error in bindings: action", a, "does not exist")
continue
}
actionfns = append(actionfns, afn)
}
bufAction := func(h *BufPane) bool {
cursors := h.Buf.GetCursors()
success := true
bufAction := func(h *BufPane, te *tcell.EventMouse) bool {
for i, a := range actionfns {
innerSuccess := true
for j, c := range cursors {
if c == nil {
continue
}
h.Buf.SetCurCursor(c.Num)
h.Cursor = c
if i == 0 || (success && types[i-1] == '&') || (!success && types[i-1] == '|') || (types[i-1] == ',') {
innerSuccess = innerSuccess && h.execAction(a, names[i], j)
} else {
break
var success bool
if _, ok := MultiActions[names[i]]; ok {
success = true
for _, c := range h.Buf.GetCursors() {
h.Buf.SetCurCursor(c.Num)
h.Cursor = c
success = success && h.execAction(a, names[i], te)
}
} else {
h.Buf.SetCurCursor(0)
h.Cursor = h.Buf.GetActiveCursor()
success = h.execAction(a, names[i], te)
}
// if the action changed the current pane, update the reference
h = MainTab().CurPane()
success = innerSuccess
if h == nil {
// stop, in case the current pane is not a BufPane
break
}
if (!success && types[i] == '&') || (success && types[i] == '|') {
break
}
}
return true
}
BufBindings.RegisterKeyBinding(k, BufKeyActionGeneral(bufAction))
}
// BufMapMouse maps a mouse event to an action
func bufMapMouse(k MouseEvent, action string) {
if f, ok := BufMouseActions[action]; ok {
BufBindings.RegisterMouseBinding(k, BufMouseActionGeneral(f))
} else {
// TODO
// delete(BufMouseBindings, k)
bufMapKey(k, action)
switch e := k.(type) {
case KeyEvent, KeySequenceEvent, RawEvent:
BufBindings.RegisterKeyBinding(e, BufKeyActionGeneral(func(h *BufPane) bool {
return bufAction(h, nil)
}))
case MouseEvent:
BufBindings.RegisterMouseBinding(e, BufMouseActionGeneral(bufAction))
}
}
@ -200,32 +214,31 @@ type BufPane struct {
// Cursor is the currently active buffer cursor
Cursor *buffer.Cursor
// Since tcell doesn't differentiate between a mouse release event
// and a mouse move event with no keys pressed, we need to keep
// track of whether or not the mouse was pressed (or not released) last event to determine
// mouse release events
mouseReleased bool
// Since tcell doesn't differentiate between a mouse press event
// and a mouse move event with button pressed (nor between a mouse
// release event and a mouse move event with no buttons pressed),
// we need to keep track of whether or not the mouse was previously
// pressed, to determine mouse release and mouse drag events.
// Moreover, since in case of a release event tcell doesn't tell us
// which button was released, we need to keep track of which
// (possibly multiple) buttons were pressed previously.
mousePressed map[MouseEvent]bool
// We need to keep track of insert key press toggle
isOverwriteMode bool
// This stores when the last click was
// This is useful for detecting double and triple clicks
lastClickTime time.Time
lastLoc buffer.Loc
// lastCutTime stores when the last ctrl+k was issued.
// It is used for clearing the clipboard to replace it with fresh cut lines.
lastCutTime time.Time
// freshClip returns true if the clipboard has never been pasted.
// freshClip returns true if one or more lines have been cut to the clipboard
// and have never been pasted yet.
freshClip bool
// Was the last mouse event actually a double click?
// Useful for detecting triple clicks -- if a double click is detected
// but the last mouse event was actually a double click, it's a triple click
doubleClick bool
DoubleClick bool
// 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
// based on selection (false for selection, true for word)
@ -250,7 +263,7 @@ func newBufPane(buf *buffer.Buffer, win display.BWindow, tab *Tab) *BufPane {
h.tab = tab
h.Cursor = h.Buf.GetActiveCursor()
h.mouseReleased = true
h.mousePressed = make(map[MouseEvent]bool)
return h
}
@ -276,7 +289,11 @@ func NewBufPaneFromBuf(buf *buffer.Buffer, tab *Tab) *BufPane {
func (h *BufPane) finishInitialize() {
h.initialRelocate()
h.initialized = true
config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
err := config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
if err != nil {
screen.TermMessage(err)
}
}
// Resize resizes the pane
@ -304,7 +321,7 @@ func (h *BufPane) ResizePane(size int) {
}
// PluginCB calls all plugin callbacks with a certain name and displays an
// error if there is one and returns the aggregrate boolean response
// error if there is one and returns the aggregate boolean response
func (h *BufPane) PluginCB(cb string) bool {
b, err := config.RunPluginFnBool(h.Buf.Settings, cb, luar.New(ulua.L, h))
if err != nil {
@ -322,6 +339,12 @@ func (h *BufPane) PluginCBRune(cb string, r rune) bool {
return b
}
func (h *BufPane) resetMouse() {
for me := range h.mousePressed {
delete(h.mousePressed, me)
}
}
// OpenBuffer opens the given buffer in this pane.
func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
h.Buf.Close()
@ -332,10 +355,7 @@ func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
h.initialRelocate()
// Set mouseReleased to true because we assume the mouse is not being
// pressed when the editor is opened
h.mouseReleased = true
// Set isOverwriteMode to false, because we assume we are in the default
// mode when editor is opened
h.isOverwriteMode = false
h.resetMouse()
h.lastClickTime = time.Time{}
}
@ -395,6 +415,12 @@ func (h *BufPane) Name() string {
return n
}
// ReOpen reloads the file opened in the bufpane from disk
func (h *BufPane) ReOpen() {
h.Buf.ReOpen()
h.Relocate()
}
func (h *BufPane) getReloadSetting() string {
reloadSetting := h.Buf.Settings["reload"]
return reloadSetting.(string)
@ -413,11 +439,11 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
if !yes || canceled {
h.Buf.UpdateModTime()
} else {
h.Buf.ReOpen()
h.ReOpen()
}
})
} else if reload == "auto" {
h.Buf.ReOpen()
h.ReOpen()
} else if reload == "disabled" {
h.Buf.DisableReload()
} else {
@ -435,61 +461,44 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
h.paste(e.Text())
h.Relocate()
case *tcell.EventKey:
ke := KeyEvent{
code: e.Key(),
mod: metaToAlt(e.Modifiers()),
r: e.Rune(),
}
ke := keyEvent(e)
done := h.DoKeyEvent(ke)
if !done && e.Key() == tcell.KeyRune {
h.DoRuneInsert(e.Rune())
}
case *tcell.EventMouse:
cancel := false
switch e.Buttons() {
case tcell.Button1:
_, my := e.Position()
if h.Buf.Type.Kind != buffer.BTInfo.Kind && h.Buf.Settings["statusline"].(bool) && my >= h.GetView().Y+h.GetView().Height-1 {
cancel = true
}
case tcell.ButtonNone:
// Mouse event with no click
if !h.mouseReleased {
// Mouse was just released
// mx, my := e.Position()
// mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my})
// we could finish the selection based on the release location as described
// below but when the mouse click is within the scroll margin this will
// cause a scroll and selection even for a simple mouse click which is
// not good
// for terminals that don't support mouse motion events, selection via
// the mouse won't work but this is ok
// Relocating here isn't really necessary because the cursor will
// be in the right place from the last mouse event
// However, if we are running in a terminal that doesn't support mouse motion
// events, this still allows the user to make selections, except only after they
// release the mouse
// if !h.doubleClick && !h.tripleClick {
// h.Cursor.SetSelectionEnd(h.Cursor.Loc)
// }
if h.Cursor.HasSelection() {
h.Cursor.CopySelection(clipboard.PrimaryReg)
}
h.mouseReleased = true
}
}
if !cancel {
if e.Buttons() != tcell.ButtonNone {
me := MouseEvent{
btn: e.Buttons(),
mod: metaToAlt(e.Modifiers()),
btn: e.Buttons(),
mod: metaToAlt(e.Modifiers()),
state: MousePress,
}
isDrag := len(h.mousePressed) > 0
if e.Buttons() & ^(tcell.WheelUp|tcell.WheelDown|tcell.WheelLeft|tcell.WheelRight) != tcell.ButtonNone {
h.mousePressed[me] = true
}
if isDrag {
me.state = MouseDrag
}
h.DoMouseEvent(me, e)
} else {
// Mouse event with no click - mouse was just released.
// If there were multiple mouse buttons pressed, we don't know which one
// was actually released, so we assume they all were released.
pressed := len(h.mousePressed) > 0
for me := range h.mousePressed {
delete(h.mousePressed, me)
me.state = MouseRelease
h.DoMouseEvent(me, e)
}
if !pressed {
// Propagate the mouse release in case the press wasn't for this BufPane
Tabs.ResetMouse()
}
}
}
h.Buf.MergeCursors()
@ -509,6 +518,14 @@ func (h *BufPane) HandleEvent(event tcell.Event) {
InfoBar.ClearGutter()
}
}
cursors := h.Buf.GetCursors()
for _, c := range cursors {
if c.NewTrailingWsY != c.Y && (!c.HasSelection() ||
(c.NewTrailingWsY != c.CurSelection[0].Y && c.NewTrailingWsY != c.CurSelection[1].Y)) {
c.NewTrailingWsY = -1
}
}
}
// Bindings returns the current bindings tree for this buffer.
@ -520,7 +537,10 @@ func (h *BufPane) Bindings() *KeyTree {
}
// DoKeyEvent executes a key event by finding the action it is bound
// to and executing it (possibly multiple times for multiple cursors)
// to and executing it (possibly multiple times for multiple cursors).
// Returns true if the action was executed OR if there are more keys
// remaining to process before executing an action (if this is a key
// sequence event). Returns false if no action found.
func (h *BufPane) DoKeyEvent(e Event) bool {
binds := h.Bindings()
action, more := binds.NextEvent(e, nil)
@ -534,30 +554,33 @@ func (h *BufPane) DoKeyEvent(e Event) bool {
return more
}
func (h *BufPane) execAction(action func(*BufPane) bool, name string, cursor int) bool {
func (h *BufPane) execAction(action BufAction, name string, te *tcell.EventMouse) bool {
if name != "Autocomplete" && name != "CycleAutocompleteBack" {
h.Buf.HasSuggestions = false
}
_, isMulti := MultiActions[name]
if (!isMulti && cursor == 0) || isMulti {
if h.PluginCB("pre" + name) {
success := action(h)
success = success && h.PluginCB("on"+name)
if !h.PluginCB("pre" + name) {
return false
}
if isMulti {
if recordingMacro {
if name != "ToggleMacro" && name != "PlayMacro" {
curmacro = append(curmacro, action)
}
}
var success bool
switch a := action.(type) {
case BufKeyAction:
success = a(h)
case BufMouseAction:
success = a(h, te)
}
success = success && h.PluginCB("on"+name)
if _, ok := MultiActions[name]; ok {
if recordingMacro {
if name != "ToggleMacro" && name != "PlayMacro" {
curmacro = append(curmacro, action)
}
return success
}
}
return false
return success
}
func (h *BufPane) completeAction(action string) {
@ -611,7 +634,7 @@ func (h *BufPane) DoRuneInsert(r rune) {
c.ResetSelection()
}
if h.isOverwriteMode {
if h.Buf.OverwriteMode {
next := c.Loc
next.X++
h.Buf.Replace(c.Loc, next, string(r))
@ -629,20 +652,28 @@ func (h *BufPane) DoRuneInsert(r rune) {
// VSplitIndex opens the given buffer in a vertical split on the given side.
func (h *BufPane) VSplitIndex(buf *buffer.Buffer, right bool) *BufPane {
e := NewBufPaneFromBuf(buf, h.tab)
e.splitID = MainTab().GetNode(h.splitID).VSplit(right)
MainTab().Panes = append(MainTab().Panes, e)
MainTab().Resize()
MainTab().SetActive(len(MainTab().Panes) - 1)
e.splitID = h.tab.GetNode(h.splitID).VSplit(right)
currentPaneIdx := h.tab.GetPane(h.splitID)
if right {
currentPaneIdx++
}
h.tab.AddPane(e, currentPaneIdx)
h.tab.Resize()
h.tab.SetActive(currentPaneIdx)
return e
}
// HSplitIndex opens the given buffer in a horizontal split on the given side.
func (h *BufPane) HSplitIndex(buf *buffer.Buffer, bottom bool) *BufPane {
e := NewBufPaneFromBuf(buf, h.tab)
e.splitID = MainTab().GetNode(h.splitID).HSplit(bottom)
MainTab().Panes = append(MainTab().Panes, e)
MainTab().Resize()
MainTab().SetActive(len(MainTab().Panes) - 1)
e.splitID = h.tab.GetNode(h.splitID).HSplit(bottom)
currentPaneIdx := h.tab.GetPane(h.splitID)
if bottom {
currentPaneIdx++
}
h.tab.AddPane(e, currentPaneIdx)
h.tab.Resize()
h.tab.SetActive(currentPaneIdx)
return e
}
@ -663,6 +694,10 @@ func (h *BufPane) Close() {
// SetActive marks this pane as active.
func (h *BufPane) SetActive(b bool) {
if h.IsActive() == b {
return
}
h.BWindow.SetActive(b)
if b {
// Display any gutter messages for this line
@ -678,8 +713,12 @@ func (h *BufPane) SetActive(b bool) {
if none && InfoBar.HasGutter {
InfoBar.ClearGutter()
}
}
err := config.RunPluginFn("onSetActive", luar.New(ulua.L, h))
if err != nil {
screen.TermMessage(err)
}
}
}
// BufKeyActions contains the list of all possible key actions the bufhandler could execute
@ -692,6 +731,9 @@ var BufKeyActions = map[string]BufKeyAction{
"CursorRight": (*BufPane).CursorRight,
"CursorStart": (*BufPane).CursorStart,
"CursorEnd": (*BufPane).CursorEnd,
"CursorToViewTop": (*BufPane).CursorToViewTop,
"CursorToViewCenter": (*BufPane).CursorToViewCenter,
"CursorToViewBottom": (*BufPane).CursorToViewBottom,
"SelectToStart": (*BufPane).SelectToStart,
"SelectToEnd": (*BufPane).SelectToEnd,
"SelectUp": (*BufPane).SelectUp,
@ -700,10 +742,16 @@ var BufKeyActions = map[string]BufKeyAction{
"SelectRight": (*BufPane).SelectRight,
"WordRight": (*BufPane).WordRight,
"WordLeft": (*BufPane).WordLeft,
"SubWordRight": (*BufPane).SubWordRight,
"SubWordLeft": (*BufPane).SubWordLeft,
"SelectWordRight": (*BufPane).SelectWordRight,
"SelectWordLeft": (*BufPane).SelectWordLeft,
"SelectSubWordRight": (*BufPane).SelectSubWordRight,
"SelectSubWordLeft": (*BufPane).SelectSubWordLeft,
"DeleteWordRight": (*BufPane).DeleteWordRight,
"DeleteWordLeft": (*BufPane).DeleteWordLeft,
"DeleteSubWordRight": (*BufPane).DeleteSubWordRight,
"DeleteSubWordLeft": (*BufPane).DeleteSubWordLeft,
"SelectLine": (*BufPane).SelectLine,
"SelectToStartOfLine": (*BufPane).SelectToStartOfLine,
"SelectToStartOfText": (*BufPane).SelectToStartOfText,
@ -711,6 +759,8 @@ var BufKeyActions = map[string]BufKeyAction{
"SelectToEndOfLine": (*BufPane).SelectToEndOfLine,
"ParagraphPrevious": (*BufPane).ParagraphPrevious,
"ParagraphNext": (*BufPane).ParagraphNext,
"SelectToParagraphPrevious": (*BufPane).SelectToParagraphPrevious,
"SelectToParagraphNext": (*BufPane).SelectToParagraphNext,
"InsertNewline": (*BufPane).InsertNewline,
"Backspace": (*BufPane).Backspace,
"Delete": (*BufPane).Delete,
@ -731,6 +781,7 @@ var BufKeyActions = map[string]BufKeyAction{
"CopyLine": (*BufPane).CopyLine,
"Cut": (*BufPane).Cut,
"CutLine": (*BufPane).CutLine,
"Duplicate": (*BufPane).Duplicate,
"DuplicateLine": (*BufPane).DuplicateLine,
"DeleteLine": (*BufPane).DeleteLine,
"MoveLinesUp": (*BufPane).MoveLinesUp,
@ -763,6 +814,7 @@ var BufKeyActions = map[string]BufKeyAction{
"ToggleRuler": (*BufPane).ToggleRuler,
"ToggleHighlightSearch": (*BufPane).ToggleHighlightSearch,
"UnhighlightSearch": (*BufPane).UnhighlightSearch,
"ResetSearch": (*BufPane).ResetSearch,
"ClearStatus": (*BufPane).ClearStatus,
"ShellMode": (*BufPane).ShellMode,
"CommandMode": (*BufPane).CommandMode,
@ -774,8 +826,12 @@ var BufKeyActions = map[string]BufKeyAction{
"AddTab": (*BufPane).AddTab,
"PreviousTab": (*BufPane).PreviousTab,
"NextTab": (*BufPane).NextTab,
"FirstTab": (*BufPane).FirstTab,
"LastTab": (*BufPane).LastTab,
"NextSplit": (*BufPane).NextSplit,
"PreviousSplit": (*BufPane).PreviousSplit,
"FirstSplit": (*BufPane).FirstSplit,
"LastSplit": (*BufPane).LastSplit,
"Unsplit": (*BufPane).Unsplit,
"VSplit": (*BufPane).VSplitAction,
"HSplit": (*BufPane).HSplitAction,
@ -791,6 +847,7 @@ var BufKeyActions = map[string]BufKeyAction{
"RemoveMultiCursor": (*BufPane).RemoveMultiCursor,
"RemoveAllMultiCursors": (*BufPane).RemoveAllMultiCursors,
"SkipMultiCursor": (*BufPane).SkipMultiCursor,
"SkipMultiCursorBack": (*BufPane).SkipMultiCursorBack,
"JumpToMatchingBrace": (*BufPane).JumpToMatchingBrace,
"JumpLine": (*BufPane).JumpLine,
"Deselect": (*BufPane).Deselect,
@ -804,6 +861,8 @@ var BufKeyActions = map[string]BufKeyAction{
// BufMouseActions contains the list of all possible mouse actions the bufhandler could execute
var BufMouseActions = map[string]BufMouseAction{
"MousePress": (*BufPane).MousePress,
"MouseDrag": (*BufPane).MouseDrag,
"MouseRelease": (*BufPane).MouseRelease,
"MouseMultiCursor": (*BufPane).MouseMultiCursor,
}
@ -828,10 +887,16 @@ var MultiActions = map[string]bool{
"SelectRight": true,
"WordRight": true,
"WordLeft": true,
"SubWordRight": true,
"SubWordLeft": true,
"SelectWordRight": true,
"SelectWordLeft": true,
"SelectSubWordRight": true,
"SelectSubWordLeft": true,
"DeleteWordRight": true,
"DeleteWordLeft": true,
"DeleteSubWordRight": true,
"DeleteSubWordLeft": true,
"SelectLine": true,
"SelectToStartOfLine": true,
"SelectToStartOfText": true,
@ -849,6 +914,7 @@ var MultiActions = map[string]bool{
"Copy": true,
"Cut": true,
"CutLine": true,
"Duplicate": true,
"DuplicateLine": true,
"DeleteLine": true,
"MoveLinesUp": true,

View File

@ -7,6 +7,7 @@ import (
"os"
"os/exec"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
@ -41,6 +42,7 @@ func InitCommands() {
"unbind": {(*BufPane).UnbindCmd, nil},
"quit": {(*BufPane).QuitCmd, nil},
"goto": {(*BufPane).GotoCmd, nil},
"jump": {(*BufPane).JumpCmd, nil},
"save": {(*BufPane).SaveCmd, nil},
"replace": {(*BufPane).ReplaceCmd, nil},
"replaceall": {(*BufPane).ReplaceAllCmd, nil},
@ -137,23 +139,25 @@ func (h *BufPane) TextFilterCmd(args []string) {
InfoBar.Error("usage: textfilter arguments")
return
}
sel := h.Cursor.GetSelection()
if len(sel) == 0 {
h.Cursor.SelectWord()
sel = h.Cursor.GetSelection()
for _, c := range h.Buf.GetCursors() {
sel := c.GetSelection()
if len(sel) == 0 {
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
@ -197,10 +201,11 @@ func (h *BufPane) TabMoveCmd(args []string) {
idxTo = util.Clamp(idxTo, 0, len(Tabs.List)-1)
activeTab := Tabs.List[idxFrom]
Tabs.RemoveTab(activeTab.ID())
Tabs.RemoveTab(activeTab.Panes[0].ID())
Tabs.List = append(Tabs.List, nil)
copy(Tabs.List[idxTo+1:], Tabs.List[idxTo:])
Tabs.List[idxTo] = activeTab
Tabs.Resize()
Tabs.UpdateNames()
Tabs.SetActive(idxTo)
// InfoBar.Message(fmt.Sprintf("Moved tab from slot %d to %d", idxFrom+1, idxTo+1))
@ -303,15 +308,8 @@ func (h *BufPane) OpenCmd(args []string) {
}
h.OpenBuffer(b)
}
if h.Buf.Modified() {
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
if !canceled && !yes {
open()
} else if !canceled && yes {
h.Save()
open()
}
})
if h.Buf.Modified() && !h.Buf.Shared() {
h.closePrompt("Save", open)
} else {
open()
}
@ -329,31 +327,84 @@ func (h *BufPane) ToggleLogCmd(args []string) {
}
}
// ReloadCmd reloads all files (syntax files, colorschemes...)
// ReloadCmd reloads all files (syntax files, colorschemes, plugins...)
func (h *BufPane) ReloadCmd(args []string) {
ReloadConfig()
reloadRuntime(true)
}
// ReloadConfig reloads only the configuration
func ReloadConfig() {
config.InitRuntimeFiles()
reloadRuntime(false)
}
func reloadRuntime(reloadPlugins bool) {
if reloadPlugins {
err := config.RunPluginFn("deinit")
if err != nil {
screen.TermMessage(err)
}
}
config.InitRuntimeFiles(true)
if reloadPlugins {
config.InitPlugins()
}
err := config.ReadSettings()
if err != nil {
screen.TermMessage(err)
} else {
parsedSettings := config.ParsedSettings()
defaultSettings := config.DefaultAllSettings()
for k := range defaultSettings {
if _, ok := config.VolatileSettings[k]; ok {
// reload should not override volatile settings
continue
}
if _, ok := parsedSettings[k]; ok {
err = doSetGlobalOptionNative(k, parsedSettings[k])
} else {
err = doSetGlobalOptionNative(k, defaultSettings[k])
}
if err != nil {
screen.TermMessage(err)
}
}
}
err = config.InitGlobalSettings()
if err != nil {
screen.TermMessage(err)
if reloadPlugins {
err = config.LoadAllPlugins()
if err != nil {
screen.TermMessage(err)
}
}
InitBindings()
InitCommands()
if reloadPlugins {
err = config.RunPluginFn("preinit")
if err != nil {
screen.TermMessage(err)
}
err = config.RunPluginFn("init")
if err != nil {
screen.TermMessage(err)
}
err = config.RunPluginFn("postinit")
if err != nil {
screen.TermMessage(err)
}
}
err = config.InitColorscheme()
if err != nil {
screen.TermMessage(err)
}
for _, b := range buffer.OpenBuffers {
b.UpdateRules()
b.ReloadSettings(true)
}
}
@ -363,50 +414,93 @@ func (h *BufPane) ReopenCmd(args []string) {
InfoBar.YNPrompt("Save file before reopen?", func(yes, canceled bool) {
if !canceled && yes {
h.Save()
h.Buf.ReOpen()
h.ReOpen()
} else if !canceled {
h.Buf.ReOpen()
h.ReOpen()
}
})
} else {
h.Buf.ReOpen()
h.ReOpen()
}
}
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 {
return errors.New(fmt.Sprint("Unable to load help text", page, "\n", err))
return errors.New(fmt.Sprintf("Unable to load help text for %s: %v", page, err))
} else {
helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
helpBuffer.SetName("Help " + page)
helpBuffer.SetOptionNative("hltaberrors", false)
helpBuffer.SetOptionNative("hltrailingws", false)
if h.Buf.Type == buffer.BTHelp {
if h.Buf.Type == buffer.BTHelp && !forceSplit {
h.OpenBuffer(helpBuffer)
} else {
} else if hsplit {
h.HSplitBuf(helpBuffer)
} else {
h.VSplitBuf(helpBuffer)
}
}
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) {
hsplit := config.GlobalSettings["helpsplit"] == "hsplit"
if len(args) < 1 {
// Open the default help if the user just typed "> help"
h.openHelp("help")
h.openHelp("help", hsplit, false)
} else {
if config.FindRuntimeFile(config.RTHelp, args[0]) != nil {
err := h.openHelp(args[0])
if err != nil {
InfoBar.Error(err)
var topics []string
forceSplit := false
const errSplit = "hsplit and vsplit are not allowed at the same time"
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
func (h *BufPane) VSplitCmd(args []string) {
if len(args) == 0 {
@ -415,16 +509,18 @@ func (h *BufPane) VSplitCmd(args []string) {
return
}
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
if err != nil {
InfoBar.Error(err)
return
}
for _, a := range args {
buf, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
if err != nil {
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
func (h *BufPane) HSplitCmd(args []string) {
if len(args) == 0 {
@ -433,13 +529,15 @@ func (h *BufPane) HSplitCmd(args []string) {
return
}
buf, err := buffer.NewBufferFromFile(args[0], buffer.BTDefault)
if err != nil {
InfoBar.Error(err)
return
}
for _, a := range args {
buf, err := buffer.NewBufferFromFile(a, buffer.BTDefault)
if err != nil {
InfoBar.Error(err)
return
}
h.HSplitBuf(buf)
h.HSplitBuf(buf)
}
}
// EvalCmd evaluates a lua expression
@ -447,7 +545,8 @@ func (h *BufPane) EvalCmd(args []string) {
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) {
width, height := screen.Screen.Size()
iOffset := config.GetInfoBarOffset()
@ -470,73 +569,98 @@ func (h *BufPane) NewTabCmd(args []string) {
}
}
func SetGlobalOptionNative(option string, nativeValue interface{}) error {
local := false
for _, s := range config.LocalSettings {
if s == option {
local = true
break
}
func doSetGlobalOptionNative(option string, nativeValue interface{}) error {
if reflect.DeepEqual(config.GlobalSettings[option], nativeValue) {
return nil
}
if !local {
config.GlobalSettings[option] = nativeValue
config.ModifiedSettings[option] = true
config.GlobalSettings[option] = nativeValue
config.ModifiedSettings[option] = true
delete(config.VolatileSettings, option)
if option == "colorscheme" {
// LoadSyntaxFiles()
config.InitColorscheme()
for _, b := range buffer.OpenBuffers {
b.UpdateRules()
}
} else if option == "infobar" || option == "keymenu" {
Tabs.Resize()
} else if option == "mouse" {
if !nativeValue.(bool) {
screen.Screen.DisableMouse()
} else {
screen.Screen.EnableMouse()
}
} else if option == "autosave" {
if nativeValue.(float64) > 0 {
config.SetAutoTime(int(nativeValue.(float64)))
config.StartAutoSave()
} else {
config.SetAutoTime(0)
}
} else if option == "paste" {
screen.Screen.SetPaste(nativeValue.(bool))
} else if option == "clipboard" {
m := clipboard.SetMethod(nativeValue.(string))
err := clipboard.Initialize(m)
if err != nil {
return err
}
if option == "colorscheme" {
// LoadSyntaxFiles()
config.InitColorscheme()
for _, b := range buffer.OpenBuffers {
b.UpdateRules()
}
} else if option == "infobar" || option == "keymenu" {
Tabs.Resize()
} else if option == "mouse" {
if !nativeValue.(bool) {
screen.Screen.DisableMouse()
} else {
for _, pl := range config.Plugins {
if option == pl.Name {
if nativeValue.(bool) && !pl.Loaded {
pl.Load()
_, err := pl.Call("init")
if err != nil && err != config.ErrNoSuchFunction {
screen.TermMessage(err)
}
} else if !nativeValue.(bool) && pl.Loaded {
_, err := pl.Call("deinit")
if err != nil && err != config.ErrNoSuchFunction {
screen.TermMessage(err)
}
screen.Screen.EnableMouse()
}
} else if option == "autosave" {
if nativeValue.(float64) > 0 {
config.SetAutoTime(nativeValue.(float64))
} else {
config.SetAutoTime(0)
}
} else if option == "paste" {
screen.Screen.SetPaste(nativeValue.(bool))
} else if option == "clipboard" {
m := clipboard.SetMethod(nativeValue.(string))
err := clipboard.Initialize(m)
if err != nil {
return err
}
} else {
for _, pl := range config.Plugins {
if option == pl.Name {
if nativeValue.(bool) && !pl.Loaded {
pl.Load()
_, err := pl.Call("init")
if err != nil && err != config.ErrNoSuchFunction {
screen.TermMessage(err)
}
} else if !nativeValue.(bool) && pl.Loaded {
_, err := pl.Call("deinit")
if err != nil && err != config.ErrNoSuchFunction {
screen.TermMessage(err)
}
}
}
}
}
for _, b := range buffer.OpenBuffers {
b.SetOptionNative(option, nativeValue)
return nil
}
func SetGlobalOptionNative(option string, nativeValue interface{}) error {
if err := config.OptionIsValid(option, nativeValue); err != nil {
return err
}
return config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json"))
// check for local option first...
for _, s := range config.LocalSettings {
if s == option {
return MainTab().CurPane().Buf.SetOptionNative(option, nativeValue)
}
}
// ...if it's not local continue with the globals...
if err := doSetGlobalOptionNative(option, nativeValue); err != nil {
return err
}
// ...at last check the buffer locals
for _, b := range buffer.OpenBuffers {
b.DoSetOptionNative(option, nativeValue)
delete(b.LocalSettings, option)
}
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 {
@ -560,16 +684,10 @@ func (h *BufPane) ResetCmd(args []string) {
}
option := args[0]
defaults := config.DefaultAllSettings()
defaultGlobals := config.DefaultGlobalSettings()
defaultLocals := config.DefaultCommonSettings()
if _, ok := defaultGlobals[option]; ok {
SetGlobalOptionNative(option, defaultGlobals[option])
return
}
if _, ok := defaultLocals[option]; ok {
h.Buf.SetOptionNative(option, defaultLocals[option])
if _, ok := defaults[option]; ok {
SetGlobalOptionNative(option, defaults[option])
return
}
InfoBar.Error(config.ErrInvalidOption)
@ -634,6 +752,11 @@ func (h *BufPane) ShowCmd(args []string) {
InfoBar.Message(option)
}
func parseKeyArg(arg string) string {
// If this is a raw escape sequence, convert it to its raw byte form
return strings.ReplaceAll(arg, "\\x1b", "\x1b")
}
// ShowKeyCmd displays the action that a key is bound to
func (h *BufPane) ShowKeyCmd(args []string) {
if len(args) < 1 {
@ -641,7 +764,7 @@ func (h *BufPane) ShowKeyCmd(args []string) {
return
}
event, err := findEvent(args[0])
event, err := findEvent(parseKeyArg(args[0]))
if err != nil {
InfoBar.Error(err)
return
@ -660,9 +783,13 @@ func (h *BufPane) BindCmd(args []string) {
return
}
_, err := TryBindKey(args[0], args[1], true)
_, err := TryBindKey(parseKeyArg(args[0]), args[1], true)
if err != nil {
InfoBar.Error(err)
if errors.Is(err, util.ErrOverwrite) {
screen.TermMessage(err)
} else {
InfoBar.Error(err)
}
}
}
@ -673,9 +800,13 @@ func (h *BufPane) UnbindCmd(args []string) {
return
}
err := UnbindKey(args[0])
err := UnbindKey(parseKeyArg(args[0]))
if err != nil {
InfoBar.Error(err)
if errors.Is(err, util.ErrOverwrite) {
screen.TermMessage(err)
} else {
InfoBar.Error(err)
}
}
}
@ -701,41 +832,67 @@ func (h *BufPane) QuitCmd(args []string) {
// position in the buffer
// For example: `goto line`, or `goto line:col`
func (h *BufPane) GotoCmd(args []string) {
line, col, err := h.parseLineCol(args)
if err != nil {
InfoBar.Error(err)
return
}
if line < 0 {
line = h.Buf.LinesNum() + 1 + line
}
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
h.RemoveAllMultiCursors()
h.Cursor.Deselect(true)
h.GotoLoc(buffer.Loc{col, line})
}
// JumpCmd is a command that will send the cursor to a certain relative
// position in the buffer
// For example: `jump line`, `jump -line`, or `jump -line:col`
func (h *BufPane) JumpCmd(args []string) {
line, col, err := h.parseLineCol(args)
if err != nil {
InfoBar.Error(err)
return
}
line = h.Buf.GetActiveCursor().Y + 1 + line
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
h.RemoveAllMultiCursors()
h.Cursor.Deselect(true)
h.GotoLoc(buffer.Loc{col, line})
}
// parseLineCol is a helper to parse the input of GotoCmd and JumpCmd
func (h *BufPane) parseLineCol(args []string) (line int, col int, err error) {
if len(args) <= 0 {
InfoBar.Error("Not enough arguments")
return 0, 0, errors.New("Not enough arguments")
}
line, col = 0, 0
if strings.Contains(args[0], ":") {
parts := strings.SplitN(args[0], ":", 2)
line, err = strconv.Atoi(parts[0])
if err != nil {
return 0, 0, err
}
col, err = strconv.Atoi(parts[1])
if err != nil {
return 0, 0, err
}
} else {
h.RemoveAllMultiCursors()
if strings.Contains(args[0], ":") {
parts := strings.SplitN(args[0], ":", 2)
line, err := strconv.Atoi(parts[0])
if err != nil {
InfoBar.Error(err)
return
}
col, err := strconv.Atoi(parts[1])
if err != nil {
InfoBar.Error(err)
return
}
if line < 0 {
line = h.Buf.LinesNum() + 1 + line
}
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
col = util.Clamp(col-1, 0, util.CharacterCount(h.Buf.LineBytes(line)))
h.GotoLoc(buffer.Loc{col, line})
} else {
line, err := strconv.Atoi(args[0])
if err != nil {
InfoBar.Error(err)
return
}
if line < 0 {
line = h.Buf.LinesNum() + 1 + line
}
line = util.Clamp(line-1, 0, h.Buf.LinesNum()-1)
h.GotoLoc(buffer.Loc{0, line})
line, err = strconv.Atoi(args[0])
if err != nil {
return 0, 0, err
}
}
return line, col, nil
}
// SaveCmd saves the buffer optionally with an argument file name
@ -743,7 +900,7 @@ func (h *BufPane) SaveCmd(args []string) {
if len(args) == 0 {
h.Save()
} else {
h.Buf.SaveAs(args[0])
h.saveBufToFile(args[0], "SaveAs", nil)
}
}
@ -804,19 +961,21 @@ func (h *BufPane) ReplaceCmd(args []string) {
nreplaced := 0
start := h.Buf.Start()
end := h.Buf.End()
searchLoc := h.Cursor.Loc
selection := h.Cursor.HasSelection()
if selection {
start = h.Cursor.CurSelection[0]
end = h.Cursor.CurSelection[1]
searchLoc = start // otherwise me might start at the end
}
if all {
nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace)
nreplaced, _ = h.Buf.ReplaceRegex(start, end, regex, replace, !noRegex)
} else {
inRange := func(l buffer.Loc) bool {
return l.GreaterEqual(start) && l.LessEqual(end)
}
searchLoc := h.Cursor.Loc
lastMatchEnd := buffer.Loc{-1, -1}
var doReplacement func()
doReplacement = func() {
locs, found, err := h.Buf.FindNext(search, start, end, searchLoc, true, true)
@ -831,6 +990,18 @@ func (h *BufPane) ReplaceCmd(args []string) {
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.SetSelectionEnd(locs[1])
h.GotoLoc(locs[0])
@ -840,7 +1011,7 @@ func (h *BufPane) ReplaceCmd(args []string) {
InfoBar.YNPrompt("Perform replacement (y,n,esc)", func(yes, canceled bool) {
if !canceled && yes {
_, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace)
_, nrunes := h.Buf.ReplaceRegex(locs[0], locs[1], regex, replace, !noRegex)
searchLoc = locs[0]
searchLoc.X += nrunes + locs[0].Diff(locs[1], h.Buf)
@ -856,6 +1027,7 @@ func (h *BufPane) ReplaceCmd(args []string) {
h.Buf.RelocateCursors()
return
}
lastMatchEnd = searchLoc
doReplacement()
})
}
@ -887,10 +1059,42 @@ func (h *BufPane) ReplaceAllCmd(args []string) {
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
func (h *BufPane) TermCmd(args []string) {
ps := h.tab.Panes
if !TermEmuSupported {
InfoBar.Error("Terminal emulator not supported on this system")
return
@ -905,56 +1109,19 @@ func (h *BufPane) TermCmd(args []string) {
args = []string{sh}
}
term := func(i int, newtab bool) {
t := new(shell.Terminal)
err := t.Start(args, false, true, nil, nil)
if err != nil {
InfoBar.Error(err)
return
}
id := h.ID()
if newtab {
h.AddTab()
i = 0
id = MainTab().Panes[0].ID()
} else {
MainTab().Panes[i].Close()
}
v := h.GetView()
tp, err := NewTermPane(v.X, v.Y, v.Width, v.Height, t, id, MainTab())
if err != nil {
InfoBar.Error(err)
return
}
MainTab().Panes[i] = tp
MainTab().SetActive(i)
}
// If there is only one open file we make a new tab instead of overwriting it
newtab := len(MainTab().Panes) == 1 && len(Tabs.List) == 1
if newtab {
term(0, true)
h.openTerm(args, true)
return
}
for i, p := range ps {
if p.ID() == h.ID() {
if h.Buf.Modified() {
InfoBar.YNPrompt("Save changes to "+h.Buf.GetName()+" before closing? (y,n,esc)", func(yes, canceled bool) {
if !canceled && !yes {
term(i, false)
} else if !canceled && yes {
h.Save()
term(i, false)
}
})
} else {
term(i, false)
}
}
if h.Buf.Modified() && !h.Buf.Shared() {
h.closePrompt("Save", func() {
h.openTerm(args, false)
})
} else {
h.openTerm(args, false)
}
}

View File

@ -3,7 +3,7 @@ package action
var termdefaults = map[string]string{
"<Ctrl-q><Ctrl-q>": "Exit",
"<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

View File

@ -45,23 +45,25 @@ var bufdefaults = map[string]string{
"Alt-]": "DiffNext|CursorEnd",
"Ctrl-z": "Undo",
"Ctrl-y": "Redo",
"Ctrl-c": "CopyLine|Copy",
"Ctrl-x": "Cut",
"Ctrl-c": "Copy|CopyLine",
"Ctrl-x": "Cut|CutLine",
"Ctrl-k": "CutLine",
"Ctrl-d": "DuplicateLine",
"Ctrl-d": "Duplicate|DuplicateLine",
"Ctrl-v": "Paste",
"Ctrl-a": "SelectAll",
"Ctrl-t": "AddTab",
"Alt-,": "PreviousTab",
"Alt-.": "NextTab",
"Alt-,": "PreviousTab|LastTab",
"Alt-.": "NextTab|FirstTab",
"Home": "StartOfTextToggle",
"End": "EndOfLine",
"CtrlHome": "CursorStart",
"CtrlEnd": "CursorEnd",
"PageUp": "CursorPageUp",
"PageDown": "CursorPageDown",
"CtrlPageUp": "PreviousTab",
"CtrlPageDown": "NextTab",
"CtrlPageUp": "PreviousTab|LastTab",
"CtrlPageDown": "NextTab|FirstTab",
"ShiftPageUp": "SelectPageUp",
"ShiftPageDown": "SelectPageDown",
"Ctrl-g": "ToggleHelp",
"Alt-g": "ToggleKeyMenu",
"Ctrl-r": "ToggleRuler",
@ -70,7 +72,7 @@ var bufdefaults = map[string]string{
"Ctrl-b": "ShellMode",
"Ctrl-q": "Quit",
"Ctrl-e": "CommandMode",
"Ctrl-w": "NextSplit",
"Ctrl-w": "NextSplit|FirstSplit",
"Ctrl-u": "ToggleMacro",
"Ctrl-j": "PlayMacro",
"Insert": "ToggleOverwriteMode",
@ -92,11 +94,13 @@ var bufdefaults = map[string]string{
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
// Mouse bindings
"MouseWheelUp": "ScrollUp",
"MouseWheelDown": "ScrollDown",
"MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
"MouseWheelUp": "ScrollUp",
"MouseWheelDown": "ScrollDown",
"MouseLeft": "MousePress",
"MouseLeftDrag": "MouseDrag",
"MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
"Alt-n": "SpawnMultiCursor",
"AltShiftUp": "SpawnMultiCursorUp",
@ -142,8 +146,8 @@ var infodefaults = map[string]string{
"Backtab": "CycleAutocompleteBack",
"Ctrl-z": "Undo",
"Ctrl-y": "Redo",
"Ctrl-c": "CopyLine|Copy",
"Ctrl-x": "Cut",
"Ctrl-c": "Copy|CopyLine",
"Ctrl-x": "Cut|CutLine",
"Ctrl-k": "CutLine",
"Ctrl-v": "Paste",
"Home": "StartOfTextToggle",
@ -175,8 +179,10 @@ var infodefaults = map[string]string{
"Esc": "AbortCommand",
// Mouse bindings
"MouseWheelUp": "HistoryUp",
"MouseWheelDown": "HistoryDown",
"MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary",
"MouseWheelUp": "HistoryUp",
"MouseWheelDown": "HistoryDown",
"MouseLeft": "MousePress",
"MouseLeftDrag": "MouseDrag",
"MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
}

View File

@ -48,23 +48,25 @@ var bufdefaults = map[string]string{
"Alt-]": "DiffNext|CursorEnd",
"Ctrl-z": "Undo",
"Ctrl-y": "Redo",
"Ctrl-c": "CopyLine|Copy",
"Ctrl-x": "Cut",
"Ctrl-c": "Copy|CopyLine",
"Ctrl-x": "Cut|CutLine",
"Ctrl-k": "CutLine",
"Ctrl-d": "DuplicateLine",
"Ctrl-d": "Duplicate|DuplicateLine",
"Ctrl-v": "Paste",
"Ctrl-a": "SelectAll",
"Ctrl-t": "AddTab",
"Alt-,": "PreviousTab",
"Alt-.": "NextTab",
"Alt-,": "PreviousTab|LastTab",
"Alt-.": "NextTab|FirstTab",
"Home": "StartOfTextToggle",
"End": "EndOfLine",
"CtrlHome": "CursorStart",
"CtrlEnd": "CursorEnd",
"PageUp": "CursorPageUp",
"PageDown": "CursorPageDown",
"CtrlPageUp": "PreviousTab",
"CtrlPageDown": "NextTab",
"CtrlPageUp": "PreviousTab|LastTab",
"CtrlPageDown": "NextTab|FirstTab",
"ShiftPageUp": "SelectPageUp",
"ShiftPageDown": "SelectPageDown",
"Ctrl-g": "ToggleHelp",
"Alt-g": "ToggleKeyMenu",
"Ctrl-r": "ToggleRuler",
@ -73,7 +75,7 @@ var bufdefaults = map[string]string{
"Ctrl-b": "ShellMode",
"Ctrl-q": "Quit",
"Ctrl-e": "CommandMode",
"Ctrl-w": "NextSplit",
"Ctrl-w": "NextSplit|FirstSplit",
"Ctrl-u": "ToggleMacro",
"Ctrl-j": "PlayMacro",
"Insert": "ToggleOverwriteMode",
@ -95,11 +97,13 @@ var bufdefaults = map[string]string{
"Esc": "Escape,Deselect,ClearInfo,RemoveAllMultiCursors,UnhighlightSearch",
// Mouse bindings
"MouseWheelUp": "ScrollUp",
"MouseWheelDown": "ScrollDown",
"MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
"MouseWheelUp": "ScrollUp",
"MouseWheelDown": "ScrollDown",
"MouseLeft": "MousePress",
"MouseLeftDrag": "MouseDrag",
"MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
"Ctrl-MouseLeft": "MouseMultiCursor",
"Alt-n": "SpawnMultiCursor",
"Alt-m": "SpawnMultiCursorSelect",
@ -145,8 +149,8 @@ var infodefaults = map[string]string{
"Backtab": "CycleAutocompleteBack",
"Ctrl-z": "Undo",
"Ctrl-y": "Redo",
"Ctrl-c": "CopyLine|Copy",
"Ctrl-x": "Cut",
"Ctrl-c": "Copy|CopyLine",
"Ctrl-x": "Cut|CutLine",
"Ctrl-k": "CutLine",
"Ctrl-v": "Paste",
"Home": "StartOfTextToggle",
@ -178,8 +182,10 @@ var infodefaults = map[string]string{
"Esc": "AbortCommand",
// Mouse bindings
"MouseWheelUp": "HistoryUp",
"MouseWheelDown": "HistoryDown",
"MouseLeft": "MousePress",
"MouseMiddle": "PastePrimary",
"MouseWheelUp": "HistoryUp",
"MouseWheelDown": "HistoryDown",
"MouseLeft": "MousePress",
"MouseLeftDrag": "MouseDrag",
"MouseLeftRelease": "MouseRelease",
"MouseMiddle": "PastePrimary",
}

View File

@ -6,7 +6,7 @@ import (
"fmt"
"strings"
"github.com/zyedidia/tcell/v2"
"github.com/micro-editor/tcell/v2"
)
type Event interface {
@ -44,6 +44,17 @@ func metaToAlt(mod tcell.ModMask) tcell.ModMask {
return mod
}
func keyEvent(e *tcell.EventKey) KeyEvent {
ke := KeyEvent{
code: e.Key(),
mod: metaToAlt(e.Modifiers()),
}
if e.Key() == tcell.KeyRune {
ke.r = e.Rune()
}
return ke
}
func (k KeyEvent) Name() string {
if k.any {
return "<any>"
@ -68,7 +79,7 @@ func (k KeyEvent) Name() string {
if k.code == tcell.KeyRune {
s = string(k.r)
} else {
s = fmt.Sprintf("Key[%d,%d]", k.code, int(k.r))
s = fmt.Sprintf("Key[%d]", k.code)
}
}
if len(m) != 0 {
@ -100,11 +111,20 @@ func (k KeySequenceEvent) Name() string {
return buf.String()
}
type MouseState int
const (
MousePress = iota
MouseDrag
MouseRelease
)
// MouseEvent is a mouse event with a mouse button and
// any possible key modifiers
type MouseEvent struct {
btn tcell.ButtonMask
mod tcell.ModMask
btn tcell.ButtonMask
mod tcell.ModMask
state MouseState
}
func (m MouseEvent) Name() string {
@ -122,9 +142,17 @@ func (m MouseEvent) Name() string {
mod = "Ctrl-"
}
state := ""
switch m.state {
case MouseDrag:
state = "Drag"
case MouseRelease:
state = "Release"
}
for k, v := range mouseEvents {
if v == m.btn {
return fmt.Sprintf("%s%s", mod, k)
return fmt.Sprintf("%s%s%s", mod, k, state)
}
}
return ""
@ -138,11 +166,7 @@ func (m MouseEvent) Name() string {
func ConstructEvent(event tcell.Event) (Event, error) {
switch e := event.(type) {
case *tcell.EventKey:
return KeyEvent{
code: e.Key(),
mod: metaToAlt(e.Modifiers()),
r: e.Rune(),
}, nil
return keyEvent(e), nil
case *tcell.EventRaw:
return RawEvent{
esc: e.EscSeq(),

View File

@ -8,6 +8,7 @@ import (
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/micro/v2/pkg/highlight"
)
// This file is meant (for now) for autocompletion in command mode, not
@ -17,7 +18,7 @@ import (
// CommandComplete autocompletes commands
func CommandComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b)
input, argstart := b.GetArg()
var suggestions []string
for cmd := range commands {
@ -38,7 +39,7 @@ func CommandComplete(b *buffer.Buffer) ([]string, []string) {
// HelpComplete autocompletes help topics
func HelpComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b)
input, argstart := b.GetArg()
var suggestions []string
@ -77,19 +78,67 @@ func colorschemeComplete(input string) (string, []string) {
return chosen, suggestions
}
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
// filetypeComplete autocompletes filetype
func filetypeComplete(input string) (string, []string) {
var suggestions []string
// We cannot match filetypes just by names of syntax files,
// since those names may be different from the actual filetype values
// specified inside syntax files (e.g. "c++" filetype in cpp.yaml).
// So we need to parse filetype values out of those files.
for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
data, err := f.Data()
if err != nil {
continue
}
header, err := highlight.MakeHeaderYaml(data)
if err != nil {
continue
}
// Prevent duplicated defaults
if header.FileType == "off" || header.FileType == "unknown" {
continue
}
if strings.HasPrefix(header.FileType, input) {
suggestions = append(suggestions, header.FileType)
}
}
return false
headerLoop:
for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
data, err := f.Data()
if err != nil {
continue
}
header, err := highlight.MakeHeader(data)
if err != nil {
continue
}
for _, v := range suggestions {
if v == header.FileType {
continue headerLoop
}
}
if strings.HasPrefix(header.FileType, input) {
suggestions = append(suggestions, header.FileType)
}
}
if strings.HasPrefix("off", input) {
suggestions = append(suggestions, "off")
}
var chosen string
if len(suggestions) == 1 {
chosen = suggestions[0]
}
return chosen, suggestions
}
// OptionComplete autocompletes options
func OptionComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b)
input, argstart := b.GetArg()
var suggestions []string
for option := range config.GlobalSettings {
@ -116,7 +165,7 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor()
l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X)
input, argstart := buffer.GetArg(b)
input, argstart := b.GetArg()
completeValue := false
args := bytes.Split(l, []byte{' '})
@ -172,13 +221,8 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
switch inputOpt {
case "colorscheme":
_, suggestions = colorschemeComplete(input)
case "fileformat":
if strings.HasPrefix("unix", input) {
suggestions = append(suggestions, "unix")
}
if strings.HasPrefix("dos", input) {
suggestions = append(suggestions, "dos")
}
case "filetype":
_, suggestions = filetypeComplete(input)
case "sucmd":
if strings.HasPrefix("sudo", input) {
suggestions = append(suggestions, "sudo")
@ -186,15 +230,13 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
if strings.HasPrefix("doas", input) {
suggestions = append(suggestions, "doas")
}
case "clipboard":
if strings.HasPrefix("external", input) {
suggestions = append(suggestions, "external")
}
if strings.HasPrefix("internal", input) {
suggestions = append(suggestions, "internal")
}
if strings.HasPrefix("terminal", input) {
suggestions = append(suggestions, "terminal")
default:
if choices, ok := config.OptionChoices[inputOpt]; ok {
for _, choice := range choices {
if strings.HasPrefix(choice, input) {
suggestions = append(suggestions, choice)
}
}
}
}
}
@ -210,7 +252,7 @@ func OptionValueComplete(b *buffer.Buffer) ([]string, []string) {
// PluginCmdComplete autocompletes the plugin command
func PluginCmdComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor()
input, argstart := buffer.GetArg(b)
input, argstart := b.GetArg()
var suggestions []string
for _, cmd := range PluginCmds {
@ -232,7 +274,7 @@ func PluginComplete(b *buffer.Buffer) ([]string, []string) {
c := b.GetActiveCursor()
l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X)
input, argstart := buffer.GetArg(b)
input, argstart := b.GetArg()
completeValue := false
args := bytes.Split(l, []byte{' '})

View File

@ -3,12 +3,12 @@ package action
import (
"bytes"
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/display"
"github.com/zyedidia/micro/v2/internal/info"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2"
)
type InfoKeyAction func(*InfoPane)
@ -83,22 +83,22 @@ func (h *InfoPane) Close() {
func (h *InfoPane) HandleEvent(event tcell.Event) {
switch e := event.(type) {
case *tcell.EventResize:
// TODO
case *tcell.EventKey:
ke := KeyEvent{
code: e.Key(),
mod: metaToAlt(e.Modifiers()),
r: e.Rune(),
}
ke := keyEvent(e)
done := h.DoKeyEvent(ke)
hasYN := h.HasYN
if e.Key() == tcell.KeyRune && hasYN {
if (e.Rune() == 'y' || e.Rune() == 'Y') && hasYN {
h.YNResp = true
h.DonePrompt(false)
} else if (e.Rune() == 'n' || e.Rune() == 'N') && hasYN {
h.YNResp = false
y := e.Rune() == 'y' || e.Rune() == 'Y'
n := e.Rune() == 'n' || e.Rune() == 'N'
if y || n {
h.YNResp = y
h.DonePrompt(false)
InfoBindings.ResetEvents()
InfoBufBindings.ResetEvents()
}
}
if e.Key() == tcell.KeyRune && !done && !hasYN {
@ -122,7 +122,10 @@ func (h *InfoPane) HandleEvent(event tcell.Event) {
}
}
// DoKeyEvent executes a key event for the command bar, doing any overridden actions
// DoKeyEvent executes a key event for the command bar, doing any overridden actions.
// Returns true if the action was executed OR if there are more keys remaining
// to process before executing an action (if this is a key sequence event).
// Returns false if no action found.
func (h *InfoPane) DoKeyEvent(e KeyEvent) bool {
action, more := InfoBindings.NextEvent(e, nil)
if action != nil && !more {
@ -136,11 +139,25 @@ func (h *InfoPane) DoKeyEvent(e KeyEvent) bool {
}
if !more {
// If no infopane action found, try to find a bufpane action.
//
// TODO: this is buggy. For example, if the command bar has the following
// two bindings:
//
// "<Ctrl-x><Ctrl-p>": "HistoryUp",
// "<Ctrl-x><Ctrl-v>": "Paste",
//
// the 2nd binding (with a bufpane action) doesn't work, since <Ctrl-x>
// has been already consumed by the 1st binding (with an infopane action).
//
// We should either iterate both InfoBindings and InfoBufBindings keytrees
// together, or just use the same keytree for both infopane and bufpane
// bindings.
action, more = InfoBufBindings.NextEvent(e, nil)
if action != nil && !more {
done := action(h.BufPane)
action(h.BufPane)
InfoBufBindings.ResetEvents()
return done
return true
} else if action == nil && !more {
InfoBufBindings.ResetEvents()
}

View File

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

View File

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

View File

@ -3,13 +3,13 @@ package action
import (
luar "layeh.com/gopher-luar"
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/display"
ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/screen"
"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
@ -107,30 +107,32 @@ func (t *TabList) HandleEvent(event tcell.Event) {
mx, my := e.Position()
switch e.Buttons() {
case tcell.Button1:
if my == t.Y && mx == 0 {
t.Scroll(-4)
return
} else if my == t.Y && mx == t.Width-1 {
t.Scroll(4)
if my == t.Y && len(t.List) > 1 {
if mx == 0 {
t.Scroll(-4)
} else if mx == t.Width-1 {
t.Scroll(4)
} else {
ind := t.LocFromVisual(buffer.Loc{mx, my})
if ind != -1 {
t.SetActive(ind)
}
}
return
}
if len(t.List) > 1 {
ind := t.LocFromVisual(buffer.Loc{mx, my})
if ind != -1 {
t.SetActive(ind)
return
}
if my == 0 {
return
}
case tcell.ButtonNone:
if t.List[t.Active()].release {
// Mouse release received, while already released
t.ResetMouse()
return
}
case tcell.WheelUp:
if my == t.Y {
if my == t.Y && len(t.List) > 1 {
t.Scroll(4)
return
}
case tcell.WheelDown:
if my == t.Y {
if my == t.Y && len(t.List) > 1 {
t.Scroll(-4)
return
}
@ -147,6 +149,56 @@ func (t *TabList) Display() {
}
}
func (t *TabList) SetActive(a int) {
t.TabWindow.SetActive(a)
for i, p := range t.List {
if i == a {
if !p.isActive {
p.isActive = true
err := config.RunPluginFn("onSetActive", luar.New(ulua.L, p.CurPane()))
if err != nil {
screen.TermMessage(err)
}
}
} else {
p.isActive = false
}
}
}
// ResetMouse resets the mouse release state after the screen was stopped
// or the pane changed.
// This prevents situations in which mouse releases are received at the wrong place
// and the mouse state is still pressed.
func (t *TabList) ResetMouse() {
for _, tab := range t.List {
if !tab.release && tab.resizing != nil {
tab.resizing = nil
}
tab.release = true
for _, p := range tab.Panes {
if bp, ok := p.(*BufPane); ok {
bp.resetMouse()
}
}
}
}
// CloseTerms notifies term panes that a terminal job has finished.
func (t *TabList) CloseTerms() {
for _, tab := range t.List {
for _, p := range tab.Panes {
if tp, ok := p.(*TermPane); ok {
tp.HandleTermClose()
}
}
}
}
// Tabs is the global tab list
var Tabs *TabList
@ -159,11 +211,13 @@ func InitTabs(bufs []*buffer.Buffer) {
for _, b := range bufs[1:] {
if multiopen == "vsplit" {
MainTab().CurPane().VSplitBuf(b)
} else { // default hsplit
} else { // default hsplit
MainTab().CurPane().HSplitBuf(b)
}
}
}
screen.RestartCallback = Tabs.ResetMouse
}
func MainTab() *Tab {
@ -177,6 +231,9 @@ func MainTab() *Tab {
type Tab struct {
*views.Node
*display.UIWindow
isActive bool
Panes []Pane
active int
@ -214,34 +271,40 @@ func NewTabFromPane(x, y, width, height int, pane Pane) *Tab {
// HandleEvent takes a tcell event and usually dispatches it to the current
// active pane. However if the event is a resize or a mouse event where the user
// is interacting with the UI (resizing splits) then the event is consumed here
// If the event is a mouse event in a pane, that pane will become active and get
// the event
// If the event is a mouse press event in a pane, that pane will become active
// and get the event
func (t *Tab) HandleEvent(event tcell.Event) {
switch e := event.(type) {
case *tcell.EventMouse:
mx, my := e.Position()
switch e.Buttons() {
case tcell.Button1:
btn := e.Buttons()
switch {
case btn & ^(tcell.WheelUp|tcell.WheelDown|tcell.WheelLeft|tcell.WheelRight) != tcell.ButtonNone:
// button press or drag
wasReleased := t.release
t.release = false
if t.resizing != nil {
var size int
if t.resizing.Kind == views.STVert {
size = mx - t.resizing.X
} else {
size = my - t.resizing.Y + 1
if btn == tcell.Button1 {
if t.resizing != nil {
var size int
if t.resizing.Kind == views.STVert {
size = mx - t.resizing.X
} else {
size = my - t.resizing.Y + 1
}
t.resizing.ResizeSplit(size)
t.Resize()
return
}
if wasReleased {
t.resizing = t.GetMouseSplitNode(buffer.Loc{mx, my})
if t.resizing != nil {
return
}
}
t.resizing.ResizeSplit(size)
t.Resize()
return
}
if wasReleased {
t.resizing = t.GetMouseSplitNode(buffer.Loc{mx, my})
if t.resizing != nil {
return
}
for i, p := range t.Panes {
v := p.GetView()
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
@ -251,10 +314,15 @@ func (t *Tab) HandleEvent(event tcell.Event) {
}
}
}
case tcell.ButtonNone:
t.resizing = nil
case btn == tcell.ButtonNone:
// button release
t.release = true
if t.resizing != nil {
t.resizing = nil
return
}
default:
// wheel move
for _, p := range t.Panes {
v := p.GetView()
inpane := mx >= v.X && mx < v.X+v.Width && my >= v.Y && my < v.Y+v.Height
@ -279,11 +347,16 @@ func (t *Tab) SetActive(i int) {
p.SetActive(false)
}
}
}
err := config.RunPluginFn("onSetActive", luar.New(ulua.L, MainTab().CurPane()))
if err != nil {
screen.TermMessage(err)
// 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

View File

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

View File

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

View File

@ -4,13 +4,13 @@ import (
"errors"
"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/config"
"github.com/zyedidia/micro/v2/internal/display"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/tcell/v2"
"github.com/zyedidia/terminal"
)
type TermKeyAction func(*TermPane)
@ -81,6 +81,10 @@ func (t *TermPane) SetID(i uint64) {
t.id = i
}
func (t *TermPane) Name() string {
return t.Terminal.Name()
}
func (t *TermPane) SetTab(tab *Tab) {
t.tab = tab
}
@ -121,11 +125,7 @@ func (t *TermPane) Unsplit() {
// copy-paste
func (t *TermPane) HandleEvent(event tcell.Event) {
if e, ok := event.(*tcell.EventKey); ok {
ke := KeyEvent{
code: e.Key(),
mod: metaToAlt(e.Modifiers()),
r: e.Rune(),
}
ke := keyEvent(e)
action, more := TermBindings.NextEvent(ke, nil)
if !more {
@ -159,9 +159,9 @@ func (t *TermPane) HandleEvent(event tcell.Event) {
if t.Status != shell.TTDone {
t.WriteString(event.EscSeq())
}
} else if e, ok := event.(*tcell.EventMouse); e != nil && (!ok || t.State.Mode(terminal.ModeMouseMask)) {
} else if e, ok := event.(*tcell.EventMouse); !ok || t.State.Mode(terminal.ModeMouseMask) {
// t.WriteString(event.EscSeq())
} else if e != nil {
} else {
x, y := e.Position()
v := t.GetView()
x -= v.X
@ -188,7 +188,12 @@ func (t *TermPane) HandleEvent(event tcell.Event) {
t.mouseReleased = true
}
}
}
// HandleTermClose is called when a terminal has finished its job
// and should be closed. If that terminal is this termpane's terminal,
// HandleTermClose will close the terminal and the termpane itself.
func (t *TermPane) HandleTermClose() {
if t.Status == shell.TTClose {
t.Quit()
}

View File

@ -2,7 +2,7 @@ package buffer
import (
"bytes"
"io/ioutil"
"io/fs"
"os"
"sort"
"strings"
@ -64,7 +64,7 @@ func (b *Buffer) CycleAutocomplete(forward bool) {
// GetWord gets the most recent word separated by any separator
// (whitespace, punctuation, any non alphanumeric character)
func GetWord(b *Buffer) ([]byte, int) {
func (b *Buffer) GetWord() ([]byte, int) {
c := b.GetActiveCursor()
l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X)
@ -73,17 +73,17 @@ func GetWord(b *Buffer) ([]byte, int) {
return []byte{}, -1
}
if util.IsNonAlphaNumeric(b.RuneAt(c.Loc.Move(-1, b))) {
if util.IsNonWordChar(b.RuneAt(c.Loc.Move(-1, b))) {
return []byte{}, c.X
}
args := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
args := bytes.FieldsFunc(l, util.IsNonWordChar)
input := args[len(args)-1]
return input, c.X - util.CharacterCount(input)
}
// GetArg gets the most recent word (separated by ' ' only)
func GetArg(b *Buffer) (string, int) {
func (b *Buffer) GetArg() (string, int) {
c := b.GetActiveCursor()
l := b.LineBytes(c.Y)
l = util.SliceStart(l, c.X)
@ -104,20 +104,20 @@ func GetArg(b *Buffer) (string, int) {
// FileComplete autocompletes filenames
func FileComplete(b *Buffer) ([]string, []string) {
c := b.GetActiveCursor()
input, argstart := GetArg(b)
input, argstart := b.GetArg()
sep := string(os.PathSeparator)
dirs := strings.Split(input, sep)
var files []os.FileInfo
var files []fs.DirEntry
var err error
if len(dirs) > 1 {
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
directories, _ = util.ReplaceHome(directories)
files, err = ioutil.ReadDir(directories)
files, err = os.ReadDir(directories)
} else {
files, err = ioutil.ReadDir(".")
files, err = os.ReadDir(".")
}
if err != nil {
@ -153,7 +153,7 @@ func FileComplete(b *Buffer) ([]string, []string) {
// BufferComplete autocompletes based on previous words in the buffer
func BufferComplete(b *Buffer) ([]string, []string) {
c := b.GetActiveCursor()
input, argstart := GetWord(b)
input, argstart := b.GetWord()
if argstart == -1 {
return []string{}, []string{}
@ -166,7 +166,7 @@ func BufferComplete(b *Buffer) ([]string, []string) {
var suggestions []string
for i := c.Y; i >= 0; i-- {
l := b.LineBytes(i)
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
words := bytes.FieldsFunc(l, util.IsNonWordChar)
for _, w := range words {
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
strw := string(w)
@ -179,7 +179,7 @@ func BufferComplete(b *Buffer) ([]string, []string) {
}
for i := c.Y + 1; i < b.LinesNum(); i++ {
l := b.LineBytes(i)
words := bytes.FieldsFunc(l, util.IsNonAlphaNumeric)
words := bytes.FieldsFunc(l, util.IsNonWordChar)
for _, w := range words {
if bytes.HasPrefix(w, input) && util.CharacterCount(w) > inputLen {
strw := string(w)

View File

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

View File

@ -7,9 +7,8 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"io/fs"
"os"
"path"
"path/filepath"
"strconv"
"strings"
@ -25,13 +24,12 @@ import (
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/micro/v2/pkg/highlight"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
)
const backupTime = 8000
var (
// OpenBuffers is a list of the currently open buffers
OpenBuffers []*Buffer
@ -57,17 +55,13 @@ var (
BTLog = BufType{2, true, true, false}
// BTScratch is a buffer that cannot be saved (for scratch work)
BTScratch = BufType{3, false, true, false}
// BTRaw is is a buffer that shows raw terminal events
// BTRaw is a buffer that shows raw terminal events
BTRaw = BufType{4, false, true, false}
// BTInfo is a buffer for inputting information
BTInfo = BufType{5, false, true, false}
// BTStdout is a buffer that only writes to stdout
// when closed
BTStdout = BufType{6, false, true, true}
// ErrFileTooLarge is returned when the file is too large to hash
// (fastdirty is automatically enabled)
ErrFileTooLarge = errors.New("File is too large to hash")
)
// SharedBuffer is a struct containing info that is shared among buffers
@ -90,6 +84,10 @@ type SharedBuffer struct {
// Settings customized by the user
Settings map[string]interface{}
// LocalSettings customized by the user for this buffer only
LocalSettings map[string]bool
encoding encoding.Encoding
Suggestions []string
Completions []string
@ -103,7 +101,8 @@ type SharedBuffer struct {
diffLock sync.RWMutex
diff map[int]DiffStatus
requestedBackup bool
RequestedBackup bool
forceKeepBackup bool
// ReloadDisabled allows the user to disable reloads if they
// are viewing a file that is constantly changing
@ -211,6 +210,11 @@ type Buffer struct {
LastSearchRegex bool
// HighlightSearch enables highlighting all instances of the last successful search
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
@ -234,17 +238,20 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
return nil, err
}
f, err := os.OpenFile(filename, os.O_WRONLY, 0)
readonly := os.IsPermission(err)
f.Close()
fileInfo, serr := os.Stat(filename)
if serr != nil && !os.IsNotExist(serr) {
if serr != nil && !errors.Is(serr, fs.ErrNotExist) {
return nil, serr
}
if serr == nil && fileInfo.IsDir() {
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)
if err == nil {
@ -252,7 +259,7 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*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
buf = NewBufferFromString("", filename, btype)
} else if err != nil {
@ -322,29 +329,19 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
b.AbsPath = absPath
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.LocalSettings = make(map[string]bool)
for k, v := range config.GlobalSettings {
if _, ok := config.DefaultGlobalOnlySettings[k]; !ok {
// make sure setting is not global-only
settings[k] = v
b.Settings[k] = v
}
}
config.InitLocalSettings(settings, absPath)
b.Settings["readonly"] = settings["readonly"]
b.Settings["filetype"] = settings["filetype"]
b.Settings["syntax"] = settings["syntax"]
config.UpdatePathGlobLocals(b.Settings, absPath)
enc, err := htmlindex.Get(settings["encoding"].(string))
b.encoding, err = htmlindex.Get(b.Settings["encoding"].(string))
if err != nil {
enc = unicode.UTF8
b.encoding = unicode.UTF8
b.Settings["encoding"] = "utf-8"
}
@ -355,19 +352,22 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
return NewBufferFromString("", "", btype)
}
if !hasBackup {
reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder()))
reader := bufio.NewReader(transform.NewReader(r, b.encoding.NewDecoder()))
var ff FileFormat = FFAuto
if size == 0 {
// for empty files, use the fileformat setting instead of
// autodetection
switch settings["fileformat"] {
switch b.Settings["fileformat"] {
case "unix":
ff = FFUnix
case "dos":
ff = FFDos
}
} else {
// in case of autodetection treat as locally set
b.LocalSettings["fileformat"] = true
}
b.LineArray = NewLineArray(uint64(size), ff, reader)
@ -390,10 +390,10 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
}
b.UpdateRules()
// init local settings again now that we know the filetype
config.InitLocalSettings(b.Settings, b.Path)
// we know the filetype now, so update per-filetype settings
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)
}
@ -478,7 +478,7 @@ func (b *Buffer) GetName() string {
name = b.Path
}
if b.Settings["basename"].(bool) {
return path.Base(name)
return filepath.Base(name)
}
return name
}
@ -544,7 +544,7 @@ func (b *Buffer) ReOpen() error {
}
reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder()))
data, err := ioutil.ReadAll(reader)
data, err := io.ReadAll(reader)
txt := string(data)
if err != nil {
@ -554,7 +554,11 @@ func (b *Buffer) ReOpen() error {
err = b.UpdateModTime()
if !b.Settings["fastdirty"].(bool) {
calcHash(b, &b.origHash)
if len(data) > LargeFileThreshold {
b.Settings["fastdirty"] = true
} else {
calcHash(b, &b.origHash)
}
}
b.isModified = false
b.RelocateCursors()
@ -568,6 +572,13 @@ func (b *Buffer) RelocateCursors() {
}
}
// DeselectCursors removes selection from all cursors
func (b *Buffer) DeselectCursors() {
for _, c := range b.cursors {
c.Deselect(true)
}
}
// RuneAt returns the rune at a given location in the buffer
func (b *Buffer) RuneAt(loc Loc) rune {
line := b.LineBytes(loc.Y)
@ -608,6 +619,16 @@ func (b *Buffer) WordAt(loc Loc) []byte {
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
// being opened
func (b *Buffer) Modified() bool {
@ -642,39 +663,122 @@ func (b *Buffer) Size() int {
}
// calcHash calculates md5 hash of all lines in the buffer
func calcHash(b *Buffer, out *[md5.Size]byte) error {
func calcHash(b *Buffer, out *[md5.Size]byte) {
h := md5.New()
size := 0
if len(b.lines) > 0 {
n, e := h.Write(b.lines[0].data)
if e != nil {
return e
}
size += n
h.Write(b.lines[0].data)
for _, l := range b.lines[1:] {
n, e = h.Write([]byte{'\n'})
if e != nil {
return e
if b.Endings == FFDos {
h.Write([]byte{'\r', '\n'})
} else {
h.Write([]byte{'\n'})
}
size += n
n, e = h.Write(l.data)
if e != nil {
return e
}
size += n
h.Write(l.data)
}
}
if size > LargeFileThreshold {
return ErrFileTooLarge
}
h.Sum((*out)[:0])
}
func parseDefFromFile(f config.RuntimeFile, header *highlight.Header) *highlight.Def {
data, err := f.Data()
if err != nil {
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
return nil
}
if header == nil {
header, err = highlight.MakeHeaderYaml(data)
if err != nil {
screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error())
return nil
}
}
file, err := highlight.ParseFile(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
return nil
}
syndef, err := highlight.ParseDef(file, header)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
return nil
}
return syndef
}
// findRealRuntimeSyntaxDef finds a specific syntax definition
// in the user's custom syntax files
func findRealRuntimeSyntaxDef(name string, header *highlight.Header) *highlight.Def {
for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
if f.Name() == name {
syndef := parseDefFromFile(f, header)
if syndef != nil {
return syndef
}
}
}
return nil
}
// findRuntimeSyntaxDef finds a specific syntax definition
// in the built-in syntax files
func findRuntimeSyntaxDef(name string, header *highlight.Header) *highlight.Def {
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
if f.Name() == name {
syndef := parseDefFromFile(f, header)
if syndef != nil {
return syndef
}
}
}
return nil
}
func resolveIncludes(syndef *highlight.Def) {
includes := highlight.GetIncludes(syndef)
if len(includes) == 0 {
return
}
var files []*highlight.File
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
data, err := f.Data()
if err != nil {
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
header, err := highlight.MakeHeaderYaml(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
for _, i := range includes {
if header.FileType == i {
file, err := highlight.ParseFile(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
files = append(files, file)
break
}
}
if len(files) >= len(includes) {
break
}
}
highlight.ResolveIncludes(syndef, files)
}
// UpdateRules updates the syntax rules and filetype for this buffer
// This is called when the colorscheme changes
func (b *Buffer) UpdateRules() {
@ -683,13 +787,32 @@ func (b *Buffer) UpdateRules() {
}
ft := b.Settings["filetype"].(string)
if ft == "off" {
b.ClearMatches()
b.SyntaxDef = nil
return
}
b.SyntaxDef = nil
// syntaxFileInfo is an internal helper structure
// to store properties of one single syntax file
type syntaxFileInfo struct {
header *highlight.Header
fileName string
syntaxDef *highlight.Def
}
fnameMatches := []syntaxFileInfo{}
headerMatches := []syntaxFileInfo{}
syntaxFile := ""
foundDef := false
var header *highlight.Header
// search for the syntax file in the user's custom syntax files
for _, f := range config.ListRealRuntimeFiles(config.RTSyntax) {
if f.Name() == "default" {
continue
}
data, err := f.Data()
if err != nil {
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
@ -701,118 +824,145 @@ func (b *Buffer) UpdateRules() {
screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error())
continue
}
file, err := highlight.ParseFile(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
matchedFileType := false
matchedFileName := false
matchedFileHeader := false
if ft == "unknown" || ft == "" {
if header.MatchFileName(b.Path) {
matchedFileName = true
}
if len(fnameMatches) == 0 && header.MatchFileHeader(b.lines[0].data) {
matchedFileHeader = true
}
} else if header.FileType == ft {
matchedFileType = true
}
if ((ft == "unknown" || ft == "") && highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data)) || header.FileType == ft {
if matchedFileType || matchedFileName || matchedFileHeader {
file, err := highlight.ParseFile(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
syndef, err := highlight.ParseDef(file, header)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
b.SyntaxDef = syndef
syntaxFile = f.Name()
foundDef = true
break
if matchedFileType {
b.SyntaxDef = syndef
syntaxFile = f.Name()
foundDef = true
break
}
if matchedFileName {
fnameMatches = append(fnameMatches, syntaxFileInfo{header, f.Name(), syndef})
} else if matchedFileHeader {
headerMatches = append(headerMatches, syntaxFileInfo{header, f.Name(), syndef})
}
}
}
// search in the default syntax files
for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
data, err := f.Data()
if err != nil {
screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error())
continue
}
if !foundDef {
// search for the syntax file in the built-in syntax files
for _, f := range config.ListRuntimeFiles(config.RTSyntaxHeader) {
data, err := f.Data()
if err != nil {
screen.TermMessage("Error loading syntax header file " + f.Name() + ": " + err.Error())
continue
}
header, err = highlight.MakeHeader(data)
if err != nil {
screen.TermMessage("Error reading syntax header file", f.Name(), err)
continue
}
header, err = highlight.MakeHeader(data)
if err != nil {
screen.TermMessage("Error reading syntax header file", f.Name(), err)
continue
}
if ft == "unknown" || ft == "" {
if highlight.MatchFiletype(header.FtDetect, b.Path, b.lines[0].data) {
if ft == "unknown" || ft == "" {
if header.MatchFileName(b.Path) {
fnameMatches = append(fnameMatches, syntaxFileInfo{header, f.Name(), nil})
}
if len(fnameMatches) == 0 && header.MatchFileHeader(b.lines[0].data) {
headerMatches = append(headerMatches, syntaxFileInfo{header, f.Name(), nil})
}
} else if header.FileType == ft {
syntaxFile = f.Name()
break
}
} else if header.FileType == ft {
syntaxFile = f.Name()
break
}
}
if syntaxFile == "" {
matches := fnameMatches
if len(matches) == 0 {
matches = headerMatches
}
length := len(matches)
if length > 0 {
signatureMatch := false
if length > 1 {
// multiple matching syntax files found, try to resolve the ambiguity
// using signatures
detectlimit := util.IntOpt(b.Settings["detectlimit"])
lineCount := len(b.lines)
limit := lineCount
if detectlimit > 0 && lineCount > detectlimit {
limit = detectlimit
}
matchLoop:
for _, m := range matches {
if m.header.HasFileSignature() {
for i := 0; i < limit; i++ {
if m.header.MatchFileSignature(b.lines[i].data) {
syntaxFile = m.fileName
if m.syntaxDef != nil {
b.SyntaxDef = m.syntaxDef
foundDef = true
}
header = m.header
signatureMatch = true
break matchLoop
}
}
}
}
}
if length == 1 || !signatureMatch {
syntaxFile = matches[0].fileName
if matches[0].syntaxDef != nil {
b.SyntaxDef = matches[0].syntaxDef
foundDef = true
}
header = matches[0].header
}
}
}
if syntaxFile != "" && !foundDef {
// we found a syntax file using a syntax header file
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
if f.Name() == syntaxFile {
data, err := f.Data()
if err != nil {
screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
continue
}
file, err := highlight.ParseFile(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
syndef, err := highlight.ParseDef(file, header)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
b.SyntaxDef = syndef
break
}
}
b.SyntaxDef = findRuntimeSyntaxDef(syntaxFile, header)
}
if b.SyntaxDef != nil && highlight.HasIncludes(b.SyntaxDef) {
includes := highlight.GetIncludes(b.SyntaxDef)
var files []*highlight.File
for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
data, err := f.Data()
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
header, err := highlight.MakeHeaderYaml(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
for _, i := range includes {
if header.FileType == i {
file, err := highlight.ParseFile(data)
if err != nil {
screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
continue
}
files = append(files, file)
break
}
}
if len(files) >= len(includes) {
break
}
}
highlight.ResolveIncludes(b.SyntaxDef, files)
}
if b.Highlighter == nil || syntaxFile != "" {
if b.SyntaxDef != nil {
b.Settings["filetype"] = b.SyntaxDef.FileType
}
if b.SyntaxDef != nil {
b.Settings["filetype"] = b.SyntaxDef.FileType
} else {
b.SyntaxDef = &highlight.EmptyDef
// search for the default file in the user's custom syntax files
b.SyntaxDef = findRealRuntimeSyntaxDef("default", nil)
if b.SyntaxDef == nil {
// search for the default file in the built-in syntax files
b.SyntaxDef = findRuntimeSyntaxDef("default", nil)
}
}
if b.SyntaxDef != nil {
resolveIncludes(b.SyntaxDef)
}
if b.SyntaxDef != nil {
@ -914,7 +1064,7 @@ func (b *Buffer) MergeCursors() {
b.EventHandler.active = b.curCursor
}
// UpdateCursors updates all the cursors indicies
// UpdateCursors updates all the cursors indices
func (b *Buffer) UpdateCursors() {
b.EventHandler.cursors = b.cursors
b.EventHandler.active = b.curCursor
@ -939,7 +1089,7 @@ func (b *Buffer) ClearCursors() {
b.cursors = b.cursors[:1]
b.UpdateCursors()
b.curCursor = 0
b.GetActiveCursor().ResetSelection()
b.GetActiveCursor().Deselect(true)
}
// MoveLinesUp moves the range of lines up one row
@ -990,34 +1140,14 @@ var BracePairs = [][2]rune{
{'[', ']'},
}
// FindMatchingBrace returns the location in the buffer of the matching bracket
// It is given a brace type containing the open and closing character, (for example
// '{' and '}') as well as the location to match from
// TODO: maybe can be more efficient with utf8 package
// returns the location of the matching brace
// if the boolean returned is true then the original matching brace is one character left
// of the starting location
func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, bool) {
curLine := []rune(string(b.LineBytes(start.Y)))
startChar := ' '
if start.X >= 0 && start.X < len(curLine) {
startChar = curLine[start.X]
}
leftChar := ' '
if start.X-1 >= 0 && start.X-1 < len(curLine) {
leftChar = curLine[start.X-1]
}
func (b *Buffer) findMatchingBrace(braceType [2]rune, start Loc, char rune) (Loc, bool) {
var i int
if startChar == braceType[0] || leftChar == braceType[0] {
if char == braceType[0] {
for y := start.Y; y < b.LinesNum(); y++ {
l := []rune(string(b.LineBytes(y)))
xInit := 0
if y == start.Y {
if startChar == braceType[0] {
xInit = start.X
} else {
xInit = start.X - 1
}
xInit = start.X
}
for x := xInit; x < len(l); x++ {
r := l[x]
@ -1026,42 +1156,76 @@ func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool, boo
} else if r == braceType[1] {
i--
if i == 0 {
if startChar == braceType[0] {
return Loc{x, y}, false, true
}
return Loc{x, y}, true, true
return Loc{x, y}, true
}
}
}
}
} else if startChar == braceType[1] || leftChar == braceType[1] {
} else if char == braceType[1] {
for y := start.Y; y >= 0; y-- {
l := []rune(string(b.lines[y].data))
xInit := len(l) - 1
if y == start.Y {
if leftChar == braceType[1] {
xInit = start.X - 1
} else {
xInit = start.X
}
xInit = start.X
}
for x := xInit; x >= 0; x-- {
r := l[x]
if r == braceType[0] {
if r == braceType[1] {
i++
} else if r == braceType[0] {
i--
if i == 0 {
if leftChar == braceType[1] {
return Loc{x, y}, true, true
}
return Loc{x, y}, false, true
return Loc{x, y}, true
}
} else if r == braceType[1] {
i++
}
}
}
}
return start, true, false
return start, false
}
// If there is a brace character (for example '{' or ']') at the given start location,
// FindMatchingBrace returns the location of the matching brace for it (for example '}'
// or '['). The second returned value is true if there was no matching brace found
// for given starting location but it was found for the location one character left
// of it. The third returned value is true if the matching brace was found at all.
func (b *Buffer) FindMatchingBrace(start Loc) (Loc, bool, bool) {
// TODO: maybe can be more efficient with utf8 package
curLine := []rune(string(b.LineBytes(start.Y)))
// first try to find matching brace for the given location (it has higher priority)
if start.X >= 0 && start.X < len(curLine) {
startChar := curLine[start.X]
for _, bp := range BracePairs {
if startChar == bp[0] || startChar == bp[1] {
mb, found := b.findMatchingBrace(bp, start, startChar)
if found {
return mb, false, true
}
}
}
}
if b.Settings["matchbraceleft"].(bool) {
// failed to find matching brace for the given location, so try to find matching
// brace for the location one character left of it
if start.X-1 >= 0 && start.X-1 < len(curLine) {
leftChar := curLine[start.X-1]
left := Loc{start.X - 1, start.Y}
for _, bp := range BracePairs {
if leftChar == bp[0] || leftChar == bp[1] {
mb, found := b.findMatchingBrace(bp, left, leftChar)
if found {
return mb, true, true
}
}
}
}
}
return start, false, false
}
// Retab changes all tabs to spaces or vice versa
@ -1083,7 +1247,11 @@ func (b *Buffer) Retab() {
}
l = bytes.TrimLeft(l, " \t")
b.Lock()
b.lines[i].data = append(ws, l...)
b.Unlock()
b.MarkModified(i, i)
dirty = true
}
@ -1126,7 +1294,7 @@ func (b *Buffer) Write(bytes []byte) (n int, err error) {
return len(bytes), nil
}
func (b *Buffer) updateDiffSync() {
func (b *Buffer) updateDiff(synchronous bool) {
b.diffLock.Lock()
defer b.diffLock.Unlock()
@ -1137,7 +1305,16 @@ func (b *Buffer) updateDiffSync() {
}
differ := dmp.New()
baseRunes, bufferRunes, _ := differ.DiffLinesToRunes(string(b.diffBase), string(b.Bytes()))
if !synchronous {
b.Lock()
}
bytes := b.Bytes()
if !synchronous {
b.Unlock()
}
baseRunes, bufferRunes, _ := differ.DiffLinesToRunes(string(b.diffBase), string(bytes))
diffs := differ.DiffMainRunes(baseRunes, bufferRunes, false)
lineN := 0
@ -1166,13 +1343,9 @@ func (b *Buffer) updateDiffSync() {
// UpdateDiff computes the diff between the diff base and the buffer content.
// The update may be performed synchronously or asynchronously.
// UpdateDiff calls the supplied callback when the update is complete.
// The argument passed to the callback is set to true if and only if
// the update was performed synchronously.
// If an asynchronous update is already pending when UpdateDiff is called,
// UpdateDiff does not schedule another update, in which case the callback
// is not called.
func (b *Buffer) UpdateDiff(callback func(bool)) {
// UpdateDiff does not schedule another update.
func (b *Buffer) UpdateDiff() {
if b.updateDiffTimer != nil {
return
}
@ -1183,20 +1356,18 @@ func (b *Buffer) UpdateDiff(callback func(bool)) {
}
if lineCount < 1000 {
b.updateDiffSync()
callback(true)
b.updateDiff(true)
} else if lineCount < 30000 {
b.updateDiffTimer = time.AfterFunc(500*time.Millisecond, func() {
b.updateDiffTimer = nil
b.updateDiffSync()
callback(false)
b.updateDiff(false)
screen.Redraw()
})
} else {
// Don't compute diffs for very large files
b.diffLock.Lock()
b.diff = make(map[int]DiffStatus)
b.diffLock.Unlock()
callback(true)
}
}
@ -1208,9 +1379,7 @@ func (b *Buffer) SetDiffBase(diffBase []byte) {
} else {
b.diffBaseLineCount = strings.Count(string(diffBase), "\n")
}
b.UpdateDiff(func(synchronous bool) {
screen.Redraw()
})
b.UpdateDiff()
}
// DiffStatus returns the diff status for a line in the buffer

View File

@ -20,6 +20,7 @@ type operation struct {
func init() {
ulua.L = lua.NewState()
config.InitRuntimeFiles(false)
config.InitGlobalSettings()
config.GlobalSettings["backup"] = false
config.GlobalSettings["fastdirty"] = true

View File

@ -20,8 +20,14 @@ type Cursor struct {
buf *Buffer
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
// 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)
CurSelection [2]Loc
@ -30,6 +36,11 @@ type Cursor struct {
// to know what the original selection was
OrigSelection [2]Loc
// The line number where a new trailing whitespace has been added
// or -1 if there is no new trailing whitespace at this cursor.
// This is used for checking if a trailing whitespace should be highlighted
NewTrailingWsY int
// Which cursor index is this (for multiple cursors)
Num int
}
@ -38,6 +49,8 @@ func NewCursor(b *Buffer, l Loc) *Cursor {
c := &Cursor{
buf: b,
Loc: l,
NewTrailingWsY: -1,
}
c.StoreVisualX()
return c
@ -54,8 +67,9 @@ func (c *Cursor) Buf() *Buffer {
// Goto puts the cursor at the given cursor's location and gives
// the current cursor its selection too
func (c *Cursor) Goto(b Cursor) {
c.X, c.Y, c.LastVisualX = b.X, b.Y, b.LastVisualX
c.X, c.Y = b.X, b.Y
c.OrigSelection, c.CurSelection = b.OrigSelection, b.CurSelection
c.StoreVisualX()
}
// GotoLoc puts the cursor at the given cursor's location and gives
@ -66,8 +80,8 @@ func (c *Cursor) GotoLoc(l Loc) {
}
// GetVisualX returns the x value of the cursor in visual spaces
func (c *Cursor) GetVisualX() int {
if c.buf.GetVisualX != nil {
func (c *Cursor) GetVisualX(wrap bool) int {
if wrap && c.buf.GetVisualX != nil {
return c.buf.GetVisualX(c.Loc)
}
@ -93,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
func (c *Cursor) Start() {
c.X = 0
c.LastVisualX = c.GetVisualX()
c.StoreVisualX()
}
// StartOfText moves the cursor to the first non-whitespace rune of
@ -124,7 +138,7 @@ func (c *Cursor) IsStartOfText() bool {
// End moves the cursor to the end of the line it is on
func (c *Cursor) End() {
c.X = util.CharacterCount(c.buf.LineBytes(c.Y))
c.LastVisualX = c.GetVisualX()
c.StoreVisualX()
}
// CopySelection copies the user's selection to either "primary"
@ -179,7 +193,7 @@ func (c *Cursor) Deselect(start bool) {
if start {
c.Loc = c.CurSelection[0]
} else {
c.Loc = c.CurSelection[1].Move(-1, c.buf)
c.Loc = c.CurSelection[1]
}
c.ResetSelection()
c.StoreVisualX()
@ -396,13 +410,26 @@ func (c *Cursor) SelectTo(loc Loc) {
// WordRight moves the cursor one word to the right
func (c *Cursor) WordRight() {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
c.Right()
return
}
for util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
c.Right()
return
}
c.Right()
}
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) &&
util.IsNonWordChar(c.RuneUnder(c.X+1)) {
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
c.Right()
}
return
}
c.Right()
for util.IsWordChar(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
@ -414,6 +441,10 @@ func (c *Cursor) WordRight() {
// WordLeft moves the cursor one word to the left
func (c *Cursor) WordLeft() {
if c.X == 0 {
c.Left()
return
}
c.Left()
for util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == 0 {
@ -421,6 +452,17 @@ func (c *Cursor) WordLeft() {
}
c.Left()
}
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) &&
util.IsNonWordChar(c.RuneUnder(c.X-1)) {
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
c.Right()
return
}
c.Left()
for util.IsWordChar(c.RuneUnder(c.X)) {
if c.X == 0 {
@ -431,6 +473,132 @@ func (c *Cursor) WordLeft() {
c.Right()
}
// SubWordRight moves the cursor one sub-word to the right
func (c *Cursor) SubWordRight() {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
c.Right()
return
}
if util.IsWhitespace(c.RuneUnder(c.X)) {
for util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
c.Right()
}
return
}
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
c.Right()
}
return
}
if util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
for util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
c.Right()
}
if util.IsWhitespace(c.RuneUnder(c.X)) {
return
}
}
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
if util.IsUpperLetter(c.RuneUnder(c.X)) &&
util.IsUpperLetter(c.RuneUnder(c.X+1)) {
for util.IsUpperAlphanumeric(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
c.Right()
}
if util.IsLowerAlphanumeric(c.RuneUnder(c.X)) {
c.Left()
}
} else {
c.Right()
for util.IsLowerAlphanumeric(c.RuneUnder(c.X)) {
if c.X == util.CharacterCount(c.buf.LineBytes(c.Y)) {
return
}
c.Right()
}
}
}
// SubWordLeft moves the cursor one sub-word to the left
func (c *Cursor) SubWordLeft() {
if c.X == 0 {
c.Left()
return
}
c.Left()
if util.IsWhitespace(c.RuneUnder(c.X)) {
for util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
c.Right()
return
}
if util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
for util.IsNonWordChar(c.RuneUnder(c.X)) && !util.IsWhitespace(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
c.Right()
return
}
if util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
for util.IsSubwordDelimiter(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
if util.IsWhitespace(c.RuneUnder(c.X)) {
c.Right()
return
}
}
if c.X == 0 {
return
}
if util.IsUpperLetter(c.RuneUnder(c.X)) &&
util.IsUpperLetter(c.RuneUnder(c.X-1)) {
for util.IsUpperAlphanumeric(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
if !util.IsUpperAlphanumeric(c.RuneUnder(c.X)) {
c.Right()
}
} else {
for util.IsLowerAlphanumeric(c.RuneUnder(c.X)) {
if c.X == 0 {
return
}
c.Left()
}
if !util.IsAlphanumeric(c.RuneUnder(c.X)) {
c.Right()
}
}
}
// RuneUnder returns the rune under the given x position
func (c *Cursor) RuneUnder(x int) rune {
line := c.buf.LineBytes(c.Y)
@ -454,5 +622,6 @@ func (c *Cursor) RuneUnder(x int) rune {
}
func (c *Cursor) StoreVisualX() {
c.LastVisualX = c.GetVisualX()
c.LastVisualX = c.GetVisualX(false)
c.LastWrappedVisualX = c.GetVisualX(true)
}

View File

@ -104,7 +104,11 @@ func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) {
c.OrigSelection[0] = move(c.OrigSelection[0])
c.OrigSelection[1] = move(c.OrigSelection[1])
c.Relocate()
c.LastVisualX = c.GetVisualX()
c.StoreVisualX()
}
if useUndo {
eh.updateTrailingWs(t)
}
}
@ -249,11 +253,11 @@ func (eh *EventHandler) Execute(t *TextEvent) {
ExecuteTextEvent(t, eh.buf)
}
// Undo the first event in the undo stack
func (eh *EventHandler) Undo() {
// Undo the first event in the undo stack. Returns false if the stack is empty.
func (eh *EventHandler) Undo() bool {
t := eh.UndoStack.Peek()
if t == nil {
return
return false
}
startTime := t.Time.UnixNano() / int64(time.Millisecond)
@ -262,15 +266,16 @@ func (eh *EventHandler) Undo() {
for {
t = eh.UndoStack.Peek()
if t == nil {
return
break
}
if t.Time.UnixNano()/int64(time.Millisecond) < endTime {
return
break
}
eh.UndoOneEvent()
}
return true
}
// UndoOneEvent undoes one event
@ -286,23 +291,20 @@ func (eh *EventHandler) UndoOneEvent() {
eh.UndoTextEvent(t)
// Set the cursor in the right place
teCursor := t.C
if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) {
t.C = *eh.cursors[teCursor.Num]
eh.cursors[teCursor.Num].Goto(teCursor)
} else {
teCursor.Num = -1
if t.C.Num >= 0 && t.C.Num < len(eh.cursors) {
eh.cursors[t.C.Num].Goto(t.C)
eh.cursors[t.C.Num].NewTrailingWsY = t.C.NewTrailingWsY
}
// Push it to the redo stack
eh.RedoStack.Push(t)
}
// Redo the first event in the redo stack
func (eh *EventHandler) Redo() {
// Redo the first event in the redo stack. Returns false if the stack is empty.
func (eh *EventHandler) Redo() bool {
t := eh.RedoStack.Peek()
if t == nil {
return
return false
}
startTime := t.Time.UnixNano() / int64(time.Millisecond)
@ -311,15 +313,16 @@ func (eh *EventHandler) Redo() {
for {
t = eh.RedoStack.Peek()
if t == nil {
return
break
}
if t.Time.UnixNano()/int64(time.Millisecond) > endTime {
return
break
}
eh.RedoOneEvent()
}
return true
}
// RedoOneEvent redoes one event
@ -329,12 +332,9 @@ func (eh *EventHandler) RedoOneEvent() {
return
}
teCursor := t.C
if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) {
t.C = *eh.cursors[teCursor.Num]
eh.cursors[teCursor.Num].Goto(teCursor)
} else {
teCursor.Num = -1
if t.C.Num >= 0 && t.C.Num < len(eh.cursors) {
eh.cursors[t.C.Num].Goto(t.C)
eh.cursors[t.C.Num].NewTrailingWsY = t.C.NewTrailingWsY
}
// Modifies the text event
@ -342,3 +342,58 @@ func (eh *EventHandler) RedoOneEvent() {
eh.UndoStack.Push(t)
}
// updateTrailingWs updates the cursor's trailing whitespace status after a text event
func (eh *EventHandler) updateTrailingWs(t *TextEvent) {
if len(t.Deltas) != 1 {
return
}
text := t.Deltas[0].Text
start := t.Deltas[0].Start
end := t.Deltas[0].End
c := eh.cursors[eh.active]
isEol := func(loc Loc) bool {
return loc.X == util.CharacterCount(eh.buf.LineBytes(loc.Y))
}
if t.EventType == TextEventInsert && c.Loc == end && isEol(end) {
var addedTrailingWs bool
addedAfterWs := false
addedWsOnly := false
if start.Y == end.Y {
addedTrailingWs = util.HasTrailingWhitespace(text)
addedWsOnly = util.IsBytesWhitespace(text)
addedAfterWs = start.X > 0 && util.IsWhitespace(c.buf.RuneAt(Loc{start.X - 1, start.Y}))
} else {
lastnl := bytes.LastIndex(text, []byte{'\n'})
addedTrailingWs = util.HasTrailingWhitespace(text[lastnl+1:])
}
if addedTrailingWs && !(addedAfterWs && addedWsOnly) {
c.NewTrailingWsY = c.Y
} else if !addedTrailingWs {
c.NewTrailingWsY = -1
}
} else if t.EventType == TextEventRemove && c.Loc == start && isEol(start) {
removedAfterWs := util.HasTrailingWhitespace(eh.buf.LineBytes(start.Y))
var removedWsOnly bool
if start.Y == end.Y {
removedWsOnly = util.IsBytesWhitespace(text)
} else {
firstnl := bytes.Index(text, []byte{'\n'})
removedWsOnly = util.IsBytesWhitespace(text[:firstnl])
}
if removedAfterWs && !removedWsOnly {
c.NewTrailingWsY = c.Y
} else if !removedAfterWs {
c.NewTrailingWsY = -1
}
} else if c.NewTrailingWsY != -1 && start.Y != end.Y && c.Loc.GreaterThan(start) &&
((t.EventType == TextEventInsert && c.Y == c.NewTrailingWsY+(end.Y-start.Y)) ||
(t.EventType == TextEventRemove && c.Y == c.NewTrailingWsY-(end.Y-start.Y))) {
// The cursor still has its new trailingws
// but its line number was shifted by insert or remove of lines above
c.NewTrailingWsY = c.Y
}
}

View File

@ -46,10 +46,9 @@ type searchState struct {
type Line struct {
data []byte
state highlight.State
match highlight.LineMatch
rehighlight bool
lock sync.Mutex
state highlight.State
match highlight.LineMatch
lock sync.Mutex
// The search states for the line, used for highlighting of search matches,
// separately from the syntax highlighting.
@ -75,6 +74,7 @@ type LineArray struct {
lines []Line
Endings FileFormat
initsize uint64
lock sync.Mutex
}
// Append efficiently appends lines together
@ -147,20 +147,18 @@ func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray
if err != nil {
if err == io.EOF {
la.lines = Append(la.lines, Line{
data: data,
state: nil,
match: nil,
rehighlight: false,
data: data,
state: nil,
match: nil,
})
}
// Last line was read
break
} else {
la.lines = Append(la.lines, Line{
data: data[:dlen-1],
state: nil,
match: nil,
rehighlight: false,
data: data[:dlen-1],
state: nil,
match: nil,
})
}
n++
@ -190,22 +188,23 @@ func (la *LineArray) Bytes() []byte {
// newlineBelow adds a newline below the given line number
func (la *LineArray) newlineBelow(y int) {
la.lines = append(la.lines, Line{
data: []byte{' '},
state: nil,
match: nil,
rehighlight: false,
data: []byte{' '},
state: nil,
match: nil,
})
copy(la.lines[y+2:], la.lines[y+1:])
la.lines[y+1] = Line{
data: []byte{},
state: la.lines[y].state,
match: nil,
rehighlight: false,
data: []byte{},
state: la.lines[y].state,
match: nil,
}
}
// Inserts a byte array at a given location
func (la *LineArray) insert(pos Loc, value []byte) {
la.lock.Lock()
defer la.lock.Unlock()
x, y := runeToByteIndex(pos.X, la.lines[pos.Y].data), pos.Y
for i := 0; i < len(value); i++ {
if value[i] == '\n' || (value[i] == '\r' && i < len(value)-1 && value[i+1] == '\n') {
@ -233,24 +232,26 @@ func (la *LineArray) insertByte(pos Loc, value byte) {
// joinLines joins the two lines a and b
func (la *LineArray) joinLines(a, b int) {
la.insert(Loc{len(la.lines[a].data), a}, la.lines[b].data)
la.lines[a].data = append(la.lines[a].data, la.lines[b].data...)
la.deleteLine(b)
}
// split splits a line at a given position
func (la *LineArray) split(pos Loc) {
la.newlineBelow(pos.Y)
la.insert(Loc{0, pos.Y + 1}, la.lines[pos.Y].data[pos.X:])
la.lines[pos.Y+1].data = append(la.lines[pos.Y+1].data, la.lines[pos.Y].data[pos.X:]...)
la.lines[pos.Y+1].state = la.lines[pos.Y].state
la.lines[pos.Y].state = nil
la.lines[pos.Y].match = nil
la.lines[pos.Y+1].match = nil
la.lines[pos.Y].rehighlight = true
la.deleteToEnd(Loc{pos.X, pos.Y})
}
// removes from start to end
func (la *LineArray) remove(start, end Loc) []byte {
la.lock.Lock()
defer la.lock.Unlock()
sub := la.Substr(start, end)
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
@ -284,11 +285,6 @@ func (la *LineArray) deleteLines(y1, y2 int) {
la.lines = la.lines[:y1+copy(la.lines[y1:], la.lines[y2+1:])]
}
// DeleteByte deletes the byte at a position
func (la *LineArray) deleteByte(pos Loc) {
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X+copy(la.lines[pos.Y].data[pos.X:], la.lines[pos.Y].data[pos.X+1:])]
}
// Substr returns the string representation between two locations
func (la *LineArray) Substr(start, end Loc) []byte {
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
@ -327,11 +323,11 @@ func (la *LineArray) End() Loc {
}
// LineBytes returns line n as an array of bytes
func (la *LineArray) LineBytes(n int) []byte {
if n >= len(la.lines) || n < 0 {
func (la *LineArray) LineBytes(lineN int) []byte {
if lineN >= len(la.lines) || lineN < 0 {
return []byte{}
}
return la.lines[n].data
return la.lines[lineN].data
}
// State gets the highlight state for the given line number
@ -362,16 +358,14 @@ func (la *LineArray) Match(lineN int) highlight.LineMatch {
return la.lines[lineN].match
}
func (la *LineArray) Rehighlight(lineN int) bool {
la.lines[lineN].lock.Lock()
defer la.lines[lineN].lock.Unlock()
return la.lines[lineN].rehighlight
// Locks the whole LineArray
func (la *LineArray) Lock() {
la.lock.Lock()
}
func (la *LineArray) SetRehighlight(lineN int, on bool) {
la.lines[lineN].lock.Lock()
defer la.lines[lineN].lock.Unlock()
la.lines[lineN].rehighlight = on
// Unlocks the whole LineArray
func (la *LineArray) Unlock() {
la.lock.Unlock()
}
// SearchMatch returns true if the location `pos` is within a match

View File

@ -47,6 +47,16 @@ func (l Loc) LessEqual(b Loc) bool {
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
// Diff returns the distance between two locations
@ -139,10 +149,5 @@ func ByteOffset(pos Loc, buf *Buffer) int {
// clamps a loc within a buffer
func clamp(pos Loc, la *LineArray) Loc {
if pos.GreaterEqual(la.End()) {
return la.End()
} else if pos.LessThan(la.Start()) {
return la.Start()
}
return pos
return pos.Clamp(la.Start(), la.End())
}

View File

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

View File

@ -5,18 +5,19 @@ import (
"bytes"
"errors"
"io"
"io/fs"
"os"
"os/exec"
"os/signal"
"path/filepath"
"runtime"
"sync/atomic"
"time"
"unicode"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/transform"
)
@ -24,68 +25,178 @@ import (
// because hashing is too slow
const LargeFileThreshold = 50000
// overwriteFile opens the given file for writing, truncating if one exists, and then calls
// the supplied function with the file as io.Writer object, also making sure the file is
// closed afterwards.
func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, withSudo bool) (err error) {
type wrappedFile struct {
writeCloser io.WriteCloser
withSudo bool
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 screenb bool
var cmd *exec.Cmd
var sigChan chan os.Signal
if withSudo {
cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name)
if writeCloser, err = cmd.StdinPipe(); err != nil {
return
writeCloser, err = cmd.StdinPipe()
if err != nil {
return wrappedFile{}, err
}
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
<-c
cmd.Process.Kill()
}()
sigChan = make(chan os.Signal, 1)
signal.Reset(os.Interrupt)
signal.Notify(sigChan, os.Interrupt)
screenb = screen.TempFini()
// 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
// 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)
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, 0644); err != nil {
return
}
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
} else {
writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, util.FileMode)
if err != nil {
return wrappedFile{}, err
}
}
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
err := cmd.Wait()
screen.TempStart(screenb)
err := wf.cmd.Wait()
screen.TempStart(wf.screenb)
signal.Notify(util.Sigterm, os.Interrupt)
signal.Stop(wf.sigChan)
if err != nil {
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
@ -93,9 +204,19 @@ func (b *Buffer) Save() error {
return b.SaveAs(b.Path)
}
// AutoSave saves the buffer to its default path
func (b *Buffer) AutoSave() error {
// Doing full b.Modified() check every time would be costly, due to the hash
// calculation. So use just isModified even if fastdirty is not set.
if !b.isModified {
return nil
}
return b.saveToFile(b.Path, false, true)
}
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
func (b *Buffer) SaveAs(filename string) error {
return b.saveToFile(filename, false)
return b.saveToFile(filename, false, false)
}
func (b *Buffer) SaveWithSudo() error {
@ -103,10 +224,10 @@ func (b *Buffer) SaveWithSudo() error {
}
func (b *Buffer) SaveAsWithSudo(filename string) error {
return b.saveToFile(filename, true)
return b.saveToFile(filename, true, false)
}
func (b *Buffer) saveToFile(filename string, withSudo bool) error {
func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error {
var err error
if b.Type.Readonly {
return errors.New("Cannot save readonly buffer")
@ -118,7 +239,7 @@ func (b *Buffer) saveToFile(filename string, withSudo bool) error {
return errors.New("Save with sudo not supported on Windows")
}
if b.Settings["rmtrailingws"].(bool) {
if !autoSave && b.Settings["rmtrailingws"].(bool) {
for i, l := range b.lines {
leftover := util.CharacterCount(bytes.TrimRightFunc(l.data, unicode.IsSpace))
@ -136,19 +257,35 @@ func (b *Buffer) saveToFile(filename string, withSudo bool) error {
}
}
// Update the last time this file was updated after saving
defer func() {
b.ModTime, _ = util.GetModTime(filename)
err = b.Serialize()
}()
filename, err = util.ReplaceHome(filename)
if err != nil {
return err
}
// Removes any tilde and replaces with the absolute path to home
absFilename, _ := util.ReplaceHome(filename)
newFile := false
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
if dirname := filepath.Dir(absFilename); dirname != "." {
// 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
if b.Settings["mkparents"].(bool) {
// Create all leading dir(s) since they don't exist
@ -162,49 +299,22 @@ func (b *Buffer) saveToFile(filename string, withSudo bool) error {
}
}
var fileSize int
enc, err := htmlindex.Get(b.Settings["encoding"].(string))
saveResponseChan := make(chan saveResponse)
saveRequestChan <- saveRequest{b, absFilename, withSudo, newFile, saveResponseChan}
result := <-saveResponseChan
err = result.err
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) {
if len(b.lines) == 0 {
return
b.UpdateModTime()
}
// end of line
var eol []byte
if b.Endings == FFDos {
eol = []byte{'\r', '\n'}
} else {
eol = []byte{'\n'}
}
// write lines
if fileSize, e = file.Write(b.lines[0].data); e != nil {
return
}
for _, l := range b.lines[1:] {
if _, e = file.Write(eol); e != nil {
return
}
if _, e = file.Write(l.data); e != nil {
return
}
fileSize += len(eol) + len(l.data)
}
return
}
if err = overwriteFile(absFilename, enc, fwriter, withSudo); err != nil {
return err
}
if !b.Settings["fastdirty"].(bool) {
if fileSize > LargeFileThreshold {
if result.size > LargeFileThreshold {
// For large files 'fastdirty' needs to be on
b.Settings["fastdirty"] = true
} else {
@ -212,10 +322,70 @@ func (b *Buffer) saveToFile(filename string, withSudo bool) error {
}
}
newPath := b.Path != filename
b.Path = filename
absPath, _ := filepath.Abs(filename)
b.AbsPath = absPath
b.AbsPath = absFilename
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
}
// 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 (
"regexp"
"unicode/utf8"
"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) {
lastcn := util.CharacterCount(b.LineBytes(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++ {
l := b.LineBytes(i)
charpos := 0
l, charpos, padMode, rPadded := findLineParams(b, start, end, i, r)
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)
}
match := r.FindIndex(l)
match := rPadded.FindIndex(l)
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}
end := Loc{charpos + util.RunePos(l, match[1]), i}
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-- {
l := b.LineBytes(i)
charpos := 0
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)
charCount := util.CharacterCount(b.LineBytes(i))
from := Loc{0, i}.Clamp(start, end)
to := Loc{charCount, i}.Clamp(start, end)
allMatches := b.findAll(r, from, to)
if allMatches != nil {
match := allMatches[len(allMatches)-1]
start := Loc{charpos + util.RunePos(l, match[0]), i}
end := Loc{charpos + util.RunePos(l, match[1]), i}
return [2]Loc{start, end}, true
return [2]Loc{match[0], match[1]}, true
}
}
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
// It returns the start and end location of the match (if found) and
// a boolean indicating if it was found
@ -146,49 +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
// 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
func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte) (int, int) {
func (b *Buffer) ReplaceRegex(start, end Loc, search *regexp.Regexp, replace []byte, captureGroups bool) (int, int) {
if start.GreaterThan(end) {
start, end = end, start
}
netrunes := 0
charsEnd := util.CharacterCount(b.LineBytes(end.Y))
found := 0
var deltas []Delta
for i := start.Y; i <= end.Y; i++ {
l := b.lines[i].data
charpos := 0
l := b.LineBytes(i)
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 {
l = util.SliceStart(l, end.X)
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == start.Y {
l = util.SliceEnd(l, start.X)
charpos = start.X
} else if i == end.Y {
l = util.SliceStart(l, end.X)
from := Loc{0, i}.Clamp(start, end)
to := Loc{charCount, i}.Clamp(start, end)
matches := b.findAll(search, from, to)
found += len(matches)
for j := len(matches) - 1; j >= 0; j-- {
// if we counted upwards, the different deltas would interfere
match := matches[j]
var newText []byte
if captureGroups {
newText = search.ReplaceAll(b.Substr(match[0], match[1]), replace)
} else {
newText = replace
}
deltas = append(deltas, Delta{newText, match[0], match[1]})
}
} else {
newLine := search.ReplaceAllFunc(l, func(in []byte) []byte {
found++
var result []byte
if captureGroups {
match := search.FindSubmatchIndex(in)
result = search.Expand(result, replace, in, match)
} else {
result = replace
}
return result
})
deltas = append(deltas, Delta{newLine, Loc{0, i}, Loc{charCount, i}})
}
newText := search.ReplaceAllFunc(l, func(in []byte) []byte {
result := []byte{}
for _, submatches := range search.FindAllSubmatchIndex(in, -1) {
result = search.Expand(result, replace, in, submatches)
}
found++
if i == end.Y {
netrunes += util.CharacterCount(result) - util.CharacterCount(in)
}
return result
})
from := Loc{charpos, i}
to := Loc{charpos + util.CharacterCount(l), i}
deltas = append(deltas, Delta{newText, from, to})
}
b.MultipleReplace(deltas)
return found, netrunes
return found, util.CharacterCount(b.LineBytes(end.Y)) - charsEnd
}

View File

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

View File

@ -1,36 +1,89 @@
package buffer
import (
"crypto/md5"
"reflect"
"github.com/zyedidia/micro/v2/internal/config"
ulua "github.com/zyedidia/micro/v2/internal/lua"
"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) SetOptionNative(option string, nativeValue interface{}) error {
func (b *Buffer) ReloadSettings(reloadFiletype bool) {
settings := config.ParsedSettings()
config.UpdatePathGlobLocals(settings, b.AbsPath)
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
b.Settings["filetype"] = "unknown"
if v, ok := settings["filetype"]; ok {
b.Settings["filetype"] = v
}
}
// update syntax rules, which will also update filetype if needed
b.UpdateRules()
curFiletype := b.Settings["filetype"].(string)
if oldFiletype != curFiletype {
b.doCallbacks("filetype", oldFiletype, curFiletype)
}
config.UpdateFileTypeLocals(settings, curFiletype)
for k, v := range config.DefaultCommonSettings() {
if k == "filetype" {
// prevent recursion
continue
}
if _, ok := config.VolatileSettings[k]; ok {
// reload should not override volatile settings
continue
}
if _, ok := b.LocalSettings[k]; ok {
// reload should not override local settings
continue
}
if _, ok := settings[k]; ok {
b.DoSetOptionNative(k, settings[k])
} else {
b.DoSetOptionNative(k, v)
}
}
}
func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) {
oldValue := b.Settings[option]
if reflect.DeepEqual(oldValue, nativeValue) {
return
}
b.Settings[option] = nativeValue
if option == "fastdirty" {
if !nativeValue.(bool) {
if !b.Modified() {
e := calcHash(b, &b.origHash)
if e == ErrFileTooLarge {
b.Settings["fastdirty"] = false
if b.Size() > LargeFileThreshold {
b.Settings["fastdirty"] = true
} else {
if !b.isModified {
calcHash(b, &b.origHash)
} else {
// prevent using an old stale origHash value
b.origHash = [md5.Size]byte{}
}
}
}
} else if option == "statusline" {
screen.Redraw()
} else if option == "filetype" {
config.InitRuntimeFiles()
err := config.ReadSettings()
if err != nil {
screen.TermMessage(err)
}
err = config.InitGlobalSettings()
if err != nil {
screen.TermMessage(err)
}
config.InitLocalSettings(b.Settings, b.Path)
b.UpdateRules()
b.ReloadSettings(false)
} else if option == "fileformat" {
switch b.Settings["fileformat"].(string) {
case "unix":
@ -46,6 +99,12 @@ func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
b.UpdateRules()
}
} 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
} else if option == "readonly" && b.Type.Kind == BTDefault.Kind {
b.Type.Readonly = nativeValue.(bool)
@ -74,12 +133,19 @@ func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
}
}
}
}
if b.OptionCallback != nil {
b.OptionCallback(option, nativeValue)
}
b.doCallbacks(option, oldValue, nativeValue)
}
func (b *Buffer) SetOptionNative(option string, nativeValue interface{}) error {
if err := config.OptionIsValid(option, nativeValue); err != nil {
return err
}
b.DoSetOptionNative(option, nativeValue)
b.LocalSettings[option] = true
return nil
}
@ -96,3 +162,15 @@ func (b *Buffer) SetOption(option, value string) error {
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"
"time"
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/tcell/v2"
)
type terminalClipboard struct{}

View File

@ -1,44 +1,49 @@
package config
import (
"sync"
"time"
)
var Autosave chan bool
var autotime int
// lock for autosave
var autolock sync.Mutex
var autotime chan float64
func init() {
Autosave = make(chan bool)
autotime = make(chan float64)
}
func SetAutoTime(a int) {
autolock.Lock()
autotime = a
autolock.Unlock()
}
func GetAutoTime() int {
autolock.Lock()
a := autotime
autolock.Unlock()
return a
func SetAutoTime(a float64) {
autotime <- a
}
func StartAutoSave() {
go func() {
var a float64
var t *time.Timer
var elapsed <-chan time.Time
for {
autolock.Lock()
a := autotime
autolock.Unlock()
if a < 1 {
break
select {
case a = <-autotime:
if t != nil {
t.Stop()
for len(elapsed) > 0 {
<-elapsed
}
}
if a > 0 {
if t != nil {
t.Reset(time.Duration(a * float64(time.Second)))
} else {
t = time.NewTimer(time.Duration(a * float64(time.Second)))
elapsed = t.C
}
}
case <-elapsed:
if a > 0 {
t.Reset(time.Duration(a * float64(time.Second)))
Autosave <- true
}
}
time.Sleep(time.Duration(a) * time.Second)
Autosave <- true
}
}()
}

View File

@ -6,7 +6,7 @@ import (
"strconv"
"strings"
"github.com/zyedidia/tcell/v2"
"github.com/micro-editor/tcell/v2"
)
// DefStyle is Micro's default style
@ -52,43 +52,55 @@ func InitColorscheme() error {
Colorscheme = make(map[string]tcell.Style)
DefStyle = tcell.StyleDefault
return LoadDefaultColorscheme()
c, err := LoadDefaultColorscheme()
if err == nil {
Colorscheme = c
}
return err
}
// LoadDefaultColorscheme loads the default colorscheme from $(ConfigDir)/colorschemes
func LoadDefaultColorscheme() error {
return LoadColorscheme(GlobalSettings["colorscheme"].(string))
func LoadDefaultColorscheme() (map[string]tcell.Style, error) {
var parsedColorschemes []string
return LoadColorscheme(GlobalSettings["colorscheme"].(string), &parsedColorschemes)
}
// LoadColorscheme loads the given colorscheme from a directory
func LoadColorscheme(colorschemeName string) error {
func LoadColorscheme(colorschemeName string, parsedColorschemes *[]string) (map[string]tcell.Style, error) {
c := make(map[string]tcell.Style)
file := FindRuntimeFile(RTColorscheme, colorschemeName)
if file == nil {
return errors.New(colorschemeName + " is not a valid colorscheme")
return c, errors.New(colorschemeName + " is not a valid colorscheme")
}
if data, err := file.Data(); err != nil {
return errors.New("Error loading colorscheme: " + err.Error())
return c, errors.New("Error loading colorscheme: " + err.Error())
} else {
Colorscheme, err = ParseColorscheme(string(data))
var err error
c, err = ParseColorscheme(file.Name(), string(data), parsedColorschemes)
if err != nil {
return err
return c, err
}
}
return nil
return c, nil
}
// ParseColorscheme parses the text definition for a colorscheme and returns the corresponding object
// Colorschemes are made up of color-link statements linking a color group to a list of colors
// For example, color-link keyword (blue,red) makes all keywords have a blue foreground and
// red background
func ParseColorscheme(text string) (map[string]tcell.Style, error) {
func ParseColorscheme(name string, text string, parsedColorschemes *[]string) (map[string]tcell.Style, error) {
var err error
parser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`)
colorParser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`)
includeParser := regexp.MustCompile(`include\s+"(.*)"`)
lines := strings.Split(text, "\n")
c := make(map[string]tcell.Style)
if parsedColorschemes != nil {
*parsedColorschemes = append(*parsedColorschemes, name)
}
lineLoop:
for _, line := range lines {
if strings.TrimSpace(line) == "" ||
strings.TrimSpace(line)[0] == '#' {
@ -96,7 +108,30 @@ func ParseColorscheme(text string) (map[string]tcell.Style, error) {
continue
}
matches := parser.FindSubmatch([]byte(line))
matches := includeParser.FindSubmatch([]byte(line))
if len(matches) == 2 {
// support includes only in case parsedColorschemes are given
if parsedColorschemes != nil {
include := string(matches[1])
for _, name := range *parsedColorschemes {
// check for circular includes...
if name == include {
// ...and prevent them
continue lineLoop
}
}
includeScheme, err := LoadColorscheme(include, parsedColorschemes)
if err != nil {
return c, err
}
for k, v := range includeScheme {
c[k] = v
}
}
continue
}
matches = colorParser.FindSubmatch([]byte(line))
if len(matches) == 3 {
link := string(matches[1])
colors := string(matches[2])

View File

@ -3,8 +3,8 @@ package config
import (
"testing"
"github.com/micro-editor/tcell/v2"
"github.com/stretchr/testify/assert"
"github.com/zyedidia/tcell/v2"
)
func TestSimpleStringToStyle(t *testing.T) {
@ -65,7 +65,7 @@ color-link constant "#AE81FF,#282828"
color-link constant.string "#E6DB74,#282828"
color-link constant.string.char "#BDE6AD,#282828"`
c, err := ParseColorscheme(testColorscheme)
c, err := ParseColorscheme("testColorscheme", testColorscheme, nil)
assert.Nil(t, err)
fg, bg, _ := c["comment"].Decompose()

View File

@ -71,7 +71,7 @@ type Plugin struct {
Info *PluginInfo // json file containing info
Srcs []RuntimeFile // lua files
Loaded bool
Default bool // pre-installed plugin
Default bool // pre-installed plugin
}
// IsLoaded returns if a plugin is enabled
@ -143,15 +143,3 @@ func FindPlugin(name string) *Plugin {
}
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"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
@ -14,8 +13,8 @@ import (
"sync"
"github.com/blang/semver"
"github.com/micro-editor/json5"
lua "github.com/yuin/gopher-lua"
"github.com/zyedidia/json5"
ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/util"
)
@ -396,7 +395,7 @@ func (pv *PluginVersion) DownloadAndInstall(out io.Writer) error {
return err
}
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}

View File

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

View File

@ -2,10 +2,8 @@ package config
import (
"errors"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"regexp"
"strings"
@ -40,6 +38,10 @@ var allFiles [][]RuntimeFile
var realFiles [][]RuntimeFile
func init() {
initRuntimeVars()
}
func initRuntimeVars() {
allFiles = make([][]RuntimeFile, NumTypes)
realFiles = make([][]RuntimeFile, NumTypes)
}
@ -58,12 +60,6 @@ type realFile string
// some asset file
type assetFile string
// some file on filesystem but with a different name
type namedFile struct {
realFile
name string
}
// a file with the data stored in memory
type memoryFile struct {
name string
@ -83,22 +79,18 @@ func (rf realFile) Name() string {
}
func (rf realFile) Data() ([]byte, error) {
return ioutil.ReadFile(string(rf))
return os.ReadFile(string(rf))
}
func (af assetFile) Name() string {
fn := path.Base(string(af))
return fn[:len(fn)-len(path.Ext(fn))]
fn := filepath.Base(string(af))
return fn[:len(fn)-len(filepath.Ext(fn))]
}
func (af assetFile) Data() ([]byte, error) {
return rt.Asset(string(af))
}
func (nf namedFile) Name() string {
return nf.name
}
// AddRuntimeFile registers a file for the given filetype
func AddRuntimeFile(fileType RTFiletype, file RuntimeFile) {
allFiles[fileType] = append(allFiles[fileType], file)
@ -113,7 +105,7 @@ func AddRealRuntimeFile(fileType RTFiletype, file RuntimeFile) {
// AddRuntimeFilesFromDirectory registers each file from the given directory for
// the filetype which matches the file-pattern
func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) {
files, _ := ioutil.ReadDir(directory)
files, _ := os.ReadDir(directory)
for _, f := range files {
if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok {
fullPath := filepath.Join(directory, f.Name())
@ -129,9 +121,17 @@ func AddRuntimeFilesFromAssets(fileType RTFiletype, directory, pattern string) {
if err != nil {
return
}
assetLoop:
for _, f := range files {
if ok, _ := path.Match(pattern, f); ok {
AddRuntimeFile(fileType, assetFile(path.Join(directory, f)))
if ok, _ := filepath.Match(pattern, f); ok {
af := assetFile(filepath.Join(directory, f))
for _, rf := range realFiles[fileType] {
if af.Name() == rf.Name() {
continue assetLoop
}
}
AddRuntimeFile(fileType, af)
}
}
}
@ -158,19 +158,30 @@ func ListRealRuntimeFiles(fileType RTFiletype) []RuntimeFile {
return realFiles[fileType]
}
// InitRuntimeFiles initializes all assets file and the config directory
func InitRuntimeFiles() {
// InitRuntimeFiles initializes all assets files and the config directory.
// If `user` is false, InitRuntimeFiles ignores the config directory and
// initializes asset files only.
func InitRuntimeFiles(user bool) {
add := func(fileType RTFiletype, dir, pattern string) {
AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern)
AddRuntimeFilesFromAssets(fileType, path.Join("runtime", dir), pattern)
if user {
AddRuntimeFilesFromDirectory(fileType, filepath.Join(ConfigDir, dir), pattern)
}
AddRuntimeFilesFromAssets(fileType, filepath.Join("runtime", dir), pattern)
}
initRuntimeVars()
add(RTColorscheme, "colorschemes", "*.micro")
add(RTSyntax, "syntax", "*.yaml")
add(RTSyntaxHeader, "syntax", "*.hdr")
add(RTHelp, "help", "*.md")
}
// InitPlugins initializes the plugins
func InitPlugins() {
Plugins = Plugins[:0]
initlua := filepath.Join(ConfigDir, "init.lua")
if _, err := os.Stat(initlua); !os.IsNotExist(err) {
p := new(Plugin)
p.Name = "initlua"
@ -181,14 +192,14 @@ func InitRuntimeFiles() {
// Search ConfigDir for plugin-scripts
plugdir := filepath.Join(ConfigDir, "plug")
files, _ := ioutil.ReadDir(plugdir)
files, _ := os.ReadDir(plugdir)
isID := regexp.MustCompile(`^[_A-Za-z0-9]+$`).MatchString
for _, d := range files {
plugpath := filepath.Join(plugdir, d.Name())
if stat, err := os.Stat(plugpath); err == nil && stat.IsDir() {
srcs, _ := ioutil.ReadDir(plugpath)
srcs, _ := os.ReadDir(plugpath)
p := new(Plugin)
p.Name = d.Name()
p.DirName = d.Name()
@ -196,7 +207,7 @@ func InitRuntimeFiles() {
if strings.HasSuffix(f.Name(), ".lua") {
p.Srcs = append(p.Srcs, realFile(filepath.Join(plugdir, d.Name(), f.Name())))
} else if strings.HasSuffix(f.Name(), ".json") {
data, err := ioutil.ReadFile(filepath.Join(plugdir, d.Name(), f.Name()))
data, err := os.ReadFile(filepath.Join(plugdir, d.Name(), f.Name()))
if err != nil {
continue
}
@ -218,7 +229,15 @@ func InitRuntimeFiles() {
plugdir = filepath.Join("runtime", "plugins")
if files, err := rt.AssetDir(plugdir); err == nil {
outer:
for _, d := range files {
for _, p := range Plugins {
if p.Name == d {
log.Println(p.Name, "built-in plugin overridden by user-defined one")
continue outer
}
}
if srcs, err := rt.AssetDir(filepath.Join(plugdir, d)); err == nil {
p := new(Plugin)
p.Name = d
@ -280,7 +299,7 @@ func PluginAddRuntimeFile(plugin string, filetype RTFiletype, filePath string) e
if _, err := os.Stat(fullpath); err == nil {
AddRealRuntimeFile(filetype, realFile(fullpath))
} else {
fullpath = path.Join("runtime", "plugins", pldir, filePath)
fullpath = filepath.Join("runtime", "plugins", pldir, filePath)
AddRuntimeFile(filetype, assetFile(fullpath))
}
return nil
@ -297,7 +316,7 @@ func PluginAddRuntimeFilesFromDirectory(plugin string, filetype RTFiletype, dire
if _, err := os.Stat(fullpath); err == nil {
AddRuntimeFilesFromDirectory(filetype, fullpath, pattern)
} else {
fullpath = path.Join("runtime", "plugins", pldir, directory)
fullpath = filepath.Join("runtime", "plugins", pldir, directory)
AddRuntimeFilesFromAssets(filetype, fullpath, pattern)
}
return nil

View File

@ -7,7 +7,7 @@ import (
)
func init() {
InitRuntimeFiles()
InitRuntimeFiles(false)
}
func TestAddFile(t *testing.T) {

View File

@ -4,340 +4,106 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"github.com/micro-editor/json5"
"github.com/zyedidia/glob"
"github.com/zyedidia/json5"
"github.com/zyedidia/micro/v2/internal/util"
"golang.org/x/text/encoding/htmlindex"
)
type optionValidator func(string, interface{}) error
var (
ErrInvalidOption = errors.New("Invalid option")
ErrInvalidValue = errors.New("Invalid value")
// The options that the user can set
GlobalSettings map[string]interface{}
// This is the raw parsed json
parsedSettings map[string]interface{}
settingsParseError bool
// ModifiedSettings is a map of settings which should be written to disk
// because they have been modified by the user in this session
ModifiedSettings map[string]bool
)
func init() {
ModifiedSettings = make(map[string]bool)
parsedSettings = make(map[string]interface{})
}
// Options with validators
// a list of settings that need option validators
var optionValidators = map[string]optionValidator{
"autosave": validateNonNegativeValue,
"clipboard": validateClipboard,
"tabsize": validatePositiveValue,
"scrollmargin": validateNonNegativeValue,
"scrollspeed": validateNonNegativeValue,
"colorscheme": validateColorscheme,
"colorcolumn": validateNonNegativeValue,
"fileformat": validateLineEnding,
"encoding": validateEncoding,
"multiopen": validateMultiOpen,
"reload": validateReload,
"autosave": validateNonNegativeValue,
"clipboard": validateChoice,
"colorcolumn": validateNonNegativeValue,
"colorscheme": validateColorscheme,
"detectlimit": validateNonNegativeValue,
"encoding": validateEncoding,
"fileformat": validateChoice,
"helpsplit": validateChoice,
"matchbracestyle": validateChoice,
"multiopen": validateChoice,
"pageoverlap": validateNonNegativeValue,
"reload": validateChoice,
"scrollmargin": validateNonNegativeValue,
"scrollspeed": validateNonNegativeValue,
"tabsize": validatePositiveValue,
"truecolor": validateChoice,
}
func ReadSettings() error {
filename := filepath.Join(ConfigDir, "settings.json")
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if err != nil {
settingsParseError = true
return errors.New("Error reading settings.json file: " + err.Error())
}
if !strings.HasPrefix(string(input), "null") {
// Unmarshal the input into the parsed map
err = json5.Unmarshal(input, &parsedSettings)
if err != nil {
settingsParseError = true
return errors.New("Error reading settings.json: " + err.Error())
}
// check if autosave is a boolean and convert it to float if so
if v, ok := parsedSettings["autosave"]; ok {
s, ok := v.(bool)
if ok {
if s {
parsedSettings["autosave"] = 8.0
} else {
parsedSettings["autosave"] = 0.0
}
}
}
}
}
return nil
}
func verifySetting(option string, value reflect.Type, def reflect.Type) bool {
var interfaceArr []interface{}
switch option {
case "pluginrepos", "pluginchannels":
return value.AssignableTo(reflect.TypeOf(interfaceArr))
default:
return def.AssignableTo(value)
}
}
// InitGlobalSettings initializes the options map and sets all options to their default values
// Must be called after ReadSettings
func InitGlobalSettings() error {
var err error
GlobalSettings = DefaultGlobalSettings()
for k, v := range parsedSettings {
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
if _, ok := GlobalSettings[k]; ok && !verifySetting(k, reflect.TypeOf(v), reflect.TypeOf(GlobalSettings[k])) {
err = fmt.Errorf("Global Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v), GlobalSettings[k], reflect.TypeOf(GlobalSettings[k]))
continue
}
GlobalSettings[k] = v
}
}
return err
}
// InitLocalSettings scans the json in settings.json and sets the options locally based
// on whether the filetype or path matches ft or glob local settings
// Must be called after ReadSettings
func InitLocalSettings(settings map[string]interface{}, path string) error {
var parseError error
for k, v := range parsedSettings {
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
if strings.HasPrefix(k, "ft:") {
if settings["filetype"].(string) == k[3:] {
for k1, v1 := range v.(map[string]interface{}) {
if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
parseError = fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v1), settings[k1], reflect.TypeOf(settings[k1]))
continue
}
settings[k1] = v1
}
}
} else {
g, err := glob.Compile(k)
if err != nil {
parseError = errors.New("Error with glob setting " + k + ": " + err.Error())
continue
}
if g.MatchString(path) {
for k1, v1 := range v.(map[string]interface{}) {
if _, ok := settings[k1]; ok && !verifySetting(k1, reflect.TypeOf(v1), reflect.TypeOf(settings[k1])) {
parseError = fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", k, reflect.TypeOf(v1), settings[k1], reflect.TypeOf(settings[k1]))
continue
}
settings[k1] = v1
}
}
}
}
}
return parseError
}
// WriteSettings writes the settings to the specified filename as JSON
func WriteSettings(filename string) error {
if settingsParseError {
// Don't write settings if there was a parse error
// because this will delete the settings.json if it
// is invalid. Instead we should allow the user to fix
// it manually.
return nil
}
var err error
if _, e := os.Stat(ConfigDir); e == nil {
defaults := DefaultGlobalSettings()
// remove any options froms parsedSettings that have since been marked as default
for k, v := range parsedSettings {
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
cur, okcur := GlobalSettings[k]
if def, ok := defaults[k]; ok && okcur && reflect.DeepEqual(cur, def) {
delete(parsedSettings, k)
}
}
}
// add any options to parsedSettings that have since been marked as non-default
for k, v := range GlobalSettings {
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
if _, wr := ModifiedSettings[k]; wr {
parsedSettings[k] = v
}
}
}
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
}
return err
}
// OverwriteSettings writes the current settings to settings.json and
// resets any user configuration of local settings present in settings.json
func OverwriteSettings(filename string) error {
settings := make(map[string]interface{})
var err error
if _, e := os.Stat(ConfigDir); e == nil {
defaults := DefaultGlobalSettings()
for k, v := range GlobalSettings {
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
if _, wr := ModifiedSettings[k]; wr {
settings[k] = v
}
}
}
txt, _ := json.MarshalIndent(settings, "", " ")
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644)
}
return err
}
// RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
func RegisterCommonOptionPlug(pl string, name string, defaultvalue interface{}) error {
name = pl + "." + name
if _, ok := GlobalSettings[name]; !ok {
defaultCommonSettings[name] = defaultvalue
GlobalSettings[name] = defaultvalue
err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
if err != nil {
return errors.New("Error writing settings.json file: " + err.Error())
}
} else {
defaultCommonSettings[name] = defaultvalue
}
return nil
}
// RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
return RegisterGlobalOption(pl+"."+name, defaultvalue)
}
// RegisterCommonOption creates a new option
func RegisterCommonOption(name string, defaultvalue interface{}) error {
if v, ok := GlobalSettings[name]; !ok {
defaultCommonSettings[name] = defaultvalue
GlobalSettings[name] = defaultvalue
err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
if err != nil {
return errors.New("Error writing settings.json file: " + err.Error())
}
} else {
defaultCommonSettings[name] = v
}
return nil
}
// RegisterGlobalOption creates a new global-only option
func RegisterGlobalOption(name string, defaultvalue interface{}) error {
if v, ok := GlobalSettings[name]; !ok {
DefaultGlobalOnlySettings[name] = defaultvalue
GlobalSettings[name] = defaultvalue
err := WriteSettings(filepath.Join(ConfigDir, "settings.json"))
if err != nil {
return errors.New("Error writing settings.json file: " + err.Error())
}
} else {
DefaultGlobalOnlySettings[name] = v
}
return nil
}
// GetGlobalOption returns the global value of the given option
func GetGlobalOption(name string) interface{} {
return GlobalSettings[name]
// a list of settings with pre-defined choices
var OptionChoices = map[string][]string{
"clipboard": {"internal", "external", "terminal"},
"fileformat": {"unix", "dos"},
"helpsplit": {"hsplit", "vsplit"},
"matchbracestyle": {"underline", "highlight"},
"multiopen": {"tab", "hsplit", "vsplit"},
"reload": {"prompt", "auto", "disabled"},
"truecolor": {"auto", "on", "off"},
}
// a list of settings that can be globally and locally modified and their
// default values
var defaultCommonSettings = map[string]interface{}{
"autoindent": true,
"autosu": false,
"backup": true,
"backupdir": "",
"basename": false,
"colorcolumn": float64(0),
"cursorline": true,
"diffgutter": false,
"encoding": "utf-8",
"eofnewline": true,
"fastdirty": false,
"fileformat": "unix",
"filetype": "unknown",
"hlsearch": false,
"incsearch": true,
"ignorecase": true,
"indentchar": " ",
"keepautoindent": false,
"matchbrace": true,
"mkparents": false,
"permbackup": false,
"readonly": false,
"reload": "prompt",
"rmtrailingws": false,
"ruler": true,
"relativeruler": false,
"savecursor": false,
"saveundo": false,
"scrollbar": false,
"scrollmargin": float64(3),
"scrollspeed": float64(2),
"smartpaste": true,
"softwrap": false,
"splitbottom": true,
"splitright": true,
"statusformatl": "$(filename) $(modified)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
"statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
"statusline": true,
"syntax": true,
"tabmovement": false,
"tabsize": float64(4),
"tabstospaces": false,
"useprimary": true,
"wordwrap": false,
}
func GetInfoBarOffset() int {
offset := 0
if GetGlobalOption("infobar").(bool) {
offset++
}
if GetGlobalOption("keymenu").(bool) {
offset += 2
}
return offset
}
// DefaultCommonSettings returns the default global settings for micro
// Note that colorscheme is a global only option
func DefaultCommonSettings() map[string]interface{} {
commonsettings := make(map[string]interface{})
for k, v := range defaultCommonSettings {
commonsettings[k] = v
}
return commonsettings
"autoindent": true,
"autosu": false,
"backup": true,
"backupdir": "",
"basename": false,
"colorcolumn": float64(0),
"cursorline": true,
"detectlimit": float64(100),
"diffgutter": false,
"encoding": "utf-8",
"eofnewline": true,
"fastdirty": false,
"fileformat": defaultFileFormat(),
"filetype": "unknown",
"hlsearch": false,
"hltaberrors": false,
"hltrailingws": false,
"ignorecase": true,
"incsearch": true,
"indentchar": " ",
"keepautoindent": false,
"matchbrace": true,
"matchbraceleft": true,
"matchbracestyle": "underline",
"mkparents": false,
"pageoverlap": float64(2),
"permbackup": false,
"readonly": false,
"relativeruler": false,
"reload": "prompt",
"rmtrailingws": false,
"ruler": true,
"savecursor": false,
"saveundo": false,
"scrollbar": false,
"scrollmargin": float64(3),
"scrollspeed": float64(2),
"smartpaste": true,
"softwrap": false,
"splitbottom": true,
"splitright": true,
"statusformatl": "$(filename) $(modified)$(overwrite)($(line),$(col)) $(status.paste)| ft:$(opt:filetype) | $(opt:fileformat) | $(opt:encoding)",
"statusformatr": "$(bind:ToggleKeyMenu): bindings, $(bind:ToggleHelp): help",
"statusline": true,
"syntax": true,
"tabmovement": false,
"tabsize": float64(4),
"tabstospaces": false,
"truecolor": "auto",
"useprimary": true,
"wordwrap": false,
}
// a list of settings that should only be globally modified and their
@ -349,6 +115,7 @@ var DefaultGlobalOnlySettings = map[string]interface{}{
"divchars": "|-",
"divreverse": true,
"fakecursor": false,
"helpsplit": "hsplit",
"infobar": true,
"keymenu": false,
"mouse": true,
@ -371,21 +138,322 @@ var LocalSettings = []string{
"readonly",
}
// DefaultGlobalSettings returns the default global settings for micro
// Note that colorscheme is a global only option
func DefaultGlobalSettings() map[string]interface{} {
globalsettings := make(map[string]interface{})
for k, v := range defaultCommonSettings {
globalsettings[k] = v
}
for k, v := range DefaultGlobalOnlySettings {
globalsettings[k] = v
}
return globalsettings
var (
ErrInvalidOption = errors.New("Invalid option")
ErrInvalidValue = errors.New("Invalid value")
// The options that the user can set
GlobalSettings map[string]interface{}
// This is the raw parsed json
parsedSettings map[string]interface{}
settingsParseError bool
// ModifiedSettings is a map of settings which should be written to disk
// because they have been modified by the user in this session
ModifiedSettings map[string]bool
// VolatileSettings is a map of settings which should not be written to disk
// because they have been temporarily set for this session only
VolatileSettings map[string]bool
)
func writeFile(name string, txt []byte) error {
return util.SafeWrite(name, txt, false)
}
// DefaultAllSettings returns a map of all settings and their
// default values (both common and global settings)
func init() {
ModifiedSettings = make(map[string]bool)
VolatileSettings = make(map[string]bool)
}
func validateParsedSettings() error {
var err error
defaults := DefaultAllSettings()
for k, v := range parsedSettings {
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
if strings.HasPrefix(k, "ft:") {
for k1, v1 := range v.(map[string]interface{}) {
if _, ok := defaults[k1]; ok {
if e := verifySetting(k1, v1, defaults[k1]); e != nil {
err = e
parsedSettings[k].(map[string]interface{})[k1] = defaults[k1]
continue
}
}
}
} else {
if _, e := glob.Compile(k); e != nil {
err = errors.New("Error with glob setting " + k + ": " + e.Error())
delete(parsedSettings, k)
continue
}
for k1, v1 := range v.(map[string]interface{}) {
if _, ok := defaults[k1]; ok {
if e := verifySetting(k1, v1, defaults[k1]); e != nil {
err = e
parsedSettings[k].(map[string]interface{})[k1] = defaults[k1]
continue
}
}
}
}
continue
}
if k == "autosave" {
// if autosave is a boolean convert it to float
s, ok := v.(bool)
if ok {
if s {
parsedSettings["autosave"] = 8.0
} else {
parsedSettings["autosave"] = 0.0
}
}
continue
}
if _, ok := defaults[k]; ok {
if e := verifySetting(k, v, defaults[k]); e != nil {
err = e
parsedSettings[k] = defaults[k]
continue
}
}
}
return err
}
func ReadSettings() error {
parsedSettings = make(map[string]interface{})
filename := filepath.Join(ConfigDir, "settings.json")
if _, e := os.Stat(filename); e == nil {
input, err := os.ReadFile(filename)
if err != nil {
settingsParseError = true
return errors.New("Error reading settings.json file: " + err.Error())
}
if !strings.HasPrefix(string(input), "null") {
// Unmarshal the input into the parsed map
err = json5.Unmarshal(input, &parsedSettings)
if err != nil {
settingsParseError = true
return errors.New("Error reading settings.json: " + err.Error())
}
err = validateParsedSettings()
if err != nil {
return err
}
}
}
return nil
}
func ParsedSettings() map[string]interface{} {
s := make(map[string]interface{})
for k, v := range parsedSettings {
s[k] = v
}
return s
}
func verifySetting(option string, value interface{}, def interface{}) error {
var interfaceArr []interface{}
valType := reflect.TypeOf(value)
defType := reflect.TypeOf(def)
assignable := false
switch option {
case "pluginrepos", "pluginchannels":
assignable = valType.AssignableTo(reflect.TypeOf(interfaceArr))
default:
assignable = defType.AssignableTo(valType)
}
if !assignable {
return fmt.Errorf("Error: setting '%s' has incorrect type (%s), using default value: %v (%s)", option, valType, def, defType)
}
if err := OptionIsValid(option, value); err != nil {
return err
}
return nil
}
// InitGlobalSettings initializes the options map and sets all options to their default values
// Must be called after ReadSettings
func InitGlobalSettings() error {
var err error
GlobalSettings = DefaultAllSettings()
for k, v := range parsedSettings {
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
GlobalSettings[k] = v
}
}
return err
}
// UpdatePathGlobLocals scans the already parsed settings and sets the options locally
// based on whether the path matches a glob
// Must be called after ReadSettings
func UpdatePathGlobLocals(settings map[string]interface{}, path string) {
for k, v := range parsedSettings {
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") && !strings.HasPrefix(k, "ft:") {
g, _ := glob.Compile(k)
if g.MatchString(path) {
for k1, v1 := range v.(map[string]interface{}) {
settings[k1] = v1
}
}
}
}
}
// 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
}
}
}
}
}
}
// WriteSettings writes the settings to the specified filename as JSON
func WriteSettings(filename string) error {
if settingsParseError {
// Don't write settings if there was a parse error
// because this will delete the settings.json if it
// is invalid. Instead we should allow the user to fix
// it manually.
return nil
}
var err error
if _, e := os.Stat(ConfigDir); e == nil {
defaults := DefaultAllSettings()
// remove any options froms parsedSettings that have since been marked as default
for k, v := range parsedSettings {
if !strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
cur, okcur := GlobalSettings[k]
_, vol := VolatileSettings[k]
if def, ok := defaults[k]; ok && okcur && !vol && reflect.DeepEqual(cur, def) {
delete(parsedSettings, k)
}
}
}
// add any options to parsedSettings that have since been marked as non-default
for k, v := range GlobalSettings {
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
if _, wr := ModifiedSettings[k]; wr {
parsedSettings[k] = v
}
}
}
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
txt = append(txt, '\n')
err = writeFile(filename, txt)
}
return err
}
// OverwriteSettings writes the current settings to settings.json and
// resets any user configuration of local settings present in settings.json
func OverwriteSettings(filename string) error {
settings := make(map[string]interface{})
var err error
if _, e := os.Stat(ConfigDir); e == nil {
defaults := DefaultAllSettings()
for k, v := range GlobalSettings {
if def, ok := defaults[k]; !ok || !reflect.DeepEqual(v, def) {
if _, wr := ModifiedSettings[k]; wr {
settings[k] = v
}
}
}
txt, _ := json.MarshalIndent(parsedSettings, "", " ")
txt = append(txt, '\n')
err = writeFile(filename, txt)
}
return err
}
// RegisterCommonOptionPlug creates a new option (called pl.name). This is meant to be called by plugins to add options.
func RegisterCommonOptionPlug(pl string, name string, defaultvalue interface{}) error {
return RegisterCommonOption(pl+"."+name, defaultvalue)
}
// RegisterGlobalOptionPlug creates a new global-only option (named pl.name)
func RegisterGlobalOptionPlug(pl string, name string, defaultvalue interface{}) error {
return RegisterGlobalOption(pl+"."+name, defaultvalue)
}
// RegisterCommonOption creates a new option
func RegisterCommonOption(name string, defaultvalue interface{}) error {
if _, ok := GlobalSettings[name]; !ok {
GlobalSettings[name] = defaultvalue
}
defaultCommonSettings[name] = defaultvalue
return nil
}
// RegisterGlobalOption creates a new global-only option
func RegisterGlobalOption(name string, defaultvalue interface{}) error {
if _, ok := GlobalSettings[name]; !ok {
GlobalSettings[name] = defaultvalue
}
DefaultGlobalOnlySettings[name] = defaultvalue
return nil
}
// GetGlobalOption returns the global value of the given option
func GetGlobalOption(name string) interface{} {
return GlobalSettings[name]
}
func defaultFileFormat() string {
if runtime.GOOS == "windows" {
return "dos"
}
return "unix"
}
func GetInfoBarOffset() int {
offset := 0
if GetGlobalOption("infobar").(bool) {
offset++
}
if GetGlobalOption("keymenu").(bool) {
offset += 2
}
return offset
}
// DefaultCommonSettings returns a map of all common buffer settings
// and their default values
func DefaultCommonSettings() map[string]interface{} {
commonsettings := make(map[string]interface{})
for k, v := range defaultCommonSettings {
commonsettings[k] = v
}
return commonsettings
}
// DefaultAllSettings returns a map of all common buffer & global-only settings
// and their default values
func DefaultAllSettings() map[string]interface{} {
allsettings := make(map[string]interface{})
for k, v := range defaultCommonSettings {
@ -410,18 +478,15 @@ func GetNativeValue(option string, realValue interface{}, value string) (interfa
} else if kind == reflect.String {
native = value
} else if kind == reflect.Float64 {
i, err := strconv.Atoi(value)
f, err := strconv.ParseFloat(value, 64)
if err != nil {
return nil, ErrInvalidValue
}
native = float64(i)
native = f
} else {
return nil, ErrInvalidValue
}
if err := OptionIsValid(option, native); err != nil {
return nil, err
}
return native, nil
}
@ -437,13 +502,13 @@ func OptionIsValid(option string, value interface{}) error {
// Option validators
func validatePositiveValue(option string, value interface{}) error {
tabsize, ok := value.(float64)
nativeValue, ok := value.(float64)
if !ok {
return errors.New("Expected numeric type for " + option)
}
if tabsize < 1 {
if nativeValue < 1 {
return errors.New(option + " must be greater than 0")
}
@ -464,6 +529,26 @@ func validateNonNegativeValue(option string, value interface{}) error {
return nil
}
func validateChoice(option string, value interface{}) error {
if choices, ok := OptionChoices[option]; ok {
val, ok := value.(string)
if !ok {
return errors.New("Expected string type for " + option)
}
for _, v := range choices {
if val == v {
return nil
}
}
choicesStr := strings.Join(choices, ", ")
return errors.New(option + " must be one of: " + choicesStr)
}
return errors.New("Option has no pre-defined choices")
}
func validateColorscheme(option string, value interface{}) error {
colorscheme, ok := value.(string)
@ -478,69 +563,7 @@ func validateColorscheme(option string, value interface{}) error {
return nil
}
func validateClipboard(option string, value interface{}) error {
val, ok := value.(string)
if !ok {
return errors.New("Expected string type for clipboard")
}
switch val {
case "internal", "external", "terminal":
default:
return errors.New(option + " must be 'internal', 'external', or 'terminal'")
}
return nil
}
func validateLineEnding(option string, value interface{}) error {
endingType, ok := value.(string)
if !ok {
return errors.New("Expected string type for file format")
}
if endingType != "unix" && endingType != "dos" {
return errors.New("File format must be either 'unix' or 'dos'")
}
return nil
}
func validateEncoding(option string, value interface{}) error {
_, err := htmlindex.Get(value.(string))
return err
}
func validateMultiOpen(option string, value interface{}) error {
val, ok := value.(string)
if !ok {
return errors.New("Expected string type for multiopen")
}
switch val {
case "tab", "hsplit", "vsplit":
default:
return errors.New(option + " must be 'tab', 'hsplit', or 'vsplit'")
}
return nil
}
func validateReload(option string, value interface{}) error {
val, ok := value.(string)
if !ok {
return errors.New("Expected string type for reload")
}
switch val {
case "prompt", "auto", "disabled":
default:
return errors.New(option + " must be 'prompt', 'auto' or 'disabled'")
}
return nil
}

View File

@ -4,11 +4,11 @@ import (
"strconv"
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/config"
"github.com/zyedidia/micro/v2/internal/screen"
"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.
@ -58,9 +58,15 @@ func (w *BufWindow) SetBuffer(b *buffer.Buffer) {
if option == "softwrap" || option == "wordwrap" {
w.Relocate()
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 {
return w.VLocFromLoc(loc).VisualX
@ -129,6 +135,11 @@ func (w *BufWindow) updateDisplayInfo() {
w.bufHeight--
}
scrollbarWidth := 0
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height && w.Width > 0 {
scrollbarWidth = 1
}
w.hasMessage = len(b.Messages) > 0
// We need to know the string length of the largest line number
@ -146,16 +157,16 @@ func (w *BufWindow) updateDisplayInfo() {
w.gutterOffset += w.maxLineNumLength + 1
}
prevBufWidth := w.bufWidth
w.bufWidth = w.Width - w.gutterOffset
if w.Buf.Settings["scrollbar"].(bool) && w.Buf.LinesNum() > w.Height {
w.bufWidth--
if w.gutterOffset > w.Width-scrollbarWidth {
w.gutterOffset = w.Width - scrollbarWidth
}
prevBufWidth := w.bufWidth
w.bufWidth = w.Width - w.gutterOffset - scrollbarWidth
if w.bufWidth != prevBufWidth && w.Buf.Settings["softwrap"].(bool) {
for _, c := range w.Buf.GetCursors() {
c.LastVisualX = c.GetVisualX()
c.LastWrappedVisualX = c.GetVisualX(true)
}
}
}
@ -233,7 +244,7 @@ func (w *BufWindow) Relocate() bool {
// horizontal relocation (scrolling)
if !b.Settings["softwrap"].(bool) {
cx := activeC.GetVisualX()
cx := activeC.GetVisualX(false)
rw := runewidth.RuneWidth(activeC.RuneUnder(activeC.X))
if rw == 0 {
rw = 1 // tab or newline
@ -243,8 +254,8 @@ func (w *BufWindow) Relocate() bool {
w.StartCol = cx
ret = true
}
if cx+w.gutterOffset+rw > w.StartCol+w.Width {
w.StartCol = cx - w.Width + w.gutterOffset + rw
if cx+rw > w.StartCol+w.bufWidth {
w.StartCol = cx - w.bufWidth + rw
ret = true
}
}
@ -277,13 +288,17 @@ func (w *BufWindow) drawGutter(vloc *buffer.Loc, bloc *buffer.Loc) {
break
}
}
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
vloc.X++
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
vloc.X++
for i := 0; i < 2 && vloc.X < w.gutterOffset; i++ {
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, char, nil, s)
vloc.X++
}
}
func (w *BufWindow) drawDiffGutter(backgroundStyle tcell.Style, softwrapped bool, vloc *buffer.Loc, bloc *buffer.Loc) {
if vloc.X >= w.gutterOffset {
return
}
symbol := ' '
styleName := ""
@ -319,26 +334,28 @@ func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, vloc
} else {
lineInt = bloc.Y - cursorLine
}
lineNum := strconv.Itoa(util.Abs(lineInt))
lineNum := []rune(strconv.Itoa(util.Abs(lineInt)))
// Write the spaces before the line number if necessary
for i := 0; i < w.maxLineNumLength-len(lineNum); i++ {
for i := 0; i < w.maxLineNumLength-len(lineNum) && vloc.X < w.gutterOffset; i++ {
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
vloc.X++
}
// Write the actual line number
for _, ch := range lineNum {
if softwrapped {
for i := 0; i < len(lineNum) && vloc.X < w.gutterOffset; i++ {
if softwrapped || (w.bufWidth == 0 && w.Buf.Settings["softwrap"] == true) {
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
} else {
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ch, nil, lineNumStyle)
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, lineNum[i], nil, lineNumStyle)
}
vloc.X++
}
// Write the extra space
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
vloc.X++
if vloc.X < w.gutterOffset {
screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, ' ', nil, lineNumStyle)
vloc.X++
}
}
// getStyle returns the highlight style for the given character position
@ -373,18 +390,7 @@ func (w *BufWindow) displayBuffer() {
if b.ModifiedThisFrame {
if b.Settings["diffgutter"].(bool) {
b.UpdateDiff(func(synchronous bool) {
// If the diff was updated asynchronously, the outer call to
// displayBuffer might already be completed and we need to
// schedule a redraw in order to display the new diff.
// Note that this cannot lead to an infinite recursion
// because the modifications were cleared above so there won't
// be another call to UpdateDiff when displayBuffer is called
// during the redraw.
if !synchronous {
screen.Redraw()
}
})
b.UpdateDiff()
}
b.ModifiedThisFrame = false
}
@ -392,26 +398,20 @@ func (w *BufWindow) displayBuffer() {
var matchingBraces []buffer.Loc
// bracePairs is defined in buffer.go
if b.Settings["matchbrace"].(bool) {
for _, bp := range buffer.BracePairs {
for _, c := range b.GetCursors() {
if c.HasSelection() {
continue
}
curX := c.X
curLoc := c.Loc
for _, c := range b.GetCursors() {
if c.HasSelection() {
continue
}
r := c.RuneUnder(curX)
rl := c.RuneUnder(curX - 1)
if r == bp[0] || r == bp[1] || rl == bp[0] || rl == bp[1] {
mb, left, found := b.FindMatchingBrace(bp, curLoc)
if found {
matchingBraces = append(matchingBraces, mb)
if !left {
matchingBraces = append(matchingBraces, curLoc)
} else {
matchingBraces = append(matchingBraces, curLoc.Move(-1, b))
}
mb, left, found := b.FindMatchingBrace(c.Loc)
if found {
matchingBraces = append(matchingBraces, mb)
if !left {
if b.Settings["matchbracestyle"].(string) != "highlight" {
matchingBraces = append(matchingBraces, c.Loc)
}
} else {
matchingBraces = append(matchingBraces, c.Loc.Move(-1, b))
}
}
}
@ -455,7 +455,7 @@ func (w *BufWindow) displayBuffer() {
currentLine := false
for _, c := range cursors {
if bloc.Y == c.Y && w.active {
if !c.HasSelection() && bloc.Y == c.Y && w.active {
currentLine = true
break
}
@ -482,6 +482,12 @@ func (w *BufWindow) displayBuffer() {
vloc.X = w.gutterOffset
}
bline := b.LineBytes(bloc.Y)
blineLen := util.CharacterCount(bline)
leadingwsEnd := len(util.GetLeadingWhitespace(bline))
trailingwsStart := blineLen - util.CharacterCount(util.GetTrailingWhitespace(bline))
line, nColsBeforeStart, bslice, startStyle := w.getStartInfo(w.StartCol, bloc.Y)
if startStyle != nil {
curStyle = *startStyle
@ -505,6 +511,37 @@ func (w *BufWindow) displayBuffer() {
// over cursor-line and color-column
dontOverrideBackground := origBg != defBg
if b.Settings["hltaberrors"].(bool) {
if s, ok := config.Colorscheme["tab-error"]; ok {
isTab := (r == '\t') || (r == ' ' && !showcursor)
if (b.Settings["tabstospaces"].(bool) && isTab) ||
(!b.Settings["tabstospaces"].(bool) && bloc.X < leadingwsEnd && r == ' ' && !isTab) {
fg, _, _ := s.Decompose()
style = style.Background(fg)
dontOverrideBackground = true
}
}
}
if b.Settings["hltrailingws"].(bool) {
if s, ok := config.Colorscheme["trailingws"]; ok {
if bloc.X >= trailingwsStart && bloc.X < blineLen {
hl := true
for _, c := range cursors {
if c.NewTrailingWsY == bloc.Y {
hl = false
break
}
}
if hl {
fg, _, _ := s.Decompose()
style = style.Background(fg)
dontOverrideBackground = true
}
}
}
}
for _, c := range cursors {
if c.HasSelection() &&
(bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) ||
@ -557,7 +594,15 @@ func (w *BufWindow) displayBuffer() {
for _, mb := range matchingBraces {
if mb.X == bloc.X && mb.Y == bloc.Y {
style = style.Underline(true)
if b.Settings["matchbracestyle"].(string) == "highlight" {
if s, ok := config.Colorscheme["match-brace"]; ok {
style = s
} else {
style = style.Reverse(true)
}
} else {
style = style.Underline(true)
}
}
}
}
@ -580,16 +625,21 @@ func (w *BufWindow) displayBuffer() {
wrap := func() {
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 b.Settings["ruler"].(bool) {
w.drawLineNum(lineNumStyle, true, &vloc, &bloc)
if vloc.Y >= 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 b.Settings["ruler"].(bool) {
w.drawLineNum(lineNumStyle, true, &vloc, &bloc)
}
} else {
vloc.X = w.gutterOffset
}
}
@ -609,7 +659,7 @@ func (w *BufWindow) displayBuffer() {
wordwidth := 0
totalwidth := w.StartCol - nColsBeforeStart
for len(line) > 0 {
for len(line) > 0 && vloc.X < maxWidth {
r, combc, size := util.DecodeCharacter(line)
line = line[size:]

View File

@ -2,12 +2,12 @@ package display
import (
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/config"
"github.com/zyedidia/micro/v2/internal/info"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2"
)
type InfoWindow struct {

View File

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

View File

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

View File

@ -2,7 +2,7 @@ package display
import (
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/config"
"github.com/zyedidia/micro/v2/internal/screen"
@ -112,10 +112,10 @@ func (w *TabWindow) Display() {
}
return tabBarStyle, tabBarActiveStyle
}
draw := func(r rune, n int, active bool, reversed bool) {
tabBarStyle, tabBarActiveStyle := reverseStyles(reversed)
style := tabBarStyle
if active {
style = tabBarActiveStyle
@ -147,15 +147,15 @@ func (w *TabWindow) Display() {
} else {
draw(' ', 1, false, tabCharHighlight)
}
for _, c := range n {
draw(c, 1, i == w.active, tabCharHighlight)
}
if i == len(w.Names)-1 {
done = true
}
if i == w.active {
draw(']', 1, true, tabCharHighlight)
draw(' ', 2, true, globalTabReverse)
@ -163,7 +163,7 @@ func (w *TabWindow) Display() {
draw(' ', 1, false, tabCharHighlight)
draw(' ', 2, false, globalTabReverse)
}
if x >= w.Width {
break
}

View File

@ -1,13 +1,13 @@
package display
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/config"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/shell"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2"
"github.com/zyedidia/terminal"
)
type TermWindow struct {
@ -110,6 +110,8 @@ func (w *TermWindow) Display() {
}
if w.State.CursorVisible() && w.active {
curx, cury := w.State.Cursor()
screen.ShowCursor(curx+w.X, cury+w.Y)
if curx < w.Width && cury < w.Height {
screen.ShowCursor(curx+w.X, cury+w.Y)
}
}
}

View File

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

View File

@ -7,7 +7,7 @@ import (
)
// The InfoBuf displays messages and other info at the bottom of the screen.
// It is respresented as a buffer and a message with a style.
// It is represented as a buffer and a message with a style.
type InfoBuf struct {
*buffer.Buffer
@ -143,13 +143,12 @@ func (i *InfoBuf) DonePrompt(canceled bool) {
if i.PromptCallback != nil {
if canceled {
i.Replace(i.Start(), i.End(), "")
i.PromptCallback("", true)
h := i.History[i.PromptType]
i.History[i.PromptType] = h[:len(h)-1]
i.PromptCallback("", true)
} else {
resp := string(i.LineBytes(0))
i.Replace(i.Start(), i.End(), "")
i.PromptCallback(resp, false)
h := i.History[i.PromptType]
h[len(h)-1] = resp
@ -160,6 +159,8 @@ func (i *InfoBuf) DonePrompt(canceled bool) {
break
}
}
i.PromptCallback(resp, false)
}
// i.PromptCallback = nil
}

View File

@ -17,7 +17,6 @@ import (
"regexp"
"runtime"
"strings"
"sync"
"time"
"unicode/utf8"
@ -27,7 +26,6 @@ import (
)
var L *lua.LState
var Lock sync.Mutex
// LoadFile loads a lua file
func LoadFile(module string, file string, data []byte) error {
@ -127,6 +125,7 @@ func importIo() *lua.LTable {
L.SetField(pkg, "MultiWriter", luar.New(L, io.MultiWriter))
L.SetField(pkg, "NewSectionReader", luar.New(L, io.NewSectionReader))
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, "ReadFull", luar.New(L, io.ReadFull))
L.SetField(pkg, "TeeReader", luar.New(L, io.TeeReader))
@ -372,6 +371,8 @@ func importOs() *lua.LTable {
L.SetField(pkg, "PathListSeparator", luar.New(L, os.PathListSeparator))
L.SetField(pkg, "PathSeparator", luar.New(L, os.PathSeparator))
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, "Remove", luar.New(L, os.Remove))
L.SetField(pkg, "RemoveAll", luar.New(L, os.RemoveAll))
@ -390,6 +391,7 @@ func importOs() *lua.LTable {
L.SetField(pkg, "TempDir", luar.New(L, os.TempDir))
L.SetField(pkg, "Truncate", luar.New(L, os.Truncate))
L.SetField(pkg, "UserHomeDir", luar.New(L, os.UserHomeDir))
L.SetField(pkg, "WriteFile", luar.New(L, os.WriteFile))
return pkg
}
@ -425,7 +427,6 @@ func importPath() *lua.LTable {
func importFilePath() *lua.LTable {
pkg := L.NewTable()
L.SetField(pkg, "Join", luar.New(L, filepath.Join))
L.SetField(pkg, "Abs", luar.New(L, filepath.Abs))
L.SetField(pkg, "Base", luar.New(L, filepath.Base))
L.SetField(pkg, "Clean", luar.New(L, filepath.Clean))
@ -523,21 +524,16 @@ func importErrors() *lua.LTable {
func importTime() *lua.LTable {
pkg := L.NewTable()
L.SetField(pkg, "After", luar.New(L, time.After))
L.SetField(pkg, "Sleep", luar.New(L, time.Sleep))
L.SetField(pkg, "Tick", luar.New(L, time.Tick))
L.SetField(pkg, "Since", luar.New(L, time.Since))
L.SetField(pkg, "FixedZone", luar.New(L, time.FixedZone))
L.SetField(pkg, "LoadLocation", luar.New(L, time.LoadLocation))
L.SetField(pkg, "NewTicker", luar.New(L, time.NewTicker))
L.SetField(pkg, "Date", luar.New(L, time.Date))
L.SetField(pkg, "Now", luar.New(L, time.Now))
L.SetField(pkg, "Parse", luar.New(L, time.Parse))
L.SetField(pkg, "ParseDuration", luar.New(L, time.ParseDuration))
L.SetField(pkg, "ParseInLocation", luar.New(L, time.ParseInLocation))
L.SetField(pkg, "Unix", luar.New(L, time.Unix))
L.SetField(pkg, "AfterFunc", luar.New(L, time.AfterFunc))
L.SetField(pkg, "NewTimer", luar.New(L, time.NewTimer))
L.SetField(pkg, "Nanosecond", luar.New(L, time.Nanosecond))
L.SetField(pkg, "Microsecond", luar.New(L, time.Microsecond))
L.SetField(pkg, "Millisecond", luar.New(L, time.Millisecond))
@ -545,6 +541,15 @@ func importTime() *lua.LTable {
L.SetField(pkg, "Minute", luar.New(L, time.Minute))
L.SetField(pkg, "Hour", luar.New(L, time.Hour))
// TODO: these raw Go timer APIs don't provide any synchronization
// with micro. Stop exposing them to lua once plugins switch to using
// the safer micro.After() interface instead. See issue #3209
L.SetField(pkg, "After", luar.New(L, time.After))
L.SetField(pkg, "AfterFunc", luar.New(L, time.AfterFunc))
L.SetField(pkg, "NewTicker", luar.New(L, time.NewTicker))
L.SetField(pkg, "NewTimer", luar.New(L, time.NewTimer))
L.SetField(pkg, "Tick", luar.New(L, time.Tick))
return pkg
}

View File

@ -6,9 +6,9 @@ import (
"os"
"sync"
"github.com/micro-editor/tcell/v2"
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/tcell/v2"
)
// Screen is the tcell screen we use to draw to the terminal
@ -22,6 +22,10 @@ var Screen tcell.Screen
// Events is the channel of tcell events
var Events chan (tcell.Event)
// RestartCallback is called when the screen is restarted after it was
// temporarily shut down
var RestartCallback func()
// The lock is necessary since the screen is polled on a separate thread
var lock sync.Mutex
@ -29,6 +33,12 @@ var lock sync.Mutex
// written to even if no event user event has occurred
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
func Lock() {
lock.Lock()
@ -117,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
func TempFini() bool {
screenWasNil := Screen == nil
@ -134,6 +172,10 @@ func TempStart(screenWasNil bool) {
if !screenWasNil {
Init()
Unlock()
if RestartCallback != nil {
RestartCallback()
}
}
}
@ -142,10 +184,13 @@ func Init() error {
drawChan = make(chan bool, 8)
// Should we enable true color?
truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
if !truecolor {
truecolor := config.GetGlobalOption("truecolor").(string)
if truecolor == "on" || (truecolor == "auto" && os.Getenv("MICRO_TRUECOLOR") == "1") {
os.Setenv("TCELL_TRUECOLOR", "enable")
} else if truecolor == "off" {
os.Setenv("TCELL_TRUECOLOR", "disable")
} else {
// For "auto", tcell already autodetects truecolor by default
}
var oldTerm string
@ -187,6 +232,10 @@ func Init() error {
Screen.EnableMouse()
}
for _, r := range rawSeq {
Screen.RegisterRawSeq(r)
}
return nil
}

View File

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

View File

@ -11,6 +11,7 @@ import (
shellquote "github.com/kballard/go-shellquote"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util"
)
// ExecCommand executes a command using exec
@ -95,28 +96,30 @@ func RunInteractiveShell(input string, wait bool, getOutput bool) (string, error
cmd.Stderr = os.Stderr
// This is a trap for Ctrl-C so that it doesn't kill micro
// Instead we trap Ctrl-C to kill the program we're running
// micro is killed if the signal is ignored only on Windows, so it is
// received
c := make(chan os.Signal, 1)
signal.Reset(os.Interrupt)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
cmd.Process.Kill()
err = cmd.Start()
if err == nil {
err = cmd.Wait()
if wait {
// This is just so we don't return right away and let the user press enter to return
screen.TermMessage("")
}
}()
cmd.Start()
err = cmd.Wait()
} else {
screen.TermMessage(err)
}
output := outputBytes.String()
if wait {
// This is just so we don't return right away and let the user press enter to return
screen.TermMessage("")
}
// Start the screen back up
screen.TempStart(screenb)
signal.Notify(util.Sigterm, os.Interrupt)
signal.Stop(c)
return output, err
}

View File

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

View File

@ -6,7 +6,9 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"net/url"
"os"
"os/user"
"path/filepath"
@ -16,6 +18,7 @@ import (
"strings"
"time"
"unicode"
"unicode/utf8"
"github.com/blang/semver"
runewidth "github.com/mattn/go-runewidth"
@ -40,8 +43,46 @@ var (
// Stdout is a buffer that is written to stdout when micro closes
Stdout *bytes.Buffer
// Sigterm is a channel where micro exits when written
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() {
var err error
SemVersion, err = semver.Make(Version)
@ -217,10 +258,56 @@ func FSize(f *os.File) int64 {
return fi.Size()
}
// IsWordChar returns whether or not the string is a 'word character'
// Word characters are defined as numbers, letters, or '_'
// IsWordChar returns whether or not a rune is a 'word character'
// Word characters are defined as numbers, letters or sub-word delimiters
func IsWordChar(r rune) bool {
return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_'
return IsAlphanumeric(r) || IsSubwordDelimiter(r)
}
// IsNonWordChar returns whether or not a rune is not a 'word character'
// Non word characters are defined as all characters not being numbers, letters or sub-word delimiters
// See IsWordChar()
func IsNonWordChar(r rune) bool {
return !IsWordChar(r)
}
// 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.
// For now the only sub-word delimiter character is '_'.
func IsSubwordDelimiter(r rune) bool {
return r == '_'
}
// IsAlphanumeric returns whether or not a rune is an 'alphanumeric character'
// Alphanumeric characters are defined as numbers or letters
func IsAlphanumeric(r rune) bool {
return unicode.IsLetter(r) || unicode.IsNumber(r)
}
// IsUpperAlphanumeric returns whether or not a rune is an 'upper alphanumeric character'
// Upper alphanumeric characters are defined as numbers or upper-case letters
func IsUpperAlphanumeric(r rune) bool {
return IsUpperLetter(r) || unicode.IsNumber(r)
}
// IsLowerAlphanumeric returns whether or not a rune is a 'lower alphanumeric character'
// Lower alphanumeric characters are defined as numbers or lower-case letters
func IsLowerAlphanumeric(r rune) bool {
return IsLowerLetter(r) || unicode.IsNumber(r)
}
// IsUpperLetter returns whether or not a rune is an 'upper letter character'
// Upper letter characters are defined as upper-case letters
func IsUpperLetter(r rune) bool {
// unicode.IsUpper() returns true for letters only
return unicode.IsUpper(r)
}
// IsLowerLetter returns whether or not a rune is a 'lower letter character'
// Lower letter characters are defined as lower-case letters
func IsLowerLetter(r rune) bool {
// unicode.IsLower() returns true for letters only
return unicode.IsLower(r)
}
// Spaces returns a string with n spaces
@ -271,6 +358,28 @@ func RunePos(b []byte, i int) int {
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
func MakeRelative(path, base string) (string, error) {
if len(path) > 0 {
@ -315,7 +424,7 @@ func ReplaceHome(path string) (string, error) {
// This is used for opening files like util.go:10:5 to specify a line and column
// Special cases like Windows Absolute path (C:\myfile.txt:10:5) are handled correctly.
func GetPathAndCursorPosition(path string) (string, []string) {
re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?`)
re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?$`)
match := re.FindStringSubmatch(path)
// no lines/columns were specified in the path, return just the path with no cursor location
if len(match) == 0 {
@ -337,8 +446,17 @@ func GetModTime(path string) (time.Time, error) {
return info.ModTime(), nil
}
// EscapePath replaces every path separator in a given path with a %
func EscapePath(path string) string {
func AppendBackupSuffix(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)
if runtime.GOOS == "windows" {
// ':' is not valid in a path name on Windows but is ok on Unix
@ -347,6 +465,24 @@ func EscapePath(path string) string {
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
func GetLeadingWhitespace(b []byte) []byte {
ws := []byte{}
@ -363,6 +499,28 @@ func GetLeadingWhitespace(b []byte) []byte {
return ws
}
// GetTrailingWhitespace returns the trailing whitespace of the given byte array
func GetTrailingWhitespace(b []byte) []byte {
ws := []byte{}
for len(b) > 0 {
r, size := utf8.DecodeLastRune(b)
if IsWhitespace(r) {
ws = append([]byte(string(r)), ws...)
} else {
break
}
b = b[:len(b)-size]
}
return ws
}
// HasTrailingWhitespace returns true if the given byte array ends with a whitespace
func HasTrailingWhitespace(b []byte) bool {
r, _ := utf8.DecodeLastRune(b)
return IsWhitespace(r)
}
// IntOpt turns a float64 setting to an int
func IntOpt(opt interface{}) int {
return int(opt.(float64))
@ -422,19 +580,9 @@ func Clamp(val, min, max int) int {
return val
}
// IsNonAlphaNumeric returns if the rune is not a number of letter or underscore.
func IsNonAlphaNumeric(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c) && c != '_'
}
// IsAutocomplete returns whether a character should begin an autocompletion.
func IsAutocomplete(c rune) bool {
return c == '.' || !IsNonAlphaNumeric(c)
}
// ParseSpecial replaces escaped ts with '\t'.
func ParseSpecial(s string) string {
return strings.ReplaceAll(s, "\\t", "\t")
return c == '.' || IsWordChar(c)
}
// String converts a byte array to a string (for lua plugins)
@ -507,3 +655,77 @@ func HttpRequest(method string, url string, headers []string) (resp *http.Respon
}
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

@ -185,6 +185,10 @@ func (n *Node) hResizeSplit(i int, size int) bool {
// ResizeSplit resizes a certain split to a given size
func (n *Node) ResizeSplit(size int) bool {
// TODO: `size < 0` does not work for some reason
if size <= 0 {
return false
}
if len(n.parent.children) <= 1 {
// cannot resize a lone node
return false
@ -201,7 +205,7 @@ func (n *Node) ResizeSplit(size int) bool {
return n.parent.hResizeSplit(ind, size)
}
// Resize sets this node's size and resizes all children accordlingly
// Resize sets this node's size and resizes all children accordingly
func (n *Node) Resize(w, h int) {
n.W, n.H = w, h
@ -435,11 +439,12 @@ func (n *Node) VSplit(right bool) uint64 {
}
// 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:])
n.children[len(n.children)-1] = nil
n.children = n.children[:len(n.children)-1]
h := n.Kind == STVert
nonrs, numr := n.getResizeInfo(h)
if numr == 0 {
// This means that this was the last child
@ -466,18 +471,62 @@ func (n *Node) Unsplit() bool {
ind = i
}
}
if n.parent.Kind == STVert {
n.parent.unsplit(ind, true)
} else {
n.parent.unsplit(ind, false)
}
n.parent.unsplit(ind)
if n.parent.IsLeaf() {
return n.parent.Unsplit()
}
n.parent.flatten()
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)
func (n *Node) String() string {
var strf func(n *Node, ident int) string

View File

@ -1,18 +0,0 @@
package highlight
import "regexp"
// MatchFiletype will use the list of syntax definitions provided and the filename and first line of the file
// to determine the filetype of the file
// It will return the corresponding syntax definition for the filetype
func MatchFiletype(ftdetect [2]*regexp.Regexp, filename string, firstLine []byte) bool {
if ftdetect[0] != nil && ftdetect[0].MatchString(filename) {
return true
}
if ftdetect[1] != nil {
return ftdetect[1].Match(firstLine)
}
return false
}

View File

@ -51,25 +51,9 @@ func runePos(p int, str []byte) int {
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
type State *region
// EmptyDef is an empty definition.
var EmptyDef = Def{nil, &rules{}}
// LineStates is an interface for a buffer-like object which can also store the states and matches for every line
type LineStates interface {
LineBytes(n int) []byte
@ -77,6 +61,8 @@ type LineStates interface {
State(lineN int) State
SetState(lineN int, s State)
SetMatch(lineN int, m LineMatch)
Lock()
Unlock()
}
// A Highlighter contains the information needed to highlight a string
@ -176,7 +162,7 @@ func (h *Highlighter) highlightRegion(highlights LineMatch, start int, canMatchE
if curRegion.group == curRegion.limitGroup || p.group == curRegion.limitGroup {
matches := findAllIndex(p.regex, line)
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++ {
fullHighlights[i] = p.group
}
@ -303,7 +289,13 @@ func (h *Highlighter) HighlightString(input string) []LineMatch {
// HighlightStates correctly sets all states for the buffer
func (h *Highlighter) HighlightStates(input LineStates) {
for i := 0; i < input.LinesNum(); i++ {
for i := 0; ; i++ {
input.Lock()
if i >= input.LinesNum() {
input.Unlock()
break
}
line := input.LineBytes(i)
// highlights := make(LineMatch)
@ -316,6 +308,7 @@ func (h *Highlighter) HighlightStates(input LineStates) {
curState := h.lastRegion
input.SetState(i, curState)
input.Unlock()
}
}
@ -324,7 +317,9 @@ func (h *Highlighter) HighlightStates(input LineStates) {
// This assumes that all the states are set correctly
func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int) {
for i := startline; i <= endline; i++ {
input.Lock()
if i >= input.LinesNum() {
input.Unlock()
break
}
@ -339,6 +334,7 @@ func (h *Highlighter) HighlightMatches(input LineStates, startline, endline int)
}
input.SetMatch(i, match)
input.Unlock()
}
}
@ -350,9 +346,19 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
h.lastRegion = nil
if startline > 0 {
h.lastRegion = input.State(startline - 1)
input.Lock()
if startline-1 < input.LinesNum() {
h.lastRegion = input.State(startline - 1)
}
input.Unlock()
}
for i := startline; i < input.LinesNum(); i++ {
for i := startline; ; i++ {
input.Lock()
if i >= input.LinesNum() {
input.Unlock()
break
}
line := input.LineBytes(i)
// highlights := make(LineMatch)
@ -366,6 +372,7 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
lastState := input.State(i)
input.SetState(i, curState)
input.Unlock()
if curState == lastState {
return i
@ -377,6 +384,9 @@ func (h *Highlighter) ReHighlightStates(input LineStates, startline int) int {
// ReHighlightLine will rehighlight the state and match for a single line
func (h *Highlighter) ReHighlightLine(input LineStates, lineN int) {
input.Lock()
defer input.Unlock()
line := input.LineBytes(lineN)
highlights := make(LineMatch)

View File

@ -33,27 +33,28 @@ func (g Group) String() string {
// Then it has the rules which define how to highlight the file
type Def struct {
*Header
rules *rules
}
type Header struct {
FileType string
FtDetect [2]*regexp.Regexp
FileType string
FileNameRegex *regexp.Regexp
HeaderRegex *regexp.Regexp
SignatureRegex *regexp.Regexp
}
type HeaderYaml struct {
FileType string `yaml:"filetype"`
Detect struct {
FNameRgx string `yaml:"filename"`
HeaderRgx string `yaml:"header"`
FNameRegexStr string `yaml:"filename"`
HeaderRegexStr string `yaml:"header"`
SignatureRegexStr string `yaml:"signature"`
} `yaml:"detect"`
}
type File struct {
FileType string
yamlSrc map[interface{}]interface{}
yamlSrc map[interface{}]interface{}
}
// A Pattern is one simple syntax rule
@ -97,20 +98,24 @@ func init() {
// A yaml file might take ~400us to parse while a header file only takes ~20us
func MakeHeader(data []byte) (*Header, error) {
lines := bytes.Split(data, []byte{'\n'})
if len(lines) < 3 {
if len(lines) < 4 {
return nil, errors.New("Header file has incorrect format")
}
header := new(Header)
var err error
header.FileType = string(lines[0])
fnameRgx := string(lines[1])
headerRgx := string(lines[2])
fnameRegexStr := string(lines[1])
headerRegexStr := string(lines[2])
signatureRegexStr := string(lines[3])
if fnameRgx != "" {
header.FtDetect[0], err = regexp.Compile(fnameRgx)
if fnameRegexStr != "" {
header.FileNameRegex, err = regexp.Compile(fnameRegexStr)
}
if err == nil && headerRgx != "" {
header.FtDetect[1], err = regexp.Compile(headerRgx)
if err == nil && headerRegexStr != "" {
header.HeaderRegex, err = regexp.Compile(headerRegexStr)
}
if err == nil && signatureRegexStr != "" {
header.SignatureRegex, err = regexp.Compile(signatureRegexStr)
}
if err != nil {
@ -132,11 +137,14 @@ func MakeHeaderYaml(data []byte) (*Header, error) {
header := new(Header)
header.FileType = hdrYaml.FileType
if hdrYaml.Detect.FNameRgx != "" {
header.FtDetect[0], err = regexp.Compile(hdrYaml.Detect.FNameRgx)
if hdrYaml.Detect.FNameRegexStr != "" {
header.FileNameRegex, err = regexp.Compile(hdrYaml.Detect.FNameRegexStr)
}
if err == nil && hdrYaml.Detect.HeaderRgx != "" {
header.FtDetect[1], err = regexp.Compile(hdrYaml.Detect.HeaderRgx)
if err == nil && hdrYaml.Detect.HeaderRegexStr != "" {
header.HeaderRegex, err = regexp.Compile(hdrYaml.Detect.HeaderRegexStr)
}
if err == nil && hdrYaml.Detect.SignatureRegexStr != "" {
header.SignatureRegex, err = regexp.Compile(hdrYaml.Detect.SignatureRegexStr)
}
if err != nil {
@ -146,6 +154,37 @@ func MakeHeaderYaml(data []byte) (*Header, error) {
return header, nil
}
// MatchFileName will check the given file name with the stored regex
func (header *Header) MatchFileName(filename string) bool {
if header.FileNameRegex != nil {
return header.FileNameRegex.MatchString(filename)
}
return false
}
func (header *Header) MatchFileHeader(firstLine []byte) bool {
if header.HeaderRegex != nil {
return header.HeaderRegex.Match(firstLine)
}
return false
}
// HasFileSignature checks the presence of a stored signature
func (header *Header) HasFileSignature() bool {
return header.SignatureRegex != nil
}
// MatchFileSignature will check the given line with the stored regex
func (header *Header) MatchFileSignature(line []byte) bool {
if header.SignatureRegex != nil {
return header.SignatureRegex.Match(line)
}
return false
}
func ParseFile(input []byte) (f *File, err error) {
// This is just so if we have an error, we can exit cleanly and return the parse error to the user
defer func() {
@ -170,11 +209,19 @@ func ParseFile(input []byte) (f *File, err error) {
if k == "filetype" {
filetype := v.(string)
if filetype == "" {
return nil, errors.New("empty filetype")
}
f.FileType = filetype
break
}
}
if f.FileType == "" {
return nil, errors.New("missing filetype")
}
return f, err
}
@ -191,12 +238,12 @@ func ParseDef(f *File, header *Header) (s *Def, err error) {
}
}()
rules := f.yamlSrc
src := f.yamlSrc
s = new(Def)
s.Header = header
for k, v := range rules {
for k, v := range src {
if k == "rules" {
inputRules := v.([]interface{})
@ -209,6 +256,11 @@ func ParseDef(f *File, header *Header) (s *Def, err error) {
}
}
if s.rules == nil {
// allow empty rules
s.rules = &rules{}
}
return s, err
}
@ -303,6 +355,10 @@ func parseRules(input []interface{}, curRegion *region) (ru *rules, err error) {
switch object := val.(type) {
case string:
if object == "" {
return nil, fmt.Errorf("Empty rule %s", k)
}
if k == "include" {
ru.includes = append(ru.includes, object)
} else {
@ -356,30 +412,56 @@ func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegio
r.group = groupNum
r.parent = prevRegion
r.start, err = regexp.Compile(regionInfo["start"].(string))
// start is mandatory
if start, ok := regionInfo["start"]; ok {
start := start.(string)
if start == "" {
return nil, fmt.Errorf("Empty start in %s", group)
}
if err != nil {
return nil, err
r.start, err = regexp.Compile(start)
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("Missing start in %s", group)
}
r.end, err = regexp.Compile(regionInfo["end"].(string))
// end is mandatory
if end, ok := regionInfo["end"]; ok {
end := end.(string)
if end == "" {
return nil, fmt.Errorf("Empty end in %s", group)
}
if err != nil {
return nil, err
r.end, err = regexp.Compile(end)
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("Missing end in %s", group)
}
// skip is optional
if _, ok := regionInfo["skip"]; ok {
r.skip, err = regexp.Compile(regionInfo["skip"].(string))
if skip, ok := regionInfo["skip"]; ok {
skip := skip.(string)
if skip == "" {
return nil, fmt.Errorf("Empty skip in %s", group)
}
r.skip, err = regexp.Compile(skip)
if err != nil {
return nil, err
}
}
// limit-color is optional
if _, ok := regionInfo["limit-group"]; ok {
groupStr := regionInfo["limit-group"].(string)
if groupStr, ok := regionInfo["limit-group"]; ok {
groupStr := groupStr.(string)
if groupStr == "" {
return nil, fmt.Errorf("Empty limit-group in %s", group)
}
if _, ok := Groups[groupStr]; !ok {
numGroups++
Groups[groupStr] = numGroups
@ -394,10 +476,17 @@ func parseRegion(group string, regionInfo map[interface{}]interface{}, prevRegio
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 {
return nil, err
if r.rules == nil {
// allow empty rules
r.rules = &rules{}
}
return r, nil

View File

@ -28,3 +28,6 @@ color-link color-column "#2D2F31"
#No extended types (bool in C, etc.)
#color-link type.extended "default"
#Plain brackets
color-link match-brace "#1D1F21,#62B1FE"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -26,3 +26,6 @@ color-link color-column "254"
#No extended types (bool in C, &c.) and plain brackets
color-link type.extended "241,231"
color-link symbol.brackets "241,231"
color-link match-brace "231,28"
color-link tab-error "210"
color-link trailingws "210"

View File

@ -42,3 +42,6 @@ color-link gutter-warning "red"
color-link color-column "cyan"
color-link underlined.url "underline blue, white"
color-link divider "blue"
color-link match-brace "black,cyan"
color-link tab-error "brightred"
color-link trailingws "brightred"

View File

@ -38,3 +38,6 @@ color-link color-column "#f26522"
color-link constant.bool "bold #55ffff"
color-link constant.bool.true "bold #85ff85"
color-link constant.bool.false "bold #ff8585"
color-link match-brace "#1e2124,#55ffff"
color-link tab-error "#d75f5f"
color-link trailingws "#d75f5f"

View File

@ -29,3 +29,6 @@ color-link color-column "#2C2C2C"
color-link type.extended "default"
#color-link symbol.brackets "default"
color-link symbol.tag "#AE81FF,#242424"
color-link match-brace "#242424,#7A9EC2"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -1,31 +1 @@
color-link default "#F8F8F2,#282828"
color-link comment "#75715E,#282828"
color-link identifier "#66D9EF,#282828"
color-link constant "#AE81FF,#282828"
color-link constant.string "#E6DB74,#282828"
color-link constant.string.char "#BDE6AD,#282828"
color-link statement "#F92672,#282828"
color-link symbol.operator "#F92671,#282828"
color-link preproc "#CB4B16,#282828"
color-link type "#66D9EF,#282828"
color-link special "#A6E22E,#282828"
color-link underlined "#D33682,#282828"
color-link error "bold #CB4B16,#282828"
color-link todo "bold #D33682,#282828"
color-link hlsearch "#282828,#E6DB74"
color-link statusline "#282828,#F8F8F2"
color-link tabbar "#282828,#F8F8F2"
color-link indent-char "#505050,#282828"
color-link line-number "#AAAAAA,#323232"
color-link current-line-number "#AAAAAA,#282828"
color-link diff-added "#00AF00"
color-link diff-modified "#FFAF00"
color-link diff-deleted "#D70000"
color-link gutter-error "#CB4B16,#282828"
color-link gutter-warning "#E6DB74,#282828"
color-link cursor-line "#323232"
color-link color-column "#323232"
#No extended types; Plain brackets.
color-link type.extended "default"
#color-link symbol.brackets "default"
color-link symbol.tag "#AE81FF,#282828"
include "monokai"

View File

@ -43,3 +43,7 @@ color-link cursor-line "#44475A,#F8F8F2"
color-link color-column "#44475A"
color-link type.extended "default"
color-link match-brace "#282A36,#FF79C6"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -33,3 +33,6 @@ color-link type "bold #3cc83c,#001e28"
color-link type.keyword "bold #5aaae6,#001e28"
color-link type.extended "#ffffff,#001e28"
color-link underlined "#608b4e,#001e28"
color-link match-brace "#001e28,#5aaae6"
color-link tab-error "#d75f5f"
color-link trailingws "#d75f5f"

View File

@ -33,3 +33,6 @@ color-link type "bold #004080,#f0f0f0"
color-link type.keyword "bold #780050,#f0f0f0"
color-link type.extended "#000000,#f0f0f0"
color-link underlined "#3f7f5f,#f0f0f0"
color-link match-brace "#f0f0f0,#780050"
color-link tab-error "#ff8787"
color-link trailingws "#ff8787"

View File

@ -33,3 +33,6 @@ color-link type "bold #3cc83c,#2d0023"
color-link type.keyword "bold #5aaae6,#2d0023"
color-link type.extended "#ffffff,#2d0023"
color-link underlined "#886484,#2d0023"
color-link match-brace "#2d0023,#5aaae6"
color-link tab-error "#d75f5f"
color-link trailingws "#d75f5f"

View File

@ -24,3 +24,6 @@ color-link diff-modified "yellow"
color-link diff-deleted "red"
color-link gutter-error ",red"
color-link gutter-warning "red"
color-link match-brace "black,cyan"
color-link tab-error "brightred"
color-link trailingws "brightred"

View File

@ -24,3 +24,6 @@ color-link gutter-warning "#EDB443,#11151C"
color-link cursor-line "#091F2E"
color-link color-column "#11151C"
color-link symbol "#99D1CE,#0C1014"
color-link match-brace "#0C1014,#D26937"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -24,3 +24,6 @@ color-link cursor-line "#3c3836"
color-link color-column "#79740e"
color-link statusline "#ebdbb2,#665c54"
color-link tabbar "#ebdbb2,#665c54"
color-link match-brace "#282828,#d3869b"
color-link tab-error "#d75f5f"
color-link trailingws "#d75f5f"

View File

@ -21,3 +21,6 @@ color-link cursor-line "237"
color-link color-column "237"
color-link statusline "223,237"
color-link tabbar "223,237"
color-link match-brace "235,72"
color-link tab-error "167"
color-link trailingws "167"

View File

@ -20,6 +20,7 @@ color-link identifier.macro "#FFCB6B,#263238"
color-link indent-char "#505050,#263238"
color-link line-number "#656866,#283942"
color-link preproc "#C792EA,#263238"
color-link scrollbar "#80DEEA,#283942"
color-link special "#C792EA,#263238"
color-link statement "#C792EA,#263238"
color-link statusline "#80DEEA,#3b4d56"
@ -30,3 +31,6 @@ color-link tabbar "#80DEEA,#3b4d56"
color-link todo "bold #C792EA,#263238"
color-link type "#FFCB6B,#263238"
color-link underlined "underline #EEFFFF,#263238"
color-link match-brace "#263238,#C792EA"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -23,3 +23,6 @@ color-link gutter-error "#CB4B16"
color-link gutter-warning "#E6DB74"
color-link cursor-line "#323232"
color-link color-column "#323232"
color-link match-brace "#1D0000,#AE81FF"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -29,3 +29,6 @@ color-link color-column "#323232"
color-link type.extended "default"
#color-link symbol.brackets "default"
color-link symbol.tag "#AE81FF,#282828"
color-link match-brace "#282828,#AE81FF"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -34,3 +34,6 @@ color-link todo "#8B98AB"
color-link type "#66D9EF"
color-link type.keyword "#C678DD"
color-link underlined "#8996A8"
color-link match-brace "#21252C,#C678DD"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -28,6 +28,10 @@ color-link tabbar "bold #b1b1b1,#232323"
color-link cursor-line "#353535"
color-link color-column "#353535"
color-link space "underline #e6e1dc,#2b2b2b"
color-link tab-error "#d75f5f"
color-link trailingws "#d75f5f"
#the Python syntax definition are wrong. This is not how you should do decorators!
color-link brightgreen "#edb753,#2b2b2b"
color-link match-brace "#2b2b2b,#a5c261"

View File

@ -10,6 +10,7 @@ color-link ignore "default"
color-link error ",brightred"
color-link todo ",brightyellow"
color-link hlsearch "black,yellow"
color-link statusline "black,white"
color-link indent-char "black"
color-link line-number "yellow"
color-link current-line-number "red"
@ -27,3 +28,6 @@ color-link type.extended "default"
color-link symbol.brackets "default"
#Color shebangs the comment color
color-link preproc.shebang "comment"
color-link match-brace ",magenta"
color-link tab-error "brightred"
color-link trailingws "brightred"

View File

@ -26,3 +26,6 @@ color-link cursor-line "#003541"
color-link color-column "#003541"
color-link type.extended "#839496,#002833"
color-link symbol.brackets "#839496,#002833"
color-link match-brace "#002833,#268BD2"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -25,3 +25,6 @@ color-link cursor-line "black"
color-link color-column "black"
color-link type.extended "default"
color-link symbol.brackets "default"
color-link match-brace ",blue"
color-link tab-error "brightred"
color-link trailingws "brightred"

View File

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

View File

@ -35,3 +35,6 @@ color-link todo "#8B98AB"
color-link type "#F9EE98"
color-link type.keyword "#CDA869"
color-link underlined "#8996A8"
color-link match-brace "#141414,#E0C589"
color-link tab-error "#D75F5F"
color-link trailingws "#D75F5F"

View File

@ -25,3 +25,6 @@ color-link gutter-warning "174,237"
color-link cursor-line "238"
color-link color-column "238"
color-link current-line-number "188,237"
color-link match-brace "237,223"
color-link tab-error "167"
color-link trailingws "167"

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