micro: Handle +/regex search from args (#3767)

This is a feature found in vim and commonly used
by Linux kernel test robots to give context about
warnings and/or failures.

e.g. vim +/imem_size +623 drivers/net/ipa/ipa_mem.c

The order in which the commands appear in the args
determines in which order the "goto line:column"
and search will be executed.
This commit is contained in:
Luca Stefani
2025-09-05 20:53:37 +02:00
committed by GitHub
parent bbea2a3f28
commit e9f241af71
3 changed files with 89 additions and 30 deletions

View File

@@ -1,11 +1,12 @@
.TH micro 1 "2025-08-16" .TH micro 1 "2025-09-03"
.SH NAME .SH NAME
micro \- A modern and intuitive terminal-based text editor micro \- A modern and intuitive terminal-based text editor
.SH SYNOPSIS .SH SYNOPSIS
.B micro .B micro
.RI [ OPTION ]...\& .RI [ OPTION ]...\&
.RI [ FILE ]...\& .RI [ FILE ]...\&
.RI [+ LINE [: COL ]] .RI [+ LINE [: COL ]]\&
.RI [+/ REGEX ]
.br .br
.B micro .B micro
.RI [ OPTION ]...\& .RI [ OPTION ]...\&
@@ -40,6 +41,11 @@ Specify a custom location for the configuration directory
Specify a line and column to start the cursor at when opening a buffer Specify a line and column to start the cursor at when opening a buffer
.RE .RE
.PP .PP
.RI +/ REGEX
.RS 4
Specify a regex to search for when opening a buffer
.RE
.PP
.B \-options .B \-options
.RS 4 .RS 4
Show all options help and exit Show all options help and exit

View File

@@ -48,7 +48,7 @@ var (
func InitFlags() { func InitFlags() {
// Note: keep this in sync with the man page in assets/packaging/micro.1 // Note: keep this in sync with the man page in assets/packaging/micro.1
flag.Usage = func() { flag.Usage = func() {
fmt.Println("Usage: micro [OPTION]... [FILE]... [+LINE[:COL]]") fmt.Println("Usage: micro [OPTION]... [FILE]... [+LINE[:COL]] [+/REGEX]")
fmt.Println(" micro [OPTION]... [FILE[:LINE[:COL]]]... (only if the `parsecursor` option is enabled)") fmt.Println(" micro [OPTION]... [FILE[:LINE[:COL]]]... (only if the `parsecursor` option is enabled)")
fmt.Println("-clean") fmt.Println("-clean")
fmt.Println(" \tClean the configuration directory and exit") fmt.Println(" \tClean the configuration directory and exit")
@@ -57,6 +57,8 @@ func InitFlags() {
fmt.Println("FILE:LINE[:COL] (only if the `parsecursor` option is enabled)") fmt.Println("FILE:LINE[:COL] (only if the `parsecursor` option is enabled)")
fmt.Println("FILE +LINE[:COL]") fmt.Println("FILE +LINE[:COL]")
fmt.Println(" \tSpecify a line and column to start the cursor at when opening a buffer") fmt.Println(" \tSpecify a line and column to start the cursor at when opening a buffer")
fmt.Println("+/REGEX")
fmt.Println(" \tSpecify a regex to search for when opening a buffer")
fmt.Println("-options") fmt.Println("-options")
fmt.Println(" \tShow all options help and exit") fmt.Println(" \tShow all options help and exit")
fmt.Println("-debug") fmt.Println("-debug")
@@ -167,39 +169,60 @@ func LoadInput(args []string) []*buffer.Buffer {
} }
files := make([]string, 0, len(args)) files := make([]string, 0, len(args))
flagStartPos := buffer.Loc{-1, -1} flagStartPos := buffer.Loc{-1, -1}
flagr := regexp.MustCompile(`^\+(\d+)(?::(\d+))?$`) posFlagr := regexp.MustCompile(`^\+(\d+)(?::(\d+))?$`)
for _, a := range args { posIndex := -1
match := flagr.FindStringSubmatch(a)
if len(match) == 3 && match[2] != "" { searchText := ""
line, err := strconv.Atoi(match[1]) searchFlagr := regexp.MustCompile(`^\+\/(.+)$`)
searchIndex := -1
for i, a := range args {
posMatch := posFlagr.FindStringSubmatch(a)
if len(posMatch) == 3 && posMatch[2] != "" {
line, err := strconv.Atoi(posMatch[1])
if err != nil { if err != nil {
screen.TermMessage(err) screen.TermMessage(err)
continue continue
} }
col, err := strconv.Atoi(match[2]) col, err := strconv.Atoi(posMatch[2])
if err != nil { if err != nil {
screen.TermMessage(err) screen.TermMessage(err)
continue continue
} }
flagStartPos = buffer.Loc{col - 1, line - 1} flagStartPos = buffer.Loc{col - 1, line - 1}
} else if len(match) == 3 && match[2] == "" { posIndex = i
line, err := strconv.Atoi(match[1]) } else if len(posMatch) == 3 && posMatch[2] == "" {
line, err := strconv.Atoi(posMatch[1])
if err != nil { if err != nil {
screen.TermMessage(err) screen.TermMessage(err)
continue continue
} }
flagStartPos = buffer.Loc{0, line - 1} flagStartPos = buffer.Loc{0, line - 1}
posIndex = i
} else { } else {
files = append(files, a) searchMatch := searchFlagr.FindStringSubmatch(a)
if len(searchMatch) == 2 {
searchText = searchMatch[1]
searchIndex = i
} else {
files = append(files, a)
}
} }
} }
command := buffer.Command{
StartCursor: flagStartPos,
SearchRegex: searchText,
SearchAfterStart: searchIndex > posIndex,
}
if len(files) > 0 { if len(files) > 0 {
// Option 1 // Option 1
// We go through each file and load it // We go through each file and load it
for i := 0; i < len(files); i++ { for i := 0; i < len(files); i++ {
buf, err := buffer.NewBufferFromFileAtLoc(files[i], btype, flagStartPos) buf, err := buffer.NewBufferFromFileWithCommand(files[i], btype, command)
if err != nil { if err != nil {
screen.TermMessage(err) screen.TermMessage(err)
continue continue
@@ -216,10 +239,10 @@ func LoadInput(args []string) []*buffer.Buffer {
screen.TermMessage("Error reading from stdin: ", err) screen.TermMessage("Error reading from stdin: ", err)
input = []byte{} input = []byte{}
} }
buffers = append(buffers, buffer.NewBufferFromStringAtLoc(string(input), filename, btype, flagStartPos)) buffers = append(buffers, buffer.NewBufferFromStringWithCommand(string(input), filename, btype, command))
} else { } else {
// Option 3, just open an empty buffer // Option 3, just open an empty buffer
buffers = append(buffers, buffer.NewBufferFromStringAtLoc(string(input), filename, btype, flagStartPos)) buffers = append(buffers, buffer.NewBufferFromStringWithCommand(string(input), filename, btype, command))
} }
return buffers return buffers

View File

@@ -215,6 +215,18 @@ const (
type DiffStatus byte type DiffStatus byte
type Command struct {
StartCursor Loc
SearchRegex string
SearchAfterStart bool
}
var emptyCommand = Command{
StartCursor: Loc{-1, -1},
SearchRegex: "",
SearchAfterStart: false,
}
// Buffer stores the main information about a currently open file including // Buffer stores the main information about a currently open file including
// the actual text (in a LineArray), the undo/redo stack (in an EventHandler) // the actual text (in a LineArray), the undo/redo stack (in an EventHandler)
// all the cursors, the syntax highlighting info, the settings for the buffer // all the cursors, the syntax highlighting info, the settings for the buffer
@@ -256,19 +268,19 @@ type Buffer struct {
OverwriteMode bool OverwriteMode bool
} }
// NewBufferFromFileAtLoc opens a new buffer with a given cursor location // NewBufferFromFileWithCommand opens a new buffer with a given command
// If cursorLoc is {-1, -1} the location does not overwrite what the cursor location // If cmd.StartCursor is {-1, -1} the location does not overwrite what the cursor location
// would otherwise be (start of file, or saved cursor position if `savecursor` is // would otherwise be (start of file, or saved cursor position if `savecursor` is
// enabled) // enabled)
func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer, error) { func NewBufferFromFileWithCommand(path string, btype BufType, cmd Command) (*Buffer, error) {
var err error var err error
filename := path filename := path
if config.GetGlobalOption("parsecursor").(bool) && cursorLoc.X == -1 && cursorLoc.Y == -1 { if config.GetGlobalOption("parsecursor").(bool) && cmd.StartCursor.X == -1 && cmd.StartCursor.Y == -1 {
var cursorPos []string var cursorPos []string
filename, cursorPos = util.GetPathAndCursorPosition(filename) filename, cursorPos = util.GetPathAndCursorPosition(filename)
cursorLoc, err = ParseCursorLocation(cursorPos) cmd.StartCursor, err = ParseCursorLocation(cursorPos)
if err != nil { if err != nil {
cursorLoc = Loc{-1, -1} cmd.StartCursor = Loc{-1, -1}
} }
} }
@@ -304,7 +316,7 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} else { } else {
buf = NewBuffer(file, util.FSize(file), filename, cursorLoc, btype) buf = NewBuffer(file, util.FSize(file), filename, btype, cmd)
if buf == nil { if buf == nil {
return nil, errors.New("could not open file") return nil, errors.New("could not open file")
} }
@@ -323,17 +335,18 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
// It will return an empty buffer if the path does not exist // It will return an empty buffer if the path does not exist
// and an error if the file is a directory // and an error if the file is a directory
func NewBufferFromFile(path string, btype BufType) (*Buffer, error) { func NewBufferFromFile(path string, btype BufType) (*Buffer, error) {
return NewBufferFromFileAtLoc(path, btype, Loc{-1, -1}) return NewBufferFromFileWithCommand(path, btype, emptyCommand)
} }
// NewBufferFromStringAtLoc creates a new buffer containing the given string with a cursor loc // NewBufferFromStringWithCommand creates a new buffer containing the given string
func NewBufferFromStringAtLoc(text, path string, btype BufType, cursorLoc Loc) *Buffer { // with a cursor loc and a search text
return NewBuffer(strings.NewReader(text), int64(len(text)), path, cursorLoc, btype) func NewBufferFromStringWithCommand(text, path string, btype BufType, cmd Command) *Buffer {
return NewBuffer(strings.NewReader(text), int64(len(text)), path, btype, cmd)
} }
// NewBufferFromString creates a new buffer containing the given string // NewBufferFromString creates a new buffer containing the given string
func NewBufferFromString(text, path string, btype BufType) *Buffer { func NewBufferFromString(text, path string, btype BufType) *Buffer {
return NewBuffer(strings.NewReader(text), int64(len(text)), path, Loc{-1, -1}, btype) return NewBuffer(strings.NewReader(text), int64(len(text)), path, btype, emptyCommand)
} }
// NewBuffer creates a new buffer from a given reader with a given path // NewBuffer creates a new buffer from a given reader with a given path
@@ -341,7 +354,7 @@ func NewBufferFromString(text, path string, btype BufType) *Buffer {
// a new buffer // a new buffer
// Places the cursor at startcursor. If startcursor is -1, -1 places the // Places the cursor at startcursor. If startcursor is -1, -1 places the
// cursor at an autodetected location (based on savecursor or :LINE:COL) // cursor at an autodetected location (based on savecursor or :LINE:COL)
func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufType) *Buffer { func NewBuffer(r io.Reader, size int64, path string, btype BufType, cmd Command) *Buffer {
absPath, err := filepath.Abs(path) absPath, err := filepath.Abs(path)
if err != nil { if err != nil {
absPath = path absPath = path
@@ -436,8 +449,8 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm) os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm)
} }
if startcursor.X != -1 && startcursor.Y != -1 { if cmd.StartCursor.X != -1 && cmd.StartCursor.Y != -1 {
b.StartCursor = startcursor b.StartCursor = cmd.StartCursor
} else if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) { } else if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
err := b.Unserialize() err := b.Unserialize()
if err != nil { if err != nil {
@@ -448,6 +461,23 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
b.AddCursor(NewCursor(b, b.StartCursor)) b.AddCursor(NewCursor(b, b.StartCursor))
b.GetActiveCursor().Relocate() b.GetActiveCursor().Relocate()
if cmd.SearchRegex != "" {
match, found, _ := b.FindNext(cmd.SearchRegex, b.Start(), b.End(), b.StartCursor, true, true)
if found {
if cmd.SearchAfterStart {
// Search from current cursor and move it accordingly
b.GetActiveCursor().SetSelectionStart(match[0])
b.GetActiveCursor().SetSelectionEnd(match[1])
b.GetActiveCursor().OrigSelection[0] = b.GetActiveCursor().CurSelection[0]
b.GetActiveCursor().OrigSelection[1] = b.GetActiveCursor().CurSelection[1]
b.GetActiveCursor().GotoLoc(match[1])
}
b.LastSearch = cmd.SearchRegex
b.LastSearchRegex = true
b.HighlightSearch = b.Settings["hlsearch"].(bool)
}
}
if !b.Settings["fastdirty"].(bool) && !found { if !b.Settings["fastdirty"].(bool) && !found {
if size > LargeFileThreshold { if size > LargeFileThreshold {
// If the file is larger than LargeFileThreshold fastdirty needs to be on // If the file is larger than LargeFileThreshold fastdirty needs to be on