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).
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.
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`.
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.
This behavior is then aligned to the actual documentation of `fastdirty`.
Additionally set the origHash to zero in case the buffer was already modified.
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.
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
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
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.
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.
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.
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.
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.
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.
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>
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.
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.
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.
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