Compare commits

..

22 Commits

Author SHA1 Message Date
Nick
1c88f24ad7 Suspend the whole process group along with micro. (#4060)
Fixes https://github.com/micro-editor/micro/issues/4059
2026-04-02 00:34:46 +02:00
Jöran Karl
25faa84fcb Fix documentation for Loc.LessThan() and other similar functions (#4058)
* Fix documentation for Loc.LessThan() and other similar functions

* Rewrite Loc.MoveLA(n): do not call util.Abs(n) in cycle, cleaner code
2026-03-30 18:13:39 +02:00
Prasad Ramdas Hiwarkhede
4d04ad7290 syntax: add Gleam language support (#4045) 2026-03-28 20:42:12 +01:00
bibaka
4ddf4fe2b1 Rewrite Loc.MoveLA(n): do not call util.Abs(n) in cycle, cleaner code 2026-03-28 02:12:08 +03:00
bibaka
1be75cc697 Fix documentation for Loc.LessThan() and other similar functions 2026-03-28 02:09:00 +03:00
Jöran Karl
d976b3f170 Merge pull request #4044 from JoeKar/fix/crash-glob
Fix crash with file globbing matching micro option names
2026-03-22 15:03:11 +01:00
Jöran Karl
4f32b47075 settings: Add glob: as prefix for file globbing maps
This gives the advantage to differentiate internal options from user defined
file globs with the same name.
2026-03-19 19:35:24 +01:00
Jöran Karl
bcd6c81f50 settings: Don't return maps with ParsedSettings()
Map-typed values in the parsedSettings map do not represent settings for
individual options, they represent maps of settings for multiple options for
the given glob or ft: pattern, and their keys are not option names, they are
glob and ft: patterns. So do not expose them to the callers of ParsedSettings(),
to prevent the callers from mistakenly treating those patterns as option names,
with unpredicted consequences.

Co-authored-by: Dmytro Maluka <dmitrymaluka@gmail.com>
2026-03-18 19:46:36 +01:00
Jöran Karl
6760768b9e micro: Rearrange signal creation (#4027)
Otherwise we can't properly react upon screen events or signals created
within early plugin functions.
2026-03-17 20:45:10 +01:00
Jöran Karl
42d0ddf73d Merge pull request #4025 from injust/patch-1
Fix unescaped backslashes and expand git-config filename pattern to include `*/git/config`
2026-03-09 10:01:41 +01:00
Justin Su
5ddb05b5ec Apply suggestion from @Andriamanitra
Co-authored-by: Mikko <Andriamanitra@users.noreply.github.com>
2026-03-08 20:02:59 -04:00
Justin Su
ad43a5da99 Replace [\/] with [\\/] 2026-03-08 16:32:24 -04:00
Justin Su
dc10f6d53c Convert filename regexes to single-quoted strings 2026-03-08 16:31:24 -04:00
Kenny Wottrich
518a274980 Add detection for SWAG nginx proxy confs (#4036)
* Add detection for SWAG nginx proxy confs

These nginx config files have the format "appname.subdomain.conf" or "appname.subfolder.conf"

* Excape dot in regex

Co-authored-by: Mikko <Andriamanitra@users.noreply.github.com>

---------

Co-authored-by: Mikko <Andriamanitra@users.noreply.github.com>
2026-03-08 20:24:49 +01:00
Justin Su
184dd259e9 Match backslash too 2026-02-26 14:27:38 -05:00
Justin Su
f1a7f0d392 Expand git-config filename pattern to include */git/config 2026-02-26 06:36:51 -05:00
usfbih8u
d38f0dfe7a Fix documentation typo (#4006)
Co-authored-by: usfbih8u <>
2026-02-12 20:19:21 +01:00
Dmytro Maluka
4aa706cbc5 serialize: Don't save undo stack if saveundo=off (#4003)
Micro always saves the undo stack information into the serialized buffer
file as long as either `saveundo` or `savecursor` is enabled. Whereas in
the case when only `savecursor` is enabled, while `saveundo` is
disabled, this information is not used afterwards, so it only wastes the
disk space in `~/.config/micro/buffers`. (And given that currently micro
never automatically removes any serialized buffer files, it may
significantly contribute to the overall ever growing size of the
`~/.config/micro/buffers` directory.)

So avoid saving the undo info if `saveundo` is disabled. This makes the
size of each serialized buffer file with savecursor=on saveundo=off
small and predictable, e.g. around 600 bytes in my observations (whereas
without this fix, it may grow indefinitely big, depending on the number
of modifications the user made before saving the file).
2026-02-10 20:32:16 +01:00
Jöran Karl
1317a2deb1 Merge pull request #3983 from Neko-Box-Coder/FixRootSplitBug
Fixing missing case for handling root node for splitting
2026-02-09 18:39:01 +01:00
Neko Box Coder
fda43aff15 Adding special case for root node for flatten() 2026-02-08 21:57:50 +00:00
Neko Box Coder
7ef8ca476d Fixing missing case for handling root node for splitting, fixes #3980 2026-02-08 21:57:50 +00:00
hemmingsv
3a7403bde4 feat(textfilter): Select output if input was from selection (#3974) 2026-01-28 19:41:51 +01:00
20 changed files with 292 additions and 240 deletions

View File

@@ -363,6 +363,12 @@ func main() {
fmt.Println("Fatal: Micro could not initialize a Screen.")
exit(1)
}
util.Sigterm = make(chan os.Signal, 1)
sighup = make(chan os.Signal, 1)
signal.Notify(util.Sigterm, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGABRT)
signal.Notify(sighup, syscall.SIGHUP)
m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string))
clipErr := clipboard.Initialize(m)
@@ -400,6 +406,8 @@ func main() {
action.InitBindings()
action.InitCommands()
timerChan = make(chan func())
err = config.RunPluginFn("preinit")
if err != nil {
screen.TermMessage(err)
@@ -444,13 +452,6 @@ func main() {
screen.Events = make(chan tcell.Event)
util.Sigterm = make(chan os.Signal, 1)
sighup = make(chan os.Signal, 1)
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 {

View File

@@ -14,9 +14,8 @@ import (
func (*BufPane) Suspend() bool {
screenb := screen.TempFini()
// suspend the process
pid := syscall.Getpid()
err := syscall.Kill(pid, syscall.SIGSTOP)
// suspend the process group
err := syscall.Kill(0, syscall.SIGSTOP)
if err != nil {
screen.TermMessage(err)
}

View File

@@ -141,9 +141,11 @@ func (h *BufPane) TextFilterCmd(args []string) {
InfoBar.Error("usage: textfilter arguments")
return
}
for _, c := range h.Buf.GetCursors() {
sel := c.GetSelection()
if len(sel) == 0 {
fromSelection := len(sel) > 0
if !fromSelection {
c.SelectWord()
sel = c.GetSelection()
}
@@ -158,7 +160,18 @@ func (h *BufPane) TextFilterCmd(args []string) {
return
}
c.DeleteSelection()
h.Buf.Insert(c.Loc, bout.String())
insertStart := c.Loc
insertedText := bout.String()
h.Buf.Insert(c.Loc, insertedText)
if fromSelection {
// Select the pasted output if the input was selected
charCount := util.CharacterCountInString(insertedText)
insertEnd := insertStart.Move(charCount, h.Buf)
c.SetSelectionStart(insertStart)
c.SetSelectionEnd(insertEnd)
c.Loc = insertEnd
}
}
}

View File

@@ -165,7 +165,7 @@ func (b *SharedBuffer) calcHash(out *[md5.Size]byte) {
h := md5.New()
if len(b.lines) > 0 {
h.Write(b.lines[0].data())
h.Write(b.lines[0].data)
for _, l := range b.lines[1:] {
if b.Endings == FFDos {
@@ -173,7 +173,7 @@ func (b *SharedBuffer) calcHash(out *[md5.Size]byte) {
} else {
h.Write([]byte{'\n'})
}
h.Write(l.data())
h.Write(l.data)
}
}
@@ -866,7 +866,7 @@ func (b *Buffer) UpdateRules() {
if header.MatchFileName(b.Path) {
matchedFileName = true
}
if len(fnameMatches) == 0 && header.MatchFileHeader(b.lines[0].data()) {
if len(fnameMatches) == 0 && header.MatchFileHeader(b.lines[0].data) {
matchedFileHeader = true
}
} else if header.FileType == ft {
@@ -920,7 +920,7 @@ func (b *Buffer) UpdateRules() {
if header.MatchFileName(b.Path) {
fnameMatches = append(fnameMatches, syntaxFileInfo{header, f.Name(), nil})
}
if len(fnameMatches) == 0 && header.MatchFileHeader(b.lines[0].data()) {
if len(fnameMatches) == 0 && header.MatchFileHeader(b.lines[0].data) {
headerMatches = append(headerMatches, syntaxFileInfo{header, f.Name(), nil})
}
} else if header.FileType == ft {
@@ -953,7 +953,7 @@ func (b *Buffer) UpdateRules() {
for _, m := range matches {
if m.header.HasFileSignature() {
for i := 0; i < limit; i++ {
if m.header.MatchFileSignature(b.lines[i].data()) {
if m.header.MatchFileSignature(b.lines[i].data) {
syntaxFile = m.fileName
if m.syntaxDef != nil {
b.SyntaxDef = m.syntaxDef
@@ -1130,11 +1130,11 @@ func (b *Buffer) MoveLinesUp(start int, end int) {
if start < 1 || start >= end || end > len(b.lines) {
return
}
l := b.LineString(start - 1)
l := string(b.LineBytes(start - 1))
if end == len(b.lines) {
b.insert(
Loc{
len(b.lines[end-1].runes),
util.CharacterCount(b.lines[end-1].data),
end - 1,
},
[]byte{'\n'},
@@ -1155,7 +1155,7 @@ func (b *Buffer) MoveLinesDown(start int, end int) {
if start < 0 || start >= end || end >= len(b.lines) {
return
}
l := b.LineString(end)
l := string(b.LineBytes(end))
b.Insert(
Loc{0, start},
l+"\n",
@@ -1196,7 +1196,7 @@ func (b *Buffer) findMatchingBrace(braceType [2]rune, start Loc, char rune) (Loc
}
} else if char == braceType[1] {
for y := start.Y; y >= 0; y-- {
l := []rune(string(b.LineBytes(y)))
l := []rune(string(b.lines[y].data))
xInit := len(l) - 1
if y == start.Y {
xInit = start.X
@@ -1281,14 +1281,7 @@ func (b *Buffer) Retab() {
l = bytes.TrimLeft(l, " \t")
b.Lock()
ws = append(ws, l...)
var runes []Character
for len(ws) > 0 {
combc, s := util.DecodeCombinedCharacter(ws)
runes = append(runes, Character{combc})
ws = ws[s:]
}
b.lines[i].runes = runes
b.lines[i].data = append(ws, l...)
b.Unlock()
b.MarkModified(i, i)
@@ -1324,7 +1317,7 @@ func ParseCursorLocation(cursorPositions []string) (Loc, error) {
// Line returns the string representation of the given line number
func (b *Buffer) Line(i int) string {
return b.LineString(i)
return string(b.LineBytes(i))
}
func (b *Buffer) Write(bytes []byte) (n int, err error) {

View File

@@ -601,13 +601,24 @@ func (c *Cursor) SubWordLeft() {
// RuneUnder returns the rune under the given x position
func (c *Cursor) RuneUnder(x int) rune {
line := c.buf.LineCharacters(c.Y)
if len(line) == 0 || x >= len(line) {
line := c.buf.LineBytes(c.Y)
if len(line) == 0 || x >= util.CharacterCount(line) {
return '\n'
} else if x < 0 {
x = 0
}
return line[x].combc[0]
i := 0
for len(line) > 0 {
r, _, size := util.DecodeCharacter(line)
line = line[size:]
if i == x {
return r
}
i++
}
return '\n'
}
func (c *Cursor) StoreVisualX() {

View File

@@ -10,6 +10,28 @@ import (
"github.com/micro-editor/micro/v2/pkg/highlight"
)
// Finds the byte index of the nth rune in a byte slice
func runeToByteIndex(n int, txt []byte) int {
if n == 0 {
return 0
}
count := 0
i := 0
for len(txt) > 0 {
_, _, size := util.DecodeCharacter(txt)
txt = txt[size:]
count += size
i++
if i == n {
break
}
}
return count
}
// A searchState contains the search match info for a single line
type searchState struct {
search string
@@ -19,14 +41,10 @@ type searchState struct {
done bool
}
type Character struct {
combc []rune
}
// A Line contains the slice of runes as well as a highlight state, match
// A Line contains the data in bytes as well as a highlight state, match
// and a flag for whether the highlighting needs to be updated
type Line struct {
runes []Character
data []byte
state highlight.State
match highlight.LineMatch
@@ -41,24 +59,6 @@ type Line struct {
search map[*Buffer]*searchState
}
// data returns the line as byte slice
func (l Line) data() []byte {
var runes []rune
for _, r := range l.runes {
runes = append(runes, r.combc[0:]...)
}
return []byte(string(runes))
}
// String returns the line as string
func (l Line) String() string {
var runes []rune
for _, r := range l.runes {
runes = append(runes, r.combc[0:]...)
}
return string(runes)
}
const (
// Line ending file formats
FFAuto = 0 // Autodetect format
@@ -94,7 +94,7 @@ func Append(slice []Line, data ...Line) []Line {
return slice
}
// NewLineArray returns a new line array from an array of runes
// NewLineArray returns a new line array from an array of bytes
func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray {
la := new(LineArray)
@@ -144,16 +144,10 @@ func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray
loaded += dlen
}
var runes []Character
if err != nil {
if err == io.EOF {
for len(data) > 0 {
combc, s := util.DecodeCombinedCharacter(data)
runes = append(runes, Character{combc})
data = data[s:]
}
la.lines = Append(la.lines, Line{
runes: runes,
data: data,
state: nil,
match: nil,
})
@@ -161,14 +155,8 @@ func NewLineArray(size uint64, endings FileFormat, reader io.Reader) *LineArray
// Last line was read
break
} else {
data = data[:dlen-1]
for len(data) > 0 {
combc, s := util.DecodeCombinedCharacter(data)
runes = append(runes, Character{combc})
data = data[s:]
}
la.lines = Append(la.lines, Line{
runes: runes,
data: data[:dlen-1],
state: nil,
match: nil,
})
@@ -186,7 +174,7 @@ func (la *LineArray) Bytes() []byte {
// initsize should provide a good estimate
b.Grow(int(la.initsize + 4096))
for i, l := range la.lines {
b.Write(l.data())
b.Write(l.data)
if i != len(la.lines)-1 {
if la.Endings == FFDos {
b.WriteByte('\r')
@@ -200,13 +188,13 @@ 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{
runes: []Character{},
data: []byte{' '},
state: nil,
match: nil,
})
copy(la.lines[y+2:], la.lines[y+1:])
la.lines[y+1] = Line{
runes: []Character{},
data: []byte{},
state: la.lines[y].state,
match: nil,
}
@@ -217,65 +205,41 @@ func (la *LineArray) insert(pos Loc, value []byte) {
la.lock.Lock()
defer la.lock.Unlock()
var runes []Character
for len(value) > 0 {
combc, s := util.DecodeCombinedCharacter(value)
runes = append(runes, Character{combc})
value = value[s:]
}
x, y := util.Min(pos.X, len(la.lines[pos.Y].runes)), pos.Y
start := -1
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') {
la.split(Loc{x, y})
x = 0
y++
outer:
for i, r := range runes {
for j := 0; j < len(r.combc); j++ {
if r.combc[j] == '\n' || (r.combc[j] == '\r' && i < len(runes)-1 && r.combc[j+1] == '\n') {
la.split(Loc{x, y})
if i > 0 && start < len(runes) && start < i {
if start < 0 {
start = 0
}
la.insertRunes(Loc{x, y}, runes[start:i])
}
x = 0
y++
if r.combc[j] == '\r' {
i++
}
if i+1 <= len(runes) {
start = i + 1
}
continue outer
if value[i] == '\r' {
i++
}
continue
}
}
if start < 0 {
la.insertRunes(Loc{x, y}, runes)
} else if start < len(runes) {
la.insertRunes(Loc{x, y}, runes[start:])
la.insertByte(Loc{x, y}, value[i])
x++
}
}
// Inserts a rune array at a given location
func (la *LineArray) insertRunes(pos Loc, runes []Character) {
la.lines[pos.Y].runes = append(la.lines[pos.Y].runes, runes...)
copy(la.lines[pos.Y].runes[pos.X+len(runes):], la.lines[pos.Y].runes[pos.X:])
copy(la.lines[pos.Y].runes[pos.X:], runes)
// InsertByte inserts a byte at a given location
func (la *LineArray) insertByte(pos Loc, value byte) {
la.lines[pos.Y].data = append(la.lines[pos.Y].data, 0)
copy(la.lines[pos.Y].data[pos.X+1:], la.lines[pos.Y].data[pos.X:])
la.lines[pos.Y].data[pos.X] = value
}
// joinLines joins the two lines a and b
func (la *LineArray) joinLines(a, b int) {
la.insertRunes(Loc{len(la.lines[a].runes), a}, la.lines[b].runes)
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.insertRunes(Loc{0, pos.Y + 1}, la.lines[pos.Y].runes[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
@@ -289,10 +253,10 @@ func (la *LineArray) remove(start, end Loc) []byte {
defer la.lock.Unlock()
sub := la.Substr(start, end)
startX := util.Min(start.X, len(la.lines[start.Y].runes))
endX := util.Min(end.X, len(la.lines[end.Y].runes))
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
if start.Y == end.Y {
la.lines[start.Y].runes = append(la.lines[start.Y].runes[:startX], la.lines[start.Y].runes[endX:]...)
la.lines[start.Y].data = append(la.lines[start.Y].data[:startX], la.lines[start.Y].data[endX:]...)
} else {
la.deleteLines(start.Y+1, end.Y-1)
la.deleteToEnd(Loc{startX, start.Y})
@@ -304,12 +268,12 @@ func (la *LineArray) remove(start, end Loc) []byte {
// deleteToEnd deletes from the end of a line to the position
func (la *LineArray) deleteToEnd(pos Loc) {
la.lines[pos.Y].runes = la.lines[pos.Y].runes[:pos.X]
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X]
}
// deleteFromStart deletes from the start of a line to the position
func (la *LineArray) deleteFromStart(pos Loc) {
la.lines[pos.Y].runes = la.lines[pos.Y].runes[pos.X+1:]
la.lines[pos.Y].data = la.lines[pos.Y].data[pos.X+1:]
}
// deleteLine deletes the line number
@@ -323,35 +287,22 @@ func (la *LineArray) deleteLines(y1, y2 int) {
// Substr returns the string representation between two locations
func (la *LineArray) Substr(start, end Loc) []byte {
startX := util.Min(start.X, len(la.lines[start.Y].runes))
endX := util.Min(end.X, len(la.lines[end.Y].runes))
var runes []rune
if start.Y == end.Y && startX <= endX {
for _, r := range la.lines[start.Y].runes[startX:endX] {
runes = append(runes, r.combc[0:]...)
}
return []byte(string(runes))
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
if start.Y == end.Y {
src := la.lines[start.Y].data[startX:endX]
dest := make([]byte, len(src))
copy(dest, src)
return dest
}
var str []byte
for _, r := range la.lines[start.Y].runes[startX:] {
runes = append(runes, r.combc[0:]...)
}
str = append(str, []byte(string(runes))...)
str := make([]byte, 0, len(la.lines[start.Y+1].data)*(end.Y-start.Y))
str = append(str, la.lines[start.Y].data[startX:]...)
str = append(str, '\n')
for i := start.Y + 1; i <= end.Y-1; i++ {
runes = runes[:0]
for _, r := range la.lines[i].runes {
runes = append(runes, r.combc[0:]...)
}
str = append(str, []byte(string(runes))...)
str = append(str, la.lines[i].data...)
str = append(str, '\n')
}
runes = runes[:0]
for _, r := range la.lines[end.Y].runes[:endX] {
runes = append(runes, r.combc[0:]...)
}
str = append(str, []byte(string(runes))...)
str = append(str, la.lines[end.Y].data[:endX]...)
return str
}
@@ -368,38 +319,15 @@ func (la *LineArray) Start() Loc {
// End returns the location of the last character in the buffer
func (la *LineArray) End() Loc {
numlines := len(la.lines)
return Loc{len(la.lines[numlines-1].runes), numlines - 1}
}
// LineCharacters returns line n as an array of characters
func (la *LineArray) LineCharacters(n int) []Character {
if n >= len(la.lines) || n < 0 {
return []Character{}
}
return la.lines[n].runes
return Loc{util.CharacterCount(la.lines[numlines-1].data), numlines - 1}
}
// 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()
}
// LineString returns line n as an string
func (la *LineArray) LineString(n int) string {
if n >= len(la.lines) || n < 0 {
return string("")
}
var runes []rune
for _, r := range la.lines[n].runes {
runes = append(runes, r.combc[0:]...)
}
return string(runes)
return la.lines[lineN].data
}
// State gets the highlight state for the given line number
@@ -481,7 +409,7 @@ func (la *LineArray) SearchMatch(b *Buffer, pos Loc) bool {
if !s.done {
s.match = nil
start := Loc{0, lineN}
end := Loc{len(la.lines[lineN].runes), lineN}
end := Loc{util.CharacterCount(la.lines[lineN].data), lineN}
for start.X < end.X {
m, found, _ := b.FindNext(b.LastSearch, start, end, start, true, b.LastSearchRegex)
if !found {

View File

@@ -9,7 +9,7 @@ type Loc struct {
X, Y int
}
// LessThan returns true if b is smaller
// LessThan returns true if l is smaller than b
func (l Loc) LessThan(b Loc) bool {
if l.Y < b.Y {
return true
@@ -17,7 +17,7 @@ func (l Loc) LessThan(b Loc) bool {
return l.Y == b.Y && l.X < b.X
}
// GreaterThan returns true if b is bigger
// GreaterThan returns true if l is bigger than b
func (l Loc) GreaterThan(b Loc) bool {
if l.Y > b.Y {
return true
@@ -25,7 +25,7 @@ func (l Loc) GreaterThan(b Loc) bool {
return l.Y == b.Y && l.X > b.X
}
// GreaterEqual returns true if b is greater than or equal to b
// GreaterEqual returns true if l is greater than or equal to b
func (l Loc) GreaterEqual(b Loc) bool {
if l.Y > b.Y {
return true
@@ -36,7 +36,7 @@ func (l Loc) GreaterEqual(b Loc) bool {
return l == b
}
// LessEqual returns true if b is less than or equal to b
// LessEqual returns true if l is less than or equal to b
func (l Loc) LessEqual(b Loc) bool {
if l.Y < b.Y {
return true
@@ -113,14 +113,13 @@ func (l Loc) left(buf *LineArray) Loc {
// MoveLA moves the cursor n characters to the left or right
// It moves the cursor left if n is negative
func (l Loc) MoveLA(n int, buf *LineArray) Loc {
if n > 0 {
for i := 0; i < n; i++ {
l = l.right(buf)
}
return l
for n > 0 {
l = l.right(buf)
n--
}
for i := 0; i < util.Abs(n); i++ {
for n < 0 {
l = l.left(buf)
n++
}
return l
}

View File

@@ -2,6 +2,7 @@ package buffer
import (
"bufio"
"bytes"
"errors"
"io"
"io/fs"
@@ -10,7 +11,6 @@ import (
"os/signal"
"path/filepath"
"runtime"
"strings"
"time"
"unicode"
@@ -156,7 +156,7 @@ func (wf wrappedFile) Write(b *SharedBuffer) (int, error) {
}
// write lines
size, err := file.Write(b.lines[0].data())
size, err := file.Write(b.lines[0].data)
if err != nil {
return 0, err
}
@@ -165,10 +165,10 @@ func (wf wrappedFile) Write(b *SharedBuffer) (int, error) {
if _, err = file.Write(eol); err != nil {
return 0, err
}
if _, err = file.Write(l.data()); err != nil {
if _, err = file.Write(l.data); err != nil {
return 0, err
}
size += len(eol) + len(l.data())
size += len(eol) + len(l.data)
}
err = file.Flush()
@@ -249,9 +249,10 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
if !autoSave && b.Settings["rmtrailingws"].(bool) {
for i, l := range b.lines {
leftover := strings.TrimRightFunc(l.String(), unicode.IsSpace)
linelen := len(l.runes)
b.Remove(Loc{len(leftover), i}, Loc{linelen, i})
leftover := util.CharacterCount(bytes.TrimRightFunc(l.data, unicode.IsSpace))
linelen := util.CharacterCount(l.data)
b.Remove(Loc{leftover, i}, Loc{linelen, i})
}
b.RelocateCursors()

View File

@@ -29,12 +29,16 @@ func (b *Buffer) Serialize() error {
return nil
}
buffer := SerializedBuffer{
Cursor: b.GetActiveCursor().Loc,
ModTime: b.ModTime,
}
if b.Settings["saveundo"].(bool) {
buffer.EventHandler = b.EventHandler
}
var buf bytes.Buffer
err := gob.NewEncoder(&buf).Encode(SerializedBuffer{
b.EventHandler,
b.GetActiveCursor().Loc,
b.ModTime,
})
err := gob.NewEncoder(&buf).Encode(buffer)
if err != nil {
return err
}
@@ -76,7 +80,7 @@ func (b *Buffer) Unserialize() error {
b.StartCursor = buffer.Cursor
}
if b.Settings["saveundo"].(bool) {
if b.Settings["saveundo"].(bool) && buffer.EventHandler != nil {
// We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime
if b.ModTime == buffer.ModTime {
b.EventHandler = buffer.EventHandler

View File

@@ -186,11 +186,19 @@ func validateParsedSettings() error {
}
}
} else {
if _, e := glob.Compile(k); e != nil {
err = errors.New("Error with glob setting " + k + ": " + e.Error())
tk := strings.TrimPrefix(k, "glob:")
if _, e := glob.Compile(tk); e != nil {
err = errors.New("Error with glob setting " + tk + ": " + e.Error())
delete(parsedSettings, k)
continue
}
if !strings.HasPrefix(k, "glob:") {
// Support non-prefixed glob settings but internally convert
// them to prefixed ones for simplicity.
delete(parsedSettings, k)
k = "glob:" + k
parsedSettings[k] = v
}
for k1, v1 := range v.(map[string]any) {
if _, ok := defaults[k1]; ok {
if e := verifySetting(k1, v1, defaults[k1]); e != nil {
@@ -256,6 +264,9 @@ func ReadSettings() error {
func ParsedSettings() map[string]any {
s := make(map[string]any)
for k, v := range parsedSettings {
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") {
continue
}
s[k] = v
}
return s
@@ -309,8 +320,9 @@ func InitGlobalSettings() error {
// Must be called after ReadSettings
func UpdatePathGlobLocals(settings map[string]any, path string) {
for k, v := range parsedSettings {
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") && !strings.HasPrefix(k, "ft:") {
g, _ := glob.Compile(k)
if strings.HasPrefix(reflect.TypeOf(v).String(), "map") && strings.HasPrefix(k, "glob:") {
tk := strings.TrimPrefix(k, "glob:")
g, _ := glob.Compile(tk)
if g.MatchString(path) {
for k1, v1 := range v.(map[string]any) {
settings[k1] = v1

View File

@@ -79,7 +79,7 @@ func (w *BufWindow) GetView() *View {
return w.View
}
// GetView sets the view.
// SetView sets the view.
func (w *BufWindow) SetView(view *View) {
w.View = view
}

View File

@@ -29,27 +29,11 @@ func isMark(r rune) bool {
// DecodeCharacter returns the next character from an array of bytes
// A character is a rune along with any accompanying combining runes
func DecodeCharacter(b []byte) (rune, []rune, int) {
combc, size := DecodeCombinedCharacter(b)
return combc[0], combc[1:], size
}
// DecodeCharacterInString returns the next character from a string
// A character is a rune along with any accompanying combining runes
func DecodeCharacterInString(str string) (rune, []rune, int) {
combc, size := DecodeCombinedCharacterInString(str)
return combc[0], combc[1:], size
}
// DecodeCombinedCharacter returns the next combined character
// from an array of bytes
// A character is a rune along with any accompanying combining runes
func DecodeCombinedCharacter(b []byte) ([]rune, int) {
var combc []rune
r, size := utf8.DecodeRune(b)
combc = append(combc, r)
b = b[size:]
c, s := utf8.DecodeRune(b)
var combc []rune
for isMark(c) {
combc = append(combc, c)
size += s
@@ -58,18 +42,17 @@ func DecodeCombinedCharacter(b []byte) ([]rune, int) {
c, s = utf8.DecodeRune(b)
}
return combc, size
return r, combc, size
}
// DecodeCombinedCharacterInString is the same as DecodeCombinedCharacter
// but for strings
func DecodeCombinedCharacterInString(str string) ([]rune, int) {
var combc []rune
// DecodeCharacterInString returns the next character from a string
// A character is a rune along with any accompanying combining runes
func DecodeCharacterInString(str string) (rune, []rune, int) {
r, size := utf8.DecodeRuneInString(str)
combc = append(combc, r)
str = str[size:]
c, s := utf8.DecodeRuneInString(str)
var combc []rune
for isMark(c) {
combc = append(combc, c)
size += s
@@ -78,7 +61,7 @@ func DecodeCombinedCharacterInString(str string) ([]rune, int) {
c, s = utf8.DecodeRuneInString(str)
}
return combc, size
return r, combc, size
}
// CharacterCount returns the number of characters in a byte array

View File

@@ -413,7 +413,7 @@ func (n *Node) HSplit(bottom bool) uint64 {
if !n.IsLeaf() {
return 0
}
if n.Kind == STUndef {
if n.parent == nil {
n.Kind = STVert
}
if n.Kind == STVert {
@@ -429,13 +429,13 @@ func (n *Node) VSplit(right bool) uint64 {
if !n.IsLeaf() {
return 0
}
if n.Kind == STUndef {
if n.parent == nil {
n.Kind = STHoriz
}
if n.Kind == STVert {
return n.vVSplit(right)
if n.Kind == STHoriz {
return n.hVSplit(0, right)
}
return n.hVSplit(0, right)
return n.vVSplit(right)
}
// unsplits the child of a split
@@ -483,7 +483,20 @@ func (n *Node) Unsplit() bool {
// 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 {
if len(n.children) != 1 {
return
}
// Special case for root node
if n.parent == nil {
*n = *n.children[0]
n.parent = nil
for _, c := range n.children {
c.parent = n
}
if len(n.children) == 0 {
n.Kind = STUndef
}
return
}
@@ -531,11 +544,19 @@ func (n *Node) flatten() {
func (n *Node) String() string {
var strf func(n *Node, ident int) string
strf = func(n *Node, ident int) string {
marker := "|"
marker := ""
if n.Kind == STHoriz {
marker = "-"
} else if n.Kind == STVert {
marker = "|"
}
str := fmt.Sprint(strings.Repeat("\t", ident), marker, n.View, n.id)
var parentId uint64 = 0
if n.parent != nil {
parentId = n.parent.id
}
str := fmt.Sprint(strings.Repeat("\t", ident), marker, n.View, n.id, parentId)
if n.IsLeaf() {
str += "🍁"
}

View File

@@ -669,6 +669,21 @@ all files except Go files, and `tabsize` 4 for all files except Ruby files:
Or similarly you can match with globs:
```json
{
"glob:*.go": {
"tabstospaces": false
},
"glob:*.rb": {
"tabsize": 2
},
"tabstospaces": true,
"tabsize": 4
}
```
You can also omit the `glob:` prefix before globs:
```json
{
"*.go": {
@@ -681,3 +696,6 @@ Or similarly you can match with globs:
"tabsize": 4
}
```
But it is generally more recommended to use the `glob:` prefix, as it avoids
potential conflicts with option names.

View File

@@ -1,7 +1,7 @@
filetype: git-commit
detect:
filename: "^(.*[\\/])?(COMMIT_EDITMSG|TAG_EDITMSG|MERGE_MSG)$"
filename: '^(.*[\\/])?(COMMIT_EDITMSG|TAG_EDITMSG|MERGE_MSG)$'
rules:
# File changes

View File

@@ -1,7 +1,7 @@
filetype: git-config
detect:
filename: "git(config|modules)$|\\.git/config$"
filename: 'git(config|modules)$|^(.*[\\/])?\.?git[\\/]config$'
rules:
- constant: "\\<(true|false)\\>"

View File

@@ -1,7 +1,7 @@
filetype: git-rebase-todo
detect:
filename: "^(.*[\\/])?git\\-rebase\\-todo$"
filename: '^(.*[\\/])?git\-rebase\-todo$'
rules:
# Rebase commands

69
runtime/syntax/gleam.yaml Normal file
View File

@@ -0,0 +1,69 @@
filetype: gleam
detect:
filename: "\\.gleam$"
rules:
- identifier: "\\b[a-z][a-z0-9_]*\\b"
- statement: "\\b(as|assert|auto|case|const|delegate|derive|echo|else|fn|if|implement|import|let|macro|opaque|panic|pub|test|todo|type|use)\\b"
- type: "\\b[A-Z][a-zA-Z0-9_]*\\b"
- type: "\\b(Int|Float|String|Bool|List|Option|Result|BitArray)\\b"
- constant: "\\b(True|False|Nil)\\b"
- preproc: "@[a-z][a-z_]*"
- statement: "(\\|>|->|<-)"
- statement: "(\\.\\.|<>)"
- statement: "(==|!=|<=\\.|>=\\.|<\\.|>\\.|<=|>=)"
- statement: "(&&|\\|\\|)"
- statement: "(\\+\\.|-\\.|\\*\\.|/\\.|\\+|-|\\*|/|%)"
- statement: "(=|<|>|!|<<|>>)"
- constant.number: "\\b0b[01](_?[01])*\\b"
- constant.number: "\\b0o[0-7](_?[0-7])*\\b"
- constant.number: "\\b0x[0-9a-fA-F](_?[0-9a-fA-F])*\\b"
- constant.number: "\\b[0-9](_?[0-9])*(\\.[0-9](_?[0-9])*)?([eE][+-]?[0-9](_?[0-9])*)?\\b"
- default:
start: "#\\("
end: "\\)"
limit-group: special
rules:
- identifier: "\\b[a-z][a-z0-9_]*\\b"
- statement: "\\b(as|assert|auto|case|const|delegate|derive|echo|else|fn|if|implement|import|let|macro|opaque|panic|pub|test|todo|type|use)\\b"
- type: "\\b[A-Z][a-zA-Z0-9_]*\\b"
- type: "\\b(Int|Float|String|Bool|List|Option|Result|BitArray)\\b"
- constant: "\\b(True|False|Nil)\\b"
- statement: "(\\|>|->|<-)"
- statement: "(\\.\\.|<>)"
- statement: "(==|!=|<=\\.|>=\\.|<\\.|>\\.|<=|>=)"
- statement: "(&&|\\|\\|)"
- statement: "(\\+\\.|-\\.|\\*\\.|/\\.|\\+|-|\\*|/|%)"
- statement: "(=|<|>|!|<<|>>)"
- constant.number: "\\b0b[01](_?[01])*\\b"
- constant.number: "\\b0o[0-7](_?[0-7])*\\b"
- constant.number: "\\b0x[0-9a-fA-F](_?[0-9a-fA-F])*\\b"
- constant.number: "\\b[0-9](_?[0-9])*(\\.[0-9](_?[0-9])*)?([eE][+-]?[0-9](_?[0-9])*)?\\b"
- constant.string:
start: '"'
end: '"'
skip: "\\\\."
rules:
- constant.specialChar: "\\\\."
- constant.string:
start: '"'
end: '"'
skip: "\\\\."
rules:
- constant.specialChar: "\\\\."
- comment:
start: "//"
end: "$"
rules:
- todo: "(TODO|FIXME|XXX):?"

View File

@@ -1,7 +1,7 @@
filetype: nginx
detect:
filename: "nginx.*\\.conf$|\\.nginx$"
filename: "nginx.*\\.conf$|\\.nginx$|\\.sub(domain|folder)\\.conf$"
header: "^(server|upstream)[a-z ]*\\{$"
rules:

View File

@@ -1,7 +1,7 @@
filetype: ruby
detect:
filename: "\\.(rb|rake|gemspec)$|^(.*[\\/])?(Gemfile|config.ru|Rakefile|Capfile|Vagrantfile|Guardfile|Appfile|Fastfile|Pluginfile|Podfile|\\.?[Bb]rewfile)$"
filename: '\.(rb|rake|gemspec)$|^(.*[\\/])?(Gemfile|config\.ru|Rakefile|Capfile|Vagrantfile|Guardfile|Appfile|Fastfile|Pluginfile|Podfile|\.?[Bb]rewfile)$'
header: "^#!.*/(env +)?ruby( |$)"
rules: