From e8fbfb1928fa4e43aac3c9e68ffcf6ebd6d5c7b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Wed, 11 Feb 2026 06:52:51 +0100 Subject: [PATCH 1/3] buffer: Resolve symlinks in file paths Otherwise we can't identify if we have the same file open multiple times via different symlinks. The test must be adapted to resolve symlinks in `findBuffer()`. --- cmd/micro/micro_test.go | 4 +++- internal/buffer/buffer.go | 9 +++++---- internal/buffer/save.go | 5 +---- internal/util/util.go | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/cmd/micro/micro_test.go b/cmd/micro/micro_test.go index 09cad432cb..5f1b71d8c8 100644 --- a/cmd/micro/micro_test.go +++ b/cmd/micro/micro_test.go @@ -11,6 +11,7 @@ import ( "github.com/micro-editor/micro/v2/internal/buffer" "github.com/micro-editor/micro/v2/internal/config" "github.com/micro-editor/micro/v2/internal/screen" + "github.com/micro-editor/micro/v2/internal/util" "github.com/micro-editor/tcell/v2" "github.com/stretchr/testify/assert" ) @@ -157,8 +158,9 @@ func openFile(file string) { func findBuffer(file string) *buffer.Buffer { var buf *buffer.Buffer + _, file = util.ResolvePath(file) for _, b := range buffer.OpenBuffers { - if b.Path == file { + if b.AbsPath == file { buf = b } } diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 2735ca467c..1c3246095f 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -355,9 +355,9 @@ func NewBufferFromString(text, path string, btype BufType) *Buffer { // Places the cursor at startcursor. If startcursor is -1, -1 places the // cursor at an autodetected location (based on savecursor or :LINE:COL) func NewBuffer(r io.Reader, size int64, path string, btype BufType, cmd Command) *Buffer { - absPath, err := filepath.Abs(path) - if err != nil { - absPath = path + absPath := path + if btype == BTDefault && path != "" { + path, absPath = util.ResolvePath(path) } b := new(Buffer) @@ -391,6 +391,7 @@ func NewBuffer(r io.Reader, size int64, path string, btype BufType, cmd Command) } config.UpdatePathGlobLocals(b.Settings, absPath) + var err error b.encoding, err = htmlindex.Get(b.Settings["encoding"].(string)) if err != nil { b.encoding = unicode.UTF8 @@ -489,7 +490,7 @@ func NewBuffer(r io.Reader, size int64, path string, btype BufType, cmd Command) } } - err = config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b)) + err := config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b)) if err != nil { screen.TermMessage(err) } diff --git a/internal/buffer/save.go b/internal/buffer/save.go index 44e8f4a3ed..a1d86172b0 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -285,10 +285,7 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error return errors.New("Error: " + filename + " is not a regular file and cannot be saved") } - absFilename, err := filepath.Abs(filename) - if err != nil { - return err - } + filename, absFilename := util.ResolvePath(filename) // Get the leading path to the file | "." is returned if there's no leading path provided if dirname := filepath.Dir(absFilename); dirname != "." { diff --git a/internal/util/util.go b/internal/util/util.go index cad6374349..e10ebc90e8 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -489,6 +489,40 @@ func DetermineEscapePath(dir string, path string) (string, string) { return url, "" } +// ResolvePath provides the absolute file path for the given relative file path +// as well as resolves symlinks in both. It returns the relative path with +// symlinks resolved and the absolute path with symlinks resolved. If it fails +// to get the absolute path or to resolve symlinks, it returns unresolved path +// in place of resolved one. The only exception is the case in which the target +// file doesn't exist. We leave the path handling to EvalSymlinks() and use the +// path causing the error as target path. +func ResolvePath(path string) (string, string) { + resolvedPath, err := filepath.EvalSymlinks(path) + if err == nil { + path = resolvedPath + } else if errors.Is(err, fs.ErrNotExist) { + if e, ok := err.(*fs.PathError); ok { + path = e.Path + } + } + + absPath, err := filepath.Abs(path) + if err != nil { + absPath = path + } + + resolvedPath, err = filepath.EvalSymlinks(absPath) + if err == nil { + absPath = resolvedPath + } else if errors.Is(err, fs.ErrNotExist) { + if e, ok := err.(*fs.PathError); ok { + absPath = e.Path + } + } + + return path, absPath +} + // GetLeadingWhitespace returns the leading whitespace of the given byte array func GetLeadingWhitespace(b []byte) []byte { ws := []byte{} From f3ea70461282277cda688655ef05b0849499fcb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Fri, 6 Feb 2026 00:06:59 +0100 Subject: [PATCH 2/3] buffer: Don't cancel the backup in case the buffer is shared Otherwise it will be removed async, which shouldn't happen in case there is still one buffer open with the same modified file. --- internal/buffer/buffer.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 1c3246095f..ab719ae310 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -528,7 +528,9 @@ func (b *Buffer) Fini() { if !b.Modified() { b.Serialize() } - b.CancelBackup() + if !b.Shared() { + b.CancelBackup() + } if b.Type == BTStdout { fmt.Fprint(util.Stdout, string(b.Bytes())) From ddda4d050d1b58e3ef09f720b4db1ab9d5eef794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Tue, 10 Feb 2026 19:17:43 +0100 Subject: [PATCH 3/3] buffer: Don't `Serialize` in case the buffer is shared Otherwise we unnecessarily serialize the shared buffer every time when closing a bufpane with this buffer, so every such serialize overwrites the previous one, thus only the last serialize (when closing the last instance of the buffer, i.e. when actually closing the file, i.e. when the buffer is not shared anymore) will be used anyway. --- internal/buffer/buffer.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index ab719ae310..6c077d8f90 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -525,10 +525,10 @@ func (b *Buffer) Close() { // Fini should be called when a buffer is closed and performs // some cleanup func (b *Buffer) Fini() { - if !b.Modified() { - b.Serialize() - } if !b.Shared() { + if !b.Modified() { + b.Serialize() + } b.CancelBackup() }